“允许一个对象在其内部状态改变时改变自身的行为。对象看起来好像事在修改自身类。”
有限状态机借鉴了计算机里的自动机理论中的一种数据结构思想。有限状态机可以看作是最简单的图灵机。
- 你拥有一组状态,并且可以在这组状态之间进行切换。
- 状态机同一时刻只能处于一种状态。
- 状态机会接受一组输入或者事件。
- 每一个状态有一组转换,每一个转换都关联着一个输入并指向另一个状态。
状态模式
每一个条件分支都可以用动态分发来解决
一个状态接口
1 | class HeroineState |
为每一个状态定义一个类
把switch语句里面的每一个case语句里的内容放置到他们对应的状态类里面去。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21class 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
12class 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
9class 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
12void 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
7class Heroine
{
// Other code...
private:
HeroineState* state_;
HeroineState* equipment_;
};
当主角派发输入事件给状态时,需要给两种状态都派发一下。1
2
3
4
5void Heroine:: handleInput(Input input)
{
state_->handleInput(*this, input);
equipment_->handleInput(*this, input);
}
层次状态机
用于解决包含大量相似的状态。一个状态有一个父状态。当有一个事件进来的时候,如果子状态不处理它,那么沿着继承链传给它的父状态来处理。换句话说,它有点像覆盖继承的方法。1
2
3
4
5
6
7
8
9class 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
16class 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)。
下推状态机有一个状态栈。它还提供其他选择:
- 你可以把这个新的状态放入栈里面。
- 你可以弹出栈顶的状态,该状态将被抛弃。
当你的问题满足以下几点要求的时候,有限状态机将会非常有用:
- 你有一个游戏实体,它的行为基于它的内部状态而改变。
- 这些状态被严格划分为相对数目较少的小集合。
- 游戏实体随着时间的变化会响应用户输入和一些游戏事件。