前言:状态机模式是一个游戏常用的经典设计模式,常被用作管理一种物体的各种状态(例如管理人物的行走,站立,跳跃等状态)。
(Unity里的Animator就是一种典型的状态机,用于控制动画状态之间的切换)
假如我们正在开发一款动作游戏,当前的任务是实现根据输入来控制主角的行为——当按下B键时,他应该跳跃。
直观的代码:
if (input == PRESS_B) { if (!m_isJumping) { m_isJumping = true; Jump();//跳跃的代码 } }
后来我们需要添加更多行为了,所有行为如下:
站立时按下 ↓ 键 =》 蹲下。
蹲下时按下 ↓ 键 =》 站立。
站立时按下 B 键 =》 跳跃。
跳跃时按下 ↓ 键 =》触发 俯冲。
if (input == PRESS_B) { //如果在站立时且没在跳跃,则跳跃 if (!m_isJumping && m_isStanding) { m_isJumping = true; player.jump();//跳跃的代码 } } else if (input == PRESS_DOWN) { //如果在跳跃时且没在俯冲,则俯冲 if (m_isJumping && !m_isDiving) { m_isDiving player.dive();//俯冲的代码 } //如果没在跳跃 else if (!m_isJumping) { //如果站立时,则蹲下 if (m_isStanding) { m_isStanding = false; player.sneak();//蹲下的代码 } //如果蹲下时,则站立 else { m_isStanding = true; player.stand();//站立的代码 } } }
可以看到一堆if-else语句非常复杂,要是添加更多行为,其逻辑结构更加难以维护,而且主角的代码又得重新编译(耦合性大)
有限状态机
有限状态:有限数量的状态。
一个可行的办法是将这些 状态&状态切换&状态对应的行为 封装成类,
(如下图)
这时候可以借助状态机这个设计模式来美化这段代码。
上面的场景中,只有4个状态(跳跃/下蹲/站立/俯冲),这就是有限状态。
于是我们设计出下面4个状态类(加一个状态的接口类):
//状态接口类 class State { public: //处理输入,然后根据输入转换相应的状态 virtual void handleInput(Player& player,const Input& input) = 0; }; //站立状态 class StandState : public State { public: void handleInput(Player& player, const Input& input) override{ if (input == PRESS_B) { player.jump();//角色跳跃的代码 player.setState(JumpState()); } else if (input == PRESS_DOWN) { player.sneak();//角色蹲下的代码 player.setState(SneakState()); } } }; //跳跃状态 class JumpState : public State { public: void handleInput(Player& player, const Input& input) override { if (input == PRESS_DOWN) { player.dive();//角色俯冲的代码 player.setState(DiveState()); } } }; //下蹲状态 class SneakState : public State { public: void handleInput(Player& player, const Input& input) override { if (input == PRESS_DOWN) { player.stand();//角色站立的代码 player.setState(StandState()); } } }; //俯冲状态 class DiveState : public State { public: void handleInput(Player& player, const Input& input) override { } };
第一次进入游戏时,给角色一个初始状态
player.setState(new StandState());
然后每次接受输入,让角色当前的状态对象去处理就可以了。
player.getState().handleInput(player,input);
简单小结:
可以看到利用状态类对象,我们把负责的条件逻辑封装到各个状态类里,让代码变得优雅,而且还减少了几个变量的使用(m_isJumping等)。
此外由于有限状态对象的属性是固定不变的,这意味着所有角色都能共享同一个状态(当同种状态时),
所以常见的状态对象存储方式是单例存储或者静态存储(每种状态只生成1个对象),避免了上文每次都要生成新状态对象的开销。
有限状态机的更多改良改进
平行的状态机
实际中,一些游戏的类可能需要多个状态(平行关系),于是可以写出以下代码
class Player{ State* m_bodyState;//身体状态 State* m_equipmentState;//装备状态 //.....其它代码 };
然后便可以用下列方式处理状态了
void Player::handleInput(const Input& input) { m_bodyState->handleInput(*this,input); m_equipmentState->handleInput(*this,input); }
层次状态机
把主角的行为更加具象化以后,可能会包含大量相似的状态,为了重用代码,便衍生层次状态机的概念。
层次状态主要思想是状态类继承,从而产生层次关系的状态。
例如,蹲下状态和站立状态 继承于 在地面状态。
class OnGroundState : public State { void handleInput(Player& player, const Input& input) override { if (input == PRESS_B){}//....跳跃 } }; class StandState : public OnGroundState { void handleInput(Player& player, const Input& input) override { //当松开↓键,才蹲下去 if (input == RELEASE_DOWN) {}//...蹲下去的代码... else {OnGroundState::handleInput(player,input);} } }; class SneakState : public OnGroundState { void handleInput(Player& player, const Input& input) override { //当松开↓键,才站起来 if (input == RELEASE_DOWN) {}//...站起来的代码... else {OnGroundState::handleInput(player, input);} } };
下推状态机
下推状态机,简单来说,就是用栈结构存储一系列状态对象。
一般来说,一个角色只需要一个状态对象,为什么要用栈结构存储一堆状态对象?
假设有一个射击游戏的角色,他现正在站立状态,执行栈顶状态中。
突然遇到敌人进行开火,于是入栈一个开火状态,并继续执行新的栈顶状态。
敌人被击中死亡,开火状态结束。为了恢复到开火前的上一个状态,于是去掉栈顶状态。
这样我们利用栈就完美模拟了一个人开火之后恢复成站立状态的过程。
简单来说,
下推自动机适用于需要记忆状态的状态机,这在一些游戏AI是常用的手法。(不过现在更流行的游戏AI是用行为树实现)
游戏设计模式系列-其他文章:
https://www.cnblogs.com/KillerAery/category/1307176.html