“允许一个对象在其内部状态改变时改变自身的行为,对象看起来好像是在修改自身类。”
介绍状态模式,不能抛开游戏里面的有限状态机(finite state machines FSM),还有层次状态机(hierarchical state machine)和下推 自动机(pushdown automata)。
假设在开发主角,玩家输入来控制主角的行为,当按下B键时,应该跳跃
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
yVelocity_ = JUMP_VELOCITY;
setGraphics(IMAGE_JUMP);
}
}很明显这样不对,应该还有如果主角着地将 isJumping_设置回false的代码。
没有阻止主角“在空中跳跃”,当主角跳起来后继续按下B键,添加一个isJumping_布尔值变量来追踪主角的跳跃
void Heroine::handleInput(Input input)
{
if(input == PRESS_B)
{
if(!isJumping_)
{
isJumping_ = true;
// Jump...
}
}
}想实现主角的闪避动作,当主角站在地面上的时候,如果玩家按下下方向键,则躲避,如果松开此键,则站立
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
// Jump if not jumping...
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
setGraphics(IMAGE_DUCK);
}
}
else if (input == RELEASE_DOWN)
{
setGraphics(IMAGE_STAND);
}
}上面存在问题:
需要添加另外一个布尔标志位来解决
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
if (!isJumping_ && !isDucking_)
{
// Jump...
}
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
isDucking_ = true;
setGraphics(IMAGE_DUCK);
}
}
else if (input == RELEASE_DOWN)
{
if (isDucking_)
{
isDucking_ = false;
setGraphics(IMAGE_STAND);
}
}
}如果主角可以在跳起来的过程中,按下方向键进行一次俯冲攻击就更好了
void Heroine::handleInput(Input input)
{
if (input == PRESS_B)
{
if (!isJumping_ && !isDucking_)
{
// Jump...
}
}
else if (input == PRESS_DOWN)
{
if (!isJumping_)
{
isDucking_ = true;
setGraphics(IMAGE_DUCK);
}
else
{
isJumping_ = false;
setGraphics(IMAGE_DIVE);
}
}
else if (input == RELEASE_DOWN)
{
if (isDucking_)
{
// Stand...
}
}
}上面又有bug了,主角在跳跃状态的时候不能再跳,但是在俯冲攻击的时候却可以跳跃,又要添加一个成员变量。
书中的这话我很喜欢:你崇拜的一些程序员,他们总是看起来会编写完美无瑕的代码,然而他们并非超人,相反,他们有一种直觉会意识到哪种类型 的代码容易出错,然后避免编写出这种代码。
躲避------释放Down----站立---按下B-->跳跃--按下Down-->俯冲
<-----按下Down---状态枚举
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;
}
}这样写代码的话就非常清晰。
如果在Heroine类中添加一个chargeTime_成员来记录主角蓄能的时间长短,
void Heroine::update()
{
if (state_ == STATE_DUCKING)
{
chargeTime_++;
if (chargeTime_ > MAX_CHARGE)
{
superBomb();
}
}
}在主角躲避的时候重置这个蓄能时间
void Heroine::handleInput(Input input)
{
switch (state_)
{
case STATE_STANDING:
if (input == PRESS_DOWN)
{
state_ = STATE_DUCKING;
chargeTime_ = 0;
setGraphics(IMAGE_DUCK);
}
// Handle other inputs...
break;
// Other states...
}
}class HeroineState
{
public:
virtual ~HeroineState() {}
virtual void handleInput(Heroine& heroine,
Input input) {}
virtual void update(Heroine& heroine) {}
};为每一个状态定义一个类
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_;
};委托状态,
class Heroine
{
public:
virtual void handleInput(Input input)
{
state_->handleInput(*this, input);
}
virtual void update() { state_->update(*this); }
// Other methods...
private:
HeroineState* state_;
};状态对象应该放在哪里?
如果一个状态对象没有任何数据成员,那么它的唯一数据成员便是虚表指针了,那样的话就没必要创建此状态的多个实例了。
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);
}有时候上面的静态状态方法可能不行,对于刚才的躲避状态就不行,因为它有一个chargeTime_成员变量,当然你可以搞 骚操作把属性搞到主角对象里去。
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 和 leave 钩子
class StandingState : public HeroineState
{
public:
virtual void enter(Heroine& heroine)
{
heroine.setGraphics(IMAGE_STAND);
}
virtual void leave(Heroine& heroine)
{
// ...
}
// Other code...
};enter和leave由上层调用
void Heroine::handleInput(Input input)
{
HeroineState* state = state_->handleInput(
*this, input);
if (state != NULL)
{
// call the leave action
state_->leave(*this);
delete state_;
state_ = state;
// Call the enter action on the new state.
state_->enter(*this);
}
}如果我们决定给主角添加持枪功能,当她持枪的时候,仍然可以:跑、跳和躲避等。但是,她也需要能够在这些状态过程中开火。
我们不要把两种不同的状态硬塞到一个状态机里面去,比较直观的解决方法就是,分开成两个状态机。
主角定义n种状态和m种能携带的武器状态,如果使用一个状态机表示则需要nxm个状态,如果需要两个状态机, 那么状态组合仅是n+m。
class Heroine
{
// Other code...
private:
HeroineState* state_;
HeroineState* equipment_;
};当主角派发输入事件给状态类时,需要给两种状态机都派发
void Heroine::handleInput(Input input)
{
state_->handleInput(*this, input);
equipment_->handleInput(*this, input);
}把主角的行为更具象化之后,她可能会包含大量相似的状态,比如,她可能有站立,走路、跑步和滑动状态。 在这些状态种任何一个状态时按下B键,我们的主角要跳跃;按下方向键,主角要躲避。
把共同的逻辑提炼到基类状态中
class OnGroundState : public HeroineState
{
public:
virtual void handleInput(Heroine& heroine,
Input input)
{
if (input == PRESS_B) // Jump...
else if (input == PRESS_DOWN) // Duck...
}
}
};子状态继承它
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);
}
}
};它要解决的是有限状态机没有历史记录的问题,我们知道当前状态,但是,我们并不知道之前的状态是什么。
比如有一个开枪状态,一种新的状态来播放开枪的动画,发射子弹并显示一些特效。 但开枪后要回到什么状态呢。
本来,有限状态机有一个指向当前状态的指针,而下推自动机有一个状态栈。 在一个有限状态机里面,当有一个状态切进来时,则替换掉之前的状态。下推自动机可以让你这样做,同时它还提供其他选择:
|站立
|站立 开火 <--push
|站立 -->pop状态机得使用范围是有限的,在游戏AI领域,趋势越来越倾向于行为树和规划系统。