游戏编程模式笔记——第七章 状态模式

“允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像事在修改自身类。”
有限状态机借鉴了计算机里的自动机理论中的一种数据结构思想。有限状态机可以看作是最简单的图灵机。

  • 你拥有一组状态,并且可以在这组状态之间进行切换。
  • 状态机同一时刻只能处于一种状态。
  • 状态机会接受一组输入或者事件。
  • 每一个状态有一组转换,每一个转换都关联着一个输入并指向另一个状态。

状态模式

每一个条件分支都可以用动态分发来解决

一个状态接口

1
2
3
4
5
6
7
class HeroineState
{
public:
virtual ~HeroineState() {}
virtual void handleInput(Heroine& heroine, Input input) {}
virtual void update(Heroine& heroine){}
};

为每一个状态定义一个类

把switch语句里面的每一个case语句里的内容放置到他们对应的状态类里面去。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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_;
};

状态委托

我们把之前的switch语句去掉,并让它去调用状态接口的虚函数,最终这些虚函数就会动态地调用具体子状态的相应函数。

1
2
3
4
5
6
7
8
9
10
11
12
class Heroine
{
public:
virtual void handleInput(Input input)
{

state_->handleInput(*this, input);
}
virtual void update() { state_->update(*this); }
// Other methods...
private:
HeroineState* state_;
};

为了修改状态,我们需要把state_指针指向另一个不同的HeroineState状态对象。

状态对象应该放在哪里呢

  • 静态状态
    如果一个对象没有任何数据成员,那么它的唯一数据成员便是虚标指针了,在那种情况下,我们可以定义一个静态实例。
  • 实例化状态
    一个拥有成员变量的状态是不能使用静态状态的。我们不得不在状态切换的时候动态地创建一个躲避状态实例。如果我们又动态分配了一个新的状态实例,则要负责清理老的状态实例。这必须相当小心,因为修改状态的函数是在当前状态里面,所以我们需要小心地处理删除的顺序。

进入状态和退出状态的行为

进入状态时需要改变角色的贴图或者动画,可以通过给每一个状态添加一个entry行为。

1
2
3
4
5
6
7
8
9
class StandingState : public HeroineState
{
public:
virtual void enter(Heroine& heroine)
{

heroine.StateGraphics(IMAGE_STAND);
}
// Other code...
};

回到Heroine类,我们修改代码来处理状态切换的情况:

1
2
3
4
5
6
7
8
9
10
11
12
void Heroine::handleInput(Input input)
{
HeroineState* state = state_->handleInput(*this, input);
if(state != NULL)
{
delete state_;
state_ = state;

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

当然我们也可以扩展这个功能来支持退出状态的行为,我们可以定义一个exit函数来定义一些在状态改变前的处理。

并发状态机

通过分成两个状态机,解决可能出现的组合建模问题,比如主角携带武器的同时可以切换动作。
首先我们可以保留原有的状态机的代码和功能不管它。接下来,我们定义一个单独的状态机,用来处理主角携带的武器。现在,我们的主角会有两个状态索引。

1
2
3
4
5
6
7
class Heroine
{
// Other code...
private:
HeroineState* state_;
HeroineState* equipment_;
};

当主角派发输入事件给状态时,需要给两种状态都派发一下。

1
2
3
4
5
void Heroine:: handleInput(Input input)
{
state_->handleInput(*this, input);
equipment_->handleInput(*this, input);
}

层次状态机

用于解决包含大量相似的状态。一个状态有一个父状态。当有一个事件进来的时候,如果子状态不处理它,那么沿着继承链传给它的父状态来处理。换句话说,它有点像覆盖继承的方法。

1
2
3
4
5
6
7
8
9
class OnGroundState : public HeroineState
{
public:
virtual void handleInput(Heroine& heroine, Input input)
{

if(input == PRESS_B) //Jump...
else if(input == PRESS_DOWN) //Duck...
}
};

然后每一个子状态都继承至它:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);
}
}
};

下推自动机

还有一种有限状态机的扩展,它们也使用状态栈,这里的栈代表了完全不同的东西,用于解决有限状态机没有历史记录的问题。
比如主角开火,开火完之后应该回到怎么样的状态,我们需要的仅仅是一种能够让我们保存开火前状态的方法,这样在开火状态完成之后可以回去。这里自动机理论再次帮上了我们的忙。相关的数据结构叫做下推自动机(pushdown automata)。
下推状态机有一个状态栈。它还提供其他选择:

  • 你可以把这个新的状态放入栈里面。
  • 你可以弹出栈顶的状态,该状态将被抛弃。

当你的问题满足以下几点要求的时候,有限状态机将会非常有用:

  • 你有一个游戏实体,它的行为基于它的内部状态而改变。
  • 这些状态被严格划分为相对数目较少的小集合。
  • 游戏实体随着时间的变化会响应用户输入和一些游戏事件。