植物明星大乱斗——功能拆解
能帮到你的话,就给个赞吧 😘
本文不做组件实现,只在组件基础上做 功能拆解以及框架实现
文章目录
- 游戏框架与概念
- 游戏框架
- 单帧
- 场景&场景管理器
- 摄像机
- 画面震动
- 定时相关
- 攻击cd
- 无敌
- 起跳&落地动画
- 物理模拟
- 重力
- 玩家
- 抛物线
- 碰撞检测
- 点与矩形
- 矩形与矩形
- 粒子特效
- 粒子
- 粒子产生器
游戏框架与概念
游戏框架
直接定位游戏过于抽象,我们先来定位于具体的一帧。
一帧是什么?带你清晰下
= 数据 + 画面
你所看到的每一帧背后都是数据,而画面则是基于数据渲染而来。
也就是 在代码层面,更新数据 + 渲染画面。即为一帧。
//t: 时间,要体现数据随时间变化
oneFrame = update(t) + render();
在此基础上,加上循环,便可实现 连续播放。
while
oneFrame = update(t) + render();
没错,一句话加上循环便是游戏的框架。代码化为
while (1) {
//数据
update(t);
//画面
render();
}
在此之上加上固定60帧。便实现流畅的游戏了
#define FPS 1000/60
while (1) {
auto frameBeginTime = now();
//数据
update(t);
//画面
render();
auto frameTime = now() - frameBeginTime;
if (frameTime < FPS)
sleep(FPS - frameTime);
}
单帧
似乎你已经非常清晰游戏的逻辑了,但其实依然模糊,因为你非常连续,而不离散。因为你依然可以拆分。
一秒是什么?
= 60帧。化为60点,[t0, t59],这便是一秒。 在这一秒内,我们要渲染[t0, t59] 这60个画面。
也就是任意时刻的一帧t0,经过一段时间后要等于 t1。即 t0 + Δt = t1。
即 t0 + update(Δt) = t1;
所以,游戏里 Δt和 帧 便组成了游戏。这也是为什么update(t)的参数需要有个t。还不严谨,应该是Δt。所以正确的代码应该是
#define FPS 1000/60
while (1) {
auto frameBeginTime = now();
//数据
update(Δt);
//画面
render();
auto frameTime = now() - frameBeginTime;
if (frameTime < FPS)
sleep(FPS - frameTime);
}
任意一帧进入循环时还是t0,经过update(Δt)后,便是t1的数据了。
至此,你才理解游戏的框架与帧与时间。
场景&场景管理器
面向对象中,单写update和render是不行的,而是要写清楚对象。
身为第九艺术,场景自然是其对象。
#define FPS 1000/60
while (1) {
auto frameBeginTime = now();
//数据
scene.update(Δt);
//画面
scene.render();
auto frameTime = now() - frameBeginTime;
if (frameTime < FPS)
sleep(FPS - frameTime);
}
自然的,在众多场景中,需要一个场景管理器统一管理,亦如导演一样。即
#define FPS 1000/60
while (1) {
auto frameBeginTime = now();
//数据
sceneManager.update(Δt);
//画面
sceneManager.render();
auto frameTime = now() - frameBeginTime;
if (frameTime < FPS)
sleep(FPS - frameTime);
}
场景管理器最重要的功能莫过于切换。
即
public:
void switchTo(Scene* scene); //切换到scene
但是切换这个功能又由谁来调用呢,没错,场景。
所以这是个交叉引用,即场景管理器需要调用场景,而场景又需要调用场景管理器。
交叉引用实现有很多,但本文意不在此,仅注重功能分析。
那么接下来,调用场景管理器切换,如在gameScene的update中
void GameScene::update(int Δt){
//玩家生命值0时,切换到菜单场景。
if(player->getHp() == 0)
sceneManager.switchTo(menuScene);
}
但此时,游戏场景内还需要存有菜单场景的引用,这就牵扯到场景内互相引用了,我们不希望场景之间互相引用,因为会非常混乱。所以,场景管理器的声明应如下
public:
void switchTo(SceneType sceneType); //切换到scene
使用枚举类来取代直接引用。
摄像机
如同场景一样,我们希望由摄像机来显示画面,而不是直接显示。
如显示玩家
void render(){
//直接显示玩家
putimage(player.x, player.y);
}
void render(){
//摄像机间接显示玩家
putimage(player.x - camera.x, player.y - camera.y);
}
这样,当移动摄像机时,player的画面也会随之移动。
void render(){
camera.moveUp(1); //摄像机向上移动1单位,player画面也会向上移动1单位。
putimage(player.x - camera.x, player.y - camera.y);
}
画面震动
那么,当所有画面都有摄像机参与时,实现震动便简单多了
camera.shake();
render();
定时相关
下面这些功能都与定时相关。
如攻击cd,无敌时间,起跳&落地动画。游戏中还有许多功能与定时有关,暂不列举。
攻击cd
我们希望玩家攻击后不能再攻击,cd过后再攻击。
if(isCanAttack){
//攻击
attack();
//攻击后不能再攻击
isCanAttack = false;
//开始计时
timer.timer(Δt);
}
timer.setCallBack(isCanAttack = true);
无敌
我们希望玩家受击后不能再受击,cd过后再受击。
if(isCanBeHit){
//受击
beHit();
//受击后不能再受击
isCanBeHit= false;
//开始计时
timer.timer(Δt);
}
timer.setCallBack(isCanBeHit = true);
起跳&落地动画
我们希望动画播放一段时间后不能再播放。以起跳为例。
注,之前是cd过后希望再重复,而动画则是不再重复。
if(isCanPlay){
//播放
jumpPlay();
//开始计时
timer.timer(Δt);
}
timer.setCallBack(isCanPlay= false);
物理模拟
二维就如同初中的物理题一样,仅是xy方向上的位移,速度,与 时间 而已。
Vector2 position, velocity; //位移,速度
Δt; //时间
重力
#define g -9.8
玩家
void Player::update(Δt){
//玩家速度
//添加重力速度即可模拟重力
velocity.y += g * Δt;
//玩家位移
position += velocity * Δt;
}
抛物线
//抛物线初始速度
velocity.x = x, velocity.y = x;
void sunBullet::update(Δt){
//速度
velocity.y += g * Δt;
//位移
position += velocity * Δt;
}
碰撞检测
游戏中仅用到两种碰撞,二维应该也就这两种碰撞吧。
rectangle{
x1,y1; //左上角
x2,y2; //右下角
width;
height;
}
点与矩形
bool checkCollision(rectangle target){
//仅需检测x 是否在 target内,y是否在target内
auto collisionX = x >= target.x1 && x <= target.x2;
auto collisionY = y >= target.y1 && y <= target.y2;
return collisionX && collisionY;
}
矩形与矩形
bool checkCollision(rectangle target){
//仅需检测两个矩形xy是否相交即可
auto collisionX = max(x2, target.x2) - min(x1, target.x1) <= width + target.width;
auto collisionY = max(y2, target.y2) - min(y1, target.y1) <= height + target.height;
return collisionX && collisionY;
}
粒子特效
粒子特效由两部分组成
粒子
粒子产生器
粒子是一个有生命期的对象,过完生命期便消失
粒子产生器则负责产生粒子
class Player{
private:
particles; //粒子
particleGeneration; //粒子生成器
private:
void run(Δt){
//跑动
...
//跑动时,生成粒子
particleGeneration.generation(Δt);
}
public:
void update(Δt){
//玩家按下跑动键-生成粒子
if(isRun)
run(Δt);
//更新粒子
//粒子生成后便需一直更新
for (auto& particle : particles)
particle.update(Δt);
//移除失效粒子
particles.erase(std::remove_if(particles.begin(), particles.end(), [](const Particle& particle) {
return !particle.checkValid();
}), particles.end());
}
}
粒子
void Particle::update(Δt){
time += Δt;
//大于生命期则失效
if (time > lifeSpan){
isValid = false;
return ;
}
}
bool Particle::checkValid() const{
return isValid;
}
粒子产生器
void Generation::generation(Δt){
//Δt内一直生成粒子
while(Δt > 0){
particles.push_back(Particle());
Δt -= interval;
}
}