状态机设计模式本身有比较多的基础理论在里面,但在这里,我们游戏所用的有限状态机FSMs,我们用一种简单的方式,来学习和分析。当我们知道了,为什么要用,有什么优点后,对状态机的理解就容易多了。当然在下面的例子中,我们的代码并不是完美的,有很多细节是需要自己去完善的。原贴:
http://gameprogrammingpatterns.com/state.html
现在就让我们开始吧:)
假象一下,我们在开发一个横版卷轴类游戏。我们的工作时实现一个英雄角色在游戏世界里面运动。也就是说,我们的角色需要响应我们的玩家的输入。如按下B键,角色会跳起来,代码如下:
void Heroine::handleInput(Input input){
if (input == PRESS_B)
{
yVelocity_ = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
}}
发现BUG没有?
上面的代码没有禁止掉空中再次跳跃的情况,如果玩家一直按B,那么角色就会一直往天上飞,不落下来了。最简单的修正这个BUG的方式就是,添加一个IsJumping_的boolean变量。当玩家跳跃的时候做检查:
void Heroine::handleInput(Input input){
if (input == PRESS_B)
{
if (!isJumping_)
{
isJumping_ = true;
// Jump...
}
}}
下一步,当角色在地面上时,玩家按下 Down 键时,让角色蹲下,当玩家释放开Down键时,角色站立起来:
void Heroine::handleInput(Input input){
if (input == PRESS_B)
{
// Jump if not jumping...
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
setGraphics(IMAGE_DUCK);
}
}
else if (input == RELEASE_DOWN)
{
setGraphics(IMAGE_STAND);
}}
发现BUG没有?
通过上面实现的代码,玩家可以做以下的操作:
按下Down键,角色蹲下。
1.按下B 键,角色蹲着的地方飞了起来。
3角色在空中,释放开Down键后。
这几步操作完后,就会发现,玩家在跳跃的空中,恢复到了站立的动画。
为了解决这个BUG,我们只有再加一个标志。。。
void Heroine::handleInput(Input input){
if (input == PRESS_B)
{
if (!isJumping_ && !isDucking_)
{
// Jump...
}
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
isDucking_ = true;
setGraphics(IMAGE_DUCK);
}
}
else if (input == RELEASE_DOWN)
{
if (isDucking_)
{
isDucking_ = false;
setGraphics(IMAGE_STAND);
}
}}
下一步,如果能让玩家在跳跃到空中的时候,按down键,来一个俯冲攻击,应该很酷:
void Heroine::handleInput(Input input){
if (input == PRESS_B)
{
if (!isJumping_ && !isDucking_)
{
// Jump...
}
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
isDucking_ = true;
setGraphics(IMAGE_DUCK);
}
else
{
isJumping_ = false;
setGraphics(IMAGE_DIVE);
}
}
else if (input == RELEASE_DOWN)
{
if (isDucking_)
{
// Stand...
}
}}
找BUG时间又到了,找到了没?
我们之前保证了玩家在跳跃的时候不能再次跳跃,但是现在玩家在俯冲的时候,又可以跳跃了。我们又只有再加一个条件。。。是不是觉得要疯掉了。
这个时候,我们应该能感觉的很明显了,我们现在的解决方案是有问题。每一次在这个段代码上添加新的需求,就会破坏之前的一些逻辑。按照这种情况持续下去,就算是吧需求做完了,后面应该会有一堆的BUG等着我们去修改。
这个时候 有限状态机 就是我们的救命稻草了。
在经历一些纠结和挫折之后,你把桌面上的所有东西都清理干净,然后只留下一个笔和一张纸,开始画一个流程图。你画上了一个个盒子,这些盒子代表着玩家的各种行为状态:站立中,跳跃中,下蹲中,俯冲中。当按键按下,从一个状态变化到另一个状态时,我们用一个条件加一根线,指向我们将切换到的新的状态。
恭喜你,你已经成功的创建了有限状态机了。其实这个来自于计算机分支科学:图灵机或者自动机理论。在这些理论中,有限状态机是最简单的理论之一。
它的要点如下:
l 你需要一些固定的状态。在我们的例子里面就是站立,跳跃中,下蹲中,俯冲中。
l 同一个时刻只能在一个状态里面。我们的例子里面,角色是不能同时跳跃中和站立中的。事实上,避免同时多个状态的情况,是我们使用FSM的一个核心原因之一。
l 一序列输入或者事件发送给状态机。在我们的例子里面,就是按键的按下或者释放事件。
l 状态机中的状态之间可以互相转换,而转换的条件就是上面说的输入或者事件。当我们输入事件来了后,如果当前的状态能够接受该输入,那么就会按照当前的输入,转换到对应的状态中去。
例如,在图中,角色在Standing状态时,收到按下 down 键事件,那么就会转换到Ducking状态。当玩家在Jumping状态时,按下down键事件,那么就会转换到Diving状态。
如果在当前状态收到一个没有定义的输入事件,那么忽略该事件。
简单的实现方式,用Enums和Swithces
其中一个问题是我们的Heroine类中的boolean变量们,像isJumping_和isDucking_他们应该是不能同时为true的。当我们有一堆变量都是boolean,但是同一时刻只能一个为True的话,就说明我们需要用Enums了。
enum State
{
STATE_STANDING,
STATE_JUMPING,
STATE_DUCKING,
STATE_DIVING
};
用枚举State一个就代替了我们的一堆的是否标识的布尔变量。在前面的代码中,我们是用按照输入来作为条件判断,然后再切的状态。这样能把一个输入下,需要做的事情放在了一起,但是却让多个状态的代码混淆在了一起。现在我们想要一个状态的代码整合在一起,那么我就需要先用状态来做switch:
void Heroine::handleInput(Input input){
switch (state_)
{
case STATE_STANDING://这里将一个状态的代码放在了一起
if (input == PRESS_B)
{
state_ = STATE_JUMPING;
yVelocity_ = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
}
else if (input == PRESS_DOWN)
{
state_ = STATE_DUCKING;
setGraphics(IMAGE_DUCK);
}
break;
case STATE_JUMPING:
if (input == PRESS_DOWN)
{
state_ = STATE_DIVING;
setGraphics(IMAGE_DIVE);
}
break;
case STATE_DUCKING:
if (input == RELEASE_DOWN)
{
state_ = STATE_STANDING;
setGraphics(IMAGE_STAND);
}
break;
}}
这个改变看起来好像没有太大的用,但事实上却是有很大进步。虽然代码中,还是存在不少的条件分支,但是我们将不容易理解的状态这个相应的代码整合在了一起。大家可以对比下前面的代码和这段代码,新的代码在理解上清晰明了了很多。这个就是最简单的实现FSM的方法。
我们的游戏可能会再进一步的扩展,如,我们想要角色在蹲下的时候自动聚气,一定时间后能够释放一个特殊的攻击技能。那么在角色蹲下的时候,我们需要记录一个聚气的时间。
我们在HeroIne中添加一个chargeTime_的变量,用来记录这个时间。同时,我们假象我们已经有了一个每帧可以更新的update()函数。那么我们就可以如下实现:
void Heroine::update(){
if (state_ == STATE_DUCKING)
{
chargeTime_++;
if (chargeTime_ > MAX_CHARGE)
{
superBomb();
}
}}
同时我们需要在角色开始蹲下的时候,重置这个时间,修改handleInput():
void Heroine::handleInput(Input input){
switch (state_)
{
case STATE_STANDING:
if (input == PRESS_DOWN)
{
state_ = STATE_DUCKING;
chargeTime_ = 0;
setGraphics(IMAGE_DUCK);
}
// Handle other inputs...
break;
// Other states...
}}
好,我们的需求修改完毕。我们只修改了2个函数,同时添加了一个chargeTIme_变量。我们的代码其实已经比较清晰了,但是我们这里添加的变量chargetTIme其实只是对蹲下状态有效的。那么下篇文章中看看我们如果处理,能有更好的整合效果把:)