游戏设计模式--状态机模式(翻译整理)(下篇)

状态模式

随着面向对象对大家的深入人心。每一个条件分支,都会觉得是一个使用动态调度的地方(换句话说,就是我们c++中的虚拟函数)。我想你可能会进一步的去思考这个问题。但是有的时候,if是够使用的。

但是在我们这个例子里面,面线对象的思路对我们才是最合适的。在四人帮的设计模式中,有这么一句话:

Allow an object to alter its behavior when its internal state changes. The object will appear to change its class.

状态接口

首先我们定义状态的抽象接口类。所有跟状态相关的函数,我们都把它们放到这个类里面去。我们的例子里面就是handleInput()update()函数。

 

class HeroineState

{

public:

  virtual ~HeroineState() {}

  virtual void handleInput(Heroine& heroine, Input input) {}

  virtual void update(Heroine& heroine) {}

};

具体化状态类

我们来实现每一个具体的状态类,首先类继承我们上面的接口类。然后把之前switch下的代码移动到每一个具体状态类中来。列如:

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

};

注意,这里我们把chargeTime_heroine类里面移动到了这里。这个是非常好的,本来聚气的时间变量就只是在下蹲状态中生效。所以这样修改后,更能反映状态对象的功能。

状态的调用

下一步,我们需要在heroine类中有变量保存当前的状态类。这样就可以取消掉前面的一大段swithc代码了:

class Heroine

{

public:

  virtual void handleInput(Input input)

  {

    state_->handleInput(*this, input);

  }

 

  virtual void update()

  {

    state_->update(*this);

  }

 

  // Other methods...private:

  HeroineState* state_;

};

那么怎么去改变当前的状态呢?我只需要将stat_变量指向需要变化到的实体状态类对象即可。一个基本的状态类框架就已经搭建完成了。

我们的状态类的对象在哪儿呢?

前面我埋了一个小坑。说改变当前的状态只需要stat_指向新的状态类对象。但是这些对象从哪儿来呢?之前我们是用enum来是实现的状态类型,但是enum本身其实只是一个数字。没有办法存储其他东西。但是现在,我们的状态已经是类实现的了。那么只需要有个一个地方去实例化,保存起来即可。这里有2种通用的实现方法:

A.静态状态

如果这些状态没有其他的变量,有的变量只是为了实现虚拟函数执行的载体。在这种情况下,我们就只需要创建一次,需要使用的时候指向这个对象即可。

这种情况下,我们可以用static简单实现了。至于状态对象放在什么地方,就看你自己的了。我们这里将状态对象放在heroinState里面

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

}

B.实例化状态

有得时候,用静态状态是实现不了的。像前面下蹲状态里面的chargeTime_变量,那个是给具体角色使用的,如果只有一个角色,这个是没有问题的,但是如果游戏有2个角色那么共用这个变量就出现问题了。

在这种情况下,当我们需要使用某个状态的时候,我们需要及时的创建这个状态对象。这样每一个状态都有了自己的实例。当然,有一个新的状态创建出来,那么老的就需要释放掉。这里我们需要小心。因为这个这个创建新状态的触发一般都在当前状态的代码里面,我们不能直接删除当前的状态。

我们这里在hadleInput()HeroineState中选择性的返回了一个新的状态。然后让Heroine在外面去做删除和交换状态的工作:

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  Exit 行为

状态模式的核心是封装改状态下的行为在一个独立的类里面。我们前面已经做了一部分,但是还差上一些。

像我们例子里面,当我们改变状态的时候,我们同时需要改变玩家的动画。我们现在的实现方式是在改变状态的地方,做的动画变动:

HeroineState* DuckingState::handleInput(Heroine& heroine,

                                        Input input)

{

  if (input == RELEASE_DOWN)

  {

    heroine.setGraphics(IMAGE_STAND);//动画变动

    return new StandingState();//状态变化

  }

 

  // Other code...

}

但是我们真正需要的是状态自己去控制动画的变化。我们这里只需要加上一个Enter行为即可:

class StandingState : public HeroineState

