状态模式的定义
允许对象在当内部状态改变时改变其行为,就好像此对象改变了自己的类一样。
举个例子
在书的示例里要求你写一个人物控制器,实现跳跃功能
直觉上来说,我们代码会这么写:
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