游戏中的状态模式

“允许一个对象在其内部状态改变时改变自身的行为,对象看起来好像是在修改自身类。”

介绍状态模式,不能抛开游戏里面的有限状态机(finite state machines FSM),还有层次状态机(hierarchical state machine)和下推 自动机(pushdown automata)。

假设在开发主角,玩家输入来控制主角的行为,当按下B键时,应该跳跃

void Heroine::handleInput(Input input)
{
 if (input == PRESS_B)
 {
  yVelocity_ = JUMP_VELOCITY;
  setGraphics(IMAGE_JUMP);
 }
}

很明显这样不对,应该还有如果主角着地将 isJumping_设置回false的代码。

没有阻止主角“在空中跳跃”,当主角跳起来后继续按下B键,添加一个isJumping_布尔值变量来追踪主角的跳跃

void Heroine::handleInput(Input input)
{
    if(input == PRESS_B)
    {
        if(!isJumping_)
        {
            isJumping_ = true;
            // Jump...
        }
    }
}

想实现主角的闪避动作,当主角站在地面上的时候,如果玩家按下下方向键,则躲避,如果松开此键,则站立

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);
 }
}

上面存在问题:

  1. 按下方向键来闪避
  2. 按B键从闪避的状态直接跳起来
  3. 玩家还在空中的时候松开下键

需要添加另外一个布尔标志位来解决

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);
   }
 }
}

如果主角可以在跳起来的过程中,按下方向键进行一次俯冲攻击就更好了

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了,主角在跳跃状态的时候不能再跳,但是在俯冲攻击的时候却可以跳跃,又要添加一个成员变量。

书中的这话我很喜欢:你崇拜的一些程序员,他们总是看起来会编写完美无瑕的代码,然而他们并非超人,相反,他们有一种直觉会意识到哪种类型 的代码容易出错,然后避免编写出这种代码。

有限状态机

躲避------释放Down----站立---按下B-->跳跃--按下Down-->俯冲
    <-----按下Down---

状态枚举

enum State
{
 STATE_STANDING,
 STATE_JUMPING,
 STATE_DUCKING,
 STATE_DIVING
};
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;
 }
}

这样写代码的话就非常清晰。

如果在Heroine类中添加一个chargeTime_成员来记录主角蓄能的时间长短,

void Heroine::update()
{
 if (state_ == STATE_DUCKING)
 {
  chargeTime_++;
  if (chargeTime_ > MAX_CHARGE)
  {
   superBomb();
  }
 }
}

在主角躲避的时候重置这个蓄能时间

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...


 }
}

引入状态模式

class HeroineState
{
public:
 virtual ~HeroineState() {}
 virtual void handleInput(Heroine& heroine, 
               Input input) {}
 virtual void update(Heroine& heroine) {}
};

为每一个状态定义一个类

class DuckingState : public HeroineState
{
public:
 DuckingState()
 : chargeTime_(0)
 {}

 virtual void handleInput(Heroine& heroine, 
               Input input) {
  if (input == RELEASE_DOWN)
  {
   // Change to standing state...


   heroine.setGraphics(IMAGE_STAND);
  }
 }

 virtual void update(Heroine& heroine) {
  chargeTime_++;
  if (chargeTime_ > MAX_CHARGE)
  {
   heroine.superBomb();
  }
 }

private:
 int chargeTime_;
};

委托状态,

class Heroine
{
public:
 virtual void handleInput(Input input)
 {
  state_->handleInput(*this, input);
 }

 virtual void update() { state_->update(*this); }

 // Other methods...


private:
 HeroineState* state_;
};

状态对象应该放在哪里?

静态状态

如果一个状态对象没有任何数据成员,那么它的唯一数据成员便是虚表指针了,那样的话就没必要创建此状态的多个实例了。

class HeroineState
{
public:
 static StandingState standing;
 static DuckingState ducking;
 static JumpingState jumping;
 static DivingState diving;
  // Other code ...
};