{

public:

  virtual void enter(Heroine& heroine)

  {

    heroine.setGraphics(IMAGE_STAND);

  }

 

  // Other code...

};

返回到Heroine代码中,我们修改处理状态改变的代码如下:

void Heroine::handleInput(Input input)

{

  HeroineState* state = state_->handleInput(*this, input);

  if (state != NULL)

  {

    delete state_;

    state_ = state;

 

    // 状态创建后,这里调用状态的Enter行为.

    state_->enter(*this);

  }

}

同时修改下蹲状态里切换状态的代码如下

 

HeroineState* DuckingState::handleInput(Heroine& heroine,

                                        Input input)

{

  if (input == RELEASE_DOWN)

  {

    //heroine.setGraphics(IMAGE_STAND);//删除掉这里动画变动的调用,因为在StandingEnter里面已经自动执行了

    return new StandingState();//状态变化

  }

 

  // Other code...

}

那么现在站立的状态相关的的代码都已经集成到了站立状态自己的类里面。这样的话,我们无论从任何状态切换到站立,都不用考虑动画要怎么去修改了。在我们游戏里面会有很多这种情况,如从Jump  StandingDucking  Standing 等等。

当然这里我们按照同样的方式可以实现Exit行为,它代表的就是我们状态离开的时候,状态需要处理的各种行为。

一些扩展

我们前面花了那么多的时间去推销这个状态机,但是这里我们会看到一些状态机纠结的地方。正是因为状态机本身的规则和统一性,也就造成了一些对应问题较纠结。

例如,我们使用状态机去做一些AI,你就会发现一个头痛的问题了。不过,谢天谢地,大部分问题,已经有先锋们,帮我发现和解决了。下面我们就会浏览下这些问题,来结束我们状态机的讲解.

并行状态机

在我们游戏里面,我们决定给我们角色拿武器的能力。当他拿上武器后,他一样可以做之前他能做的事情,如跑,跳,下蹲等等。同时做这些事情的时候,他同样可以开枪。

如果我们想要用当前的状态机去实现,那么我们就会让我们的状态机类的数量翻倍。如在现有的状态类型下,我们需要这些状态同时拥有武装能力,那么我们就需要状态:站立状态,站立拿枪状态,跳跃状态,跳跃拿枪状态,等等。当然这样也解决了问题。

但是如果我们游戏还能拿刀呢?那我们的状态数量就会爆炸掉。这里不只是状态的数量变多。而且会有一堆重复的代码。以为拿武器和不拿武器的2个类只在特殊的地方游戏代码不一样,大部分都是一样的代码。

这里出现问题的主要原因就是我们将我们在做什么,和我们装备着什么,2个状态合在一个状态机里面。解决的方法,也就很明显了,我们需要分2个状态机来实现。

我们保留之前的状态机不变。我们独立一个角色装备的状态机出来。这样我们就有2个状态机:

class Heroine

{

  // Other code...

private:

  HeroineState* state_;

  HeroineState* equipment_;

};

当然在我们处理输入的时候,2个状态机都需要处理:

void Heroine::handleInput(Input input)

{

  state_->handleInput(*this, input);

  equipment_->handleInput(*this, input);

}

每个状态机都自己处理自己的事情,当我们2个状态机之前没有什么关联的时候,这个处理就没有任何问题。

但在实际的游戏里面。这多个状态机之间一般情况下,都是有联系的。例如,也许我们的角色在跳跃的时候,是不允许开枪的。或者是在我们在武装的情况下,是不允许下冲攻击的。那么这种情况下,在我们的状态代码里面为了判断另一个状态机的情况,还是会有一些难受的IF处理。虽然这个也不是一个完美的处理方案,但是一般情况下还是够用的了。

继承关系的状态机

在游戏的后面,我们一般情况下都会添加一些小状态,如,站立,行走,跑步,滑行。在这些状态下,当我们按下B键的时候,角色都可以跳跃,按下Down键的时候,都可以下蹲。

