《游戏编程模式》学习笔记(七)状态模式 State Pattern

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6

状态模式的定义

允许对象在当内部状态改变时改变其行为就好像此对象改变了自己的类一样。

举个例子

在书的示例里要求你写一个人物控制器实现跳跃功能
直觉上来说我们代码会这么写

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    yVelocity_ = JUMP_VELOCITY;
    setGraphics(IMAGE_JUMP);
  }
}
可是这么写不对因为人物本身应该只能跳一次这样写的话人物就可以无限按B实现跳跃了。我们加一个bool变量来限制跳跃的情况。
void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    if (!isJumping_)
    {
      isJumping_ = true;
      // 跳跃……
    }
  }
}

好的现在还要加一个趴下的功能松开按键还得能站起来。如果我们这么加代码

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    // 如果没在跳跃就跳起来……
  }
  else if (input == PRESS_DOWN)
  {
    if (!isJumping_)
    {
      setGraphics(IMAGE_DUCK);
    }
  }
  else if (input == RELEASE_DOWN)
  {
    setGraphics(IMAGE_STAND);
  }
}

实际上就会出bug如果玩家在趴下的状态下按了B跳起此时再松开趴下键人物就会在空中变成站立的姿势。那么为了防止这种情况的发生我们又加了一个bool变量来标识趴下的情况

void Heroine::handleInput(Input input)
{
  if (input == PRESS_B)
  {
    if (!isJumping_ && !isDucking_)
    {
      // 跳跃……
    }
  }
  else if (input == PRESS_DOWN)
  {
    if (!isJumping_)
    {
      isDucking_ = true;
      setGraphics(IMAGE_DUCK);
    }
  }
  else if (input == RELEASE_DOWN)
  {
    if (isDucking_)
    {
      isDucking_ = false;
      setGraphics(IMAGE_STAND);
    }
  }
}

这段代码已经很臃肿了如果我们还想让人物实现移动是不是又得加个标志位再进一步人物如果要实现攻击呢代码就会越来越复杂……
这个时候我们就需要FSM来救场了。
这里说的FSM和状态模式是同一个东西下同
FSM的要点
在这里插入图片描述

顺着这个思路这里列出一个最简单的FSM我们先用枚举定义状态

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

我们扔掉了烦人的标志位简化了状态的变化将其变成了字段然后将处理所有状态的代码都聚集在了一起。这就是最简单的一种FSM。
现在让我们更进一步看看对于复杂情况我们要如何构建一个状态模式控制下的人物逻辑。
对于一些复杂的状态我们有时候既要处理输入又要处理时间。因为有些状态会根据按下时间的长短进行改变。
比如现在趴下一定时间后会进行充能充能后发动的攻击威力更大。
我们以此为目标按照面向对象的逻辑我们先写一个状态基类

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

这里的handleInput()就是处理输入的接口update()就是处理状态随着时间变化的接口。
我们再以此为基础写趴下状态将其单独变为一个类并且继承这个基类

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

  virtual void handleInput(Heroine& heroine, Input input) {
    if (input == RELEASE_DOWN)
    {
      // 改回站立状态……
      heroine.setGraphics(IMAGE_STAND);
    }
  }

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

private:
  int chargeTime_;
};

这样我们在人物Heroine的类中添加当前状态的指针就可以让人物拥有趴下的状态了

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

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

private:
  HeroineState* state_;
};

要改变状态只要让指针指向别的地方就OK了。
这就是一个面向对象式的相对复杂的状态模式的实现方式。是不是还算很简单

一些细节

如果状态中不存储数据或者只有全程只有一个人物拥有这些状态你可以直接静态声明这些状态将其放在全局存储区内。但如果这些状态包含着数据就像上边的例子中的chargeTime你就需要考虑把这些状态实例化以便管理。
有时候你需要对状态加入入口行为和出口行为来控制状态的转换。例如在每个状态的入口行为方法中改变人物的贴图等等。

原文 https://gpp.tkchu.me/state.html

阿里云国内75折 回扣 微信号:monov8
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6