修改状态的方式

if(input == PRESS_B)
{
    heroine.state_ = &HeroineState::jumping;
    heroine.setGraphics(IMAGE_JUMP);
}

实例化状态

有时候上面的静态状态方法可能不行,对于刚才的躲避状态就不行,因为它有一个chargeTime_成员变量,当然你可以搞 骚操作把属性搞到主角对象里去。

void Heroine::handleInput(Input input)
{
    // 返回想要切换到的状态
 HeroineState* state = state_>handleInput(  
      *this, input);
 if (state != NULL)
 {
    // 删除老状态
  delete state_;
    // 切换状态
  state_ = state;
 }
}

例如切换状态

HeroineState* StandingState::handleInput(  
      Heroine& heroine, Input input)
{
 if (input == PRESS_DOWN)
 {
  // Other code...


  return new DuckingState();
 }
 // Stay in this state.


 return NULL;
}

为状态添加 enter 和 leave 钩子

class StandingState : public HeroineState
{
public:
 virtual void enter(Heroine& heroine)
 {
  heroine.setGraphics(IMAGE_STAND);
 }
  virtual void leave(Heroine& heroine)
  {
    // ...
  }


 // Other code...

};

enter和leave由上层调用

void Heroine::handleInput(Input input)
{
 HeroineState* state = state_->handleInput(  
      *this, input);
 if (state != NULL)
 {
    // call the leave action
    state_->leave(*this);
  delete state_;
  state_ = state;

  // Call the enter action on the new state.
  state_->enter(*this);
 }
}

并发状态机

如果我们决定给主角添加持枪功能,当她持枪的时候,仍然可以:跑、跳和躲避等。但是,她也需要能够在这些状态过程中开火。

我们不要把两种不同的状态硬塞到一个状态机里面去,比较直观的解决方法就是,分开成两个状态机。

主角定义n种状态和m种能携带的武器状态,如果使用一个状态机表示则需要nxm个状态,如果需要两个状态机, 那么状态组合仅是n+m。

class Heroine
{
 // Other code...

private:
 HeroineState* state_;
 HeroineState* equipment_;
};

当主角派发输入事件给状态类时,需要给两种状态机都派发

void Heroine::handleInput(Input input)
{
 state_->handleInput(*this, input);
 equipment_->handleInput(*this, input);
}

层次状态机

把主角的行为更具象化之后,她可能会包含大量相似的状态,比如,她可能有站立,走路、跑步和滑动状态。 在这些状态种任何一个状态时按下B键,我们的主角要跳跃;按下方向键,主角要躲避。

把共同的逻辑提炼到基类状态中

class OnGroundState : public HeroineState
{
public:
 virtual void handleInput(Heroine& heroine,   
               Input input)
 {
  if (input == PRESS_B) // Jump...


  else if (input == PRESS_DOWN) // Duck...


  }
 }
};

子状态继承它

class DuckingState : public OnGroundState
{
public:
 virtual void handleInput(Heroine& heroine,   
               Input input)
 {
  if (input == RELEASE_DOWN)
  {
   // Stand up...


  }
  else
  {
      // 一层一层向上找
   // Didn't handle input, so walk up hierarchy.
   OnGroundState::handleInput(heroine, input);
  }
 }
};

下推状态机

它要解决的是有限状态机没有历史记录的问题,我们知道当前状态,但是,我们并不知道之前的状态是什么。

比如有一个开枪状态,一种新的状态来播放开枪的动画,发射子弹并显示一些特效。 但开枪后要回到什么状态呢。

本来,有限状态机有一个指向当前状态的指针,而下推自动机有一个状态栈。 在一个有限状态机里面,当有一个状态切进来时,则替换掉之前的状态。下推自动机可以让你这样做,同时它还提供其他选择:

|站立
|站立 开火 <--push
|站立 -->pop

状态机得使用范围是有限的,在游戏AI领域,趋势越来越倾向于行为树和规划系统。