当我们实现这些东西的时候,就会发现我们会重复一堆相同的代码在上面的各个状态类里面。如果可能的话,我们肯定都希望我们的代码只实现一次,然后复用起来。

如我们去用面向对象的技术的话,继承就是一个好的方案,来解决上面的问题。我们可以实现一个叫做on ground”的状态类,来处理跳跃和下蹲的情况。然后站立,行走,跑,滑行都继承于这个on ground类,然后在各自实现自己的代码即可。

上面这个的实现就是继承关系的状态机。一个状态都有自己的父类。当一个事件抛下来执行的时候,如果自己不执行就丢给父类执行。

在我们的例子里面,我们就可以HeroineState作为所有的状态的基类,而OnGround就是他的一个子类:

class OnGroundState : public HeroineState

{

public:

  virtual void handleInput(Heroine& heroine, Input input)

  {

    if (input == PRESS_B)

    {

      // Jump...

    }

    else if (input == PRESS_DOWN)

    {

      // Duck...

    }

  }};

然后我们实现前面说的其他的类,继续与OnGroundState,如:

class DuckingState : public OnGroundState

{

public:

  virtual void handleInput(Heroine& heroine, Input input)

  {

    if (input == RELEASE_DOWN)

    {

      // Stand up...

    }

    else

    {

      //这里下蹲类自己不处理,丢给父亲处理

      OnGroundState::handleInput(heroine, input);

    }

  }};

当然这个并不是实现继承关系的唯一方式。

 入栈自动机

在有限状态机里面,这里还有一个比较常用的技术,就是栈式状态机。这个是处理一些特殊情况的技术。

主要的问题就是,我们之前的实现方式里面,是没有历史状态这个概念的。我们只知道我们当前是在那一个状态,但是并不知道,之前我们在哪一个状态。那么就没有办法做回滚到之前状态的操作。

我们举一个例子:我们让我们的角色拿上了武器,然后也实现华丽的带有特效的FireState。同时让玩家开枪的时候,从任何能够开枪的状体,转变到了FireState

纠结的地方就是,当我们开枪完成之后,该怎么处理了。因为角色可以在站立,跑,跳跃,下蹲等状态切换到开火状态,那么当开火完成的时候,角色因为回到他在开火前的状态里面去。

如果用我们之前的方式,那么开完枪,角色已近忘记了他之前在做什么了。我们可以通过记录,或者将开火状态做成各种条件的开火状态,如跳跃下开火状态,跑步下开火状态,那么开火结束就知道之前是什么状态了,但是这样实现就非常的冗余了。

这个时候入栈自动机理论就帮助了我们。

实现的方式就是,做一个堆栈,堆栈的顶上的状态是当前运行的状态。当我们需要转换到新的状态的时候,我们就压栈,当我们状态结束的时候,有2种情况,一种是需要切换到指定的状态,那么这个状态也是压栈;另一种就只是想退出状态,回到之前的状态的话,那么就只需要出栈,即可:

游戏设计模式--状态机模式(翻译整理)(下篇)_第1张图片

这个正是我们开枪处理所需要的。我们只需要创建一个开火状态即可,当我们开火的时候,我们将FireingState压入栈顶。当我们开火完成后,退出堆栈,那么栈顶自动就是之前的状态了。

状态机到底有多大的用处呢?

即使我们做了这么多状态机的扩展,但是他依然是有限制的。当我们在处理一个复杂的AI的时候,还会用到像Behavior trees  planning systems等技术。需要一些其他书籍的知识才能完全胜任。

但是这并不代表前面讲的状态机和各种扩展,就没有用处了。对一些特殊的应用,状态机能够起到非常好的作用,下面几点如果满足的话,状态机一般能起很好的作用:

你有一个对象,他的行为的改变是依靠内部状态来实现的。

这些状态之间可以比较的独立。

这个对象的变化是依靠一些输入或者是事件来影响的

在游戏里面就是我们常见的AI,但是也可以用来出来,我们的游戏状态,输入处理,解析文本,网络协议,和其他的一些异步行为。

你可能感兴趣的:(游戏编程)