理论要点

  • 什么是状态模式:允许对象在当内部状态改变时改变其行为,就好像此对象改变了自己的类一样。通俗点总结就是:一个对象存在很多状态,这些状态可以通过外部输入转移到另一个状态。即状态,输入,转移就是此模式的全部核心了。

  • 要点:
    1,你拥有状态机所有可能状态的集合。
    2,状态机同时只能在一个状态。
    3,一连串的输入或事件被发送给状态机。
    4,每个状态都有一系列的转移,每个转移与输入和另一状态相关。

  • 使用场合:
    很多MMO游戏的核心框架其实就是通过状态机来维护的,在游戏update中每帧检测当前状态,外部输入(遥感,键盘)来改变状态执行对应操作。
    一般状态模式就是简单的有限状态机实现,同时它还有一些扩展来解决特定情形:并发状态机,分层状态机,下推自动机,这些后面代码分析都会一一列举。
    即使状态机有这些常见的扩展,它们还是很受限制。 这让今日游戏AI移向了更加激动人心的领域,比如行为树和规划系统 。 如果你关注复杂AI,这一节只是为了勾起你的食欲。

代码分析

1,假设要实现玩家在游戏世界中操作的女英雄。 当按B键时她应该跳跃。当按下方向键时,如果角色在地上,我们想要她卧倒,而松开按键时站起来。还有如果玩家在跳跃途中按下下方向键,英雄能够做跳斩攻击就太酷了。
就上面这些我们要怎么实现呢?我们先来试着画个有限状态机的示意图:

好,现在我们先用最常规的switch case方式来实现这个流程图:
首先定义所有状态的枚举,如下:

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

这是实现状态机最简单的方法(switch case方式),在某些情况下,这也不错。

但是,假设我们想增加一个动作,英雄可以俯卧一段时间充能,之后释放一次特殊攻击。 当她俯卧时,我们需要追踪充能的持续时间。
我们为Heroine添加了_chargeTime字段,记录充能的时间长度。 假设我们已经有一个每帧都会调用的update()方法。在那里,我们添加:

void Heroine::update()
{if (_state == STATE_DUCKING){_chargeTime++;if (_chargeTime > MAX_CHARGE){superBomb();}}
}

我们需要在她开始俯卧的时候重置计时器,所以我们修改handleInput():

void Heroine::handleInput(Input input)
{switch (_state){case STATE_STANDING:if (input == PRESS_DOWN){_state = STATE_DUCKING;_chargeTime = 0;setGraphics(IMAGE_DUCK);}// 处理其他输入……break;// 其他状态……}
}

总而言之,为了增加这个充能攻击,我们需要修改两个方法, 添加一个_chargeTime字段到Heroine,哪怕它只在俯卧时有意义。
然而我们更喜欢的是让所有相关的代码和数据都待在同一个地方。想想有什么更好的实现方式么?是不是想到了面向对象思想和这很吻合,把数据和行为集中到一起。

2,好,前面我们使用switch case方式,现在我们试试另外一种面向对象的方式
首先,我们为状态定义接口基类。

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){// 改回站立状态……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);}// 其他方法……
private:HeroineState* _state;
};

为了“改变状态”,我们只需要将_state声明指向不同的HeroineState对象。 这就是状态模式的全部了。

然而,这里的_state具体从哪里来呢?实例化它通常有两种方案:
第一种:静态状态

class HeroineState
{public:static StandingState standing;static DuckingState ducking;static JumpingState jumping;static DivingState diving;// 其他代码……
};

这样的话,其实可以像前面enum列举一样使用了。每个静态字段都是游戏状态类的一个实例。为了让英雄跳跃,站立状态会这样做:

if (input == PRESS_B)
{
  heroine._state = &HeroineState::jumping;heroine.setGraphics(IMAGE_JUMP);
}

然而,静态对象有一个弊端,就是它只有一个实例,对于像俯卧状态这种有个计时的_chargeTime字段的,如果这时我们游戏要添加两个英雄双人合作,那这就尴尬了。
这时我们另一种:实例化状态(为每个状态new新的,释放上次的)
既然要先释放上次的状态对象,所以我们的处理接口得返回当前的状态,如:

HeroineState* StandingState::handleInput(Heroine& heroine,Input input)
{if (input == PRESS_DOWN){// 其他代码……return new DuckingState();}// 保持这个状态return NULL;
}

然后修改英雄控制类,先delete上次状态对象,然后赋上当前新状态:

void Heroine::handleInput(Input input)
{HeroineState* state = _state->handleInput(*this, input);if (state != NULL){delete _state;_state = state;}
}

好,状态实例化两种方式已经讲完了。但是,我倾向于使用静态状态,因为它们不会在状态转换时消耗太多的内存和CPU。 但是,对于特殊情况,需要耗费一些精力来实现。

3,面向对象的状态模式已经全部实现了,但是最后还有一个细节可以再优化下,就是在某状态类中我们改变了其它状态的贴图:

HeroineState* DuckingState::handleInput(Heroine& heroine,Input input)
{if (input == RELEASE_DOWN){heroine.setGraphics(IMAGE_STAND);return new StandingState();}// 其他代码……
}

在俯卧状态里却需要设置站立状态的贴图,这样看一起和面向对象思想有点不合。oop告诉我们最好是自己的贴图管理自己的,好,我们改造下,基类中加一个统一的入口行为,派生类实现如下:

class StandingState : public HeroineState
{
public:virtual void enter(Heroine& heroine){heroine.setGraphics(IMAGE_STAND);}// 其他代码……
};

我们把入口行为在状态转换时调用:

void Heroine::handleInput(Input input)
{HeroineState* state = _state->handleInput(*this, input);if (state != NULL){delete _state;_state = state;// 调用新状态的入口行为_state->enter(*this);}
}

这样的话再改变状态我们就不需要去再关心对应状态贴图了。
同样的,在我们离开现有状态,转换到新状态之前也可以加一个出口行为。

4,好,目前关于基本的状态模式已经全部讲完了。下面还要讲的就是针对一些复杂的东西看我们如果处理:并发状态机,分层状态机,下推自动机。

并发状态机:
游戏情形:我们决定给英雄拿枪的能力。 当她拿着枪的时候,她还是能做她之前的任何事情:跑动,跳跃,跳斩,等等。 但是她在做这些的同时也要能开火。
如果我们执着于FSM,我们需要翻倍现有状态。 对于每个现有状态,我们需要另一个她持枪状态:站立,持枪站立,跳跃,持枪跳跃, 你知道我的意思了吧。
多加几种武器,状态就会指数爆炸。 不但增加了大量的状态,这也增加了大量的冗余: 持枪和不持枪的状态是完全一样的,只是多了一点负责射击的代码。

为了处理类似这样很多状态,但它们明显可以分为两组,而且这两组互不相关。 处理方法很明显:使用两个单独的状态机。如下面这样:

class Heroine
{// 其他代码……private:HeroineState* _state;        //以前状态HeroineState* _equipment;    //武器状态
};

控制状态处就得两个都控制了:

void Heroine::handleInput(Input input)
{_state->handleInput(*this, input);_equipment->handleInput(*this, input);
}

每个状态机都能响应输入,发生行为。 当两个状态集合几乎没有联系的时候,它工作得不错。
但在实践中,你会发现状态有时需要交互。 举个例子,也许她在跳跃时不能开火,或者她在持枪时不能跳斩攻击。 为了完成这个,你也许会在状态的代码中做一些粗糙的if判断, 这不是最优雅的解决方案,但这可以搞定工作。

分层状态机:
游戏情形:再充实一下英雄的行为,她可能会有更多相似的状态。 举个例子,她也许有站立,行走,奔跑,和滑铲状态。在这些状态中,按B跳,按下蹲。
这里你会发现这些相似地状态它们都有相同的输出行为,换言之就是有多个相似状态对象共用一些操作时,就可以通过继承来减少重复代码,这其实就是分层状态机。
如上面的,我们可以封装一个在地面上的状态基类,处理相同的输出行为:

class OnGroundState : public HeroineState
{
public:virtual void handleInput(Heroine& heroine, Input input){if (input == PRESS_B){// 跳跃……}else if (input == PRESS_DOWN){// 下蹲……}}
};

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

class DuckingState : public OnGroundState
{
public:virtual void handleInput(Heroine& heroine, Input input){if (input == RELEASE_DOWN){// 站起……}else{// 没有处理输入,返回上一层基类中找OnGroundState::handleInput(heroine, input);}}
};

就是一个状态有一个父状态。当有一个输入进来时,如果子状态没有处理,那么沿着继承链传给它的父状态来处理。这有点像覆盖继承的方式。

下推自动机:
游戏情形:玩家按下开火键就能进入开火状态,我们定义为FiringState,但是当开火结束后我们要回到之前的状态。这里要解决的问题是有限状态机没有任何历史的概念。 你知道当前正在什么状态中,但是没有简单的办法重回上一状态。
那么下推自动机就是用来解决回到历史状态问题的。上面状态机只有一个指向当前状态的指针。而下推自动机则有一个状态栈,当前状态永远存在栈顶,历史状态自然压入它下面。同时也可以弹出栈顶状态,将该状态抛弃。
所以上面的游戏情形就可以这么实现:当开火时,我们把开火状态push进栈顶,而开火结束时,我们把这个开火状态pop出去。此时,状态机会自动切换到我们开火前的上一个状态。示意图如下:

好,状态模式就先介绍到这了,结束~

重返设计模式--状态模式相关推荐

  1. Python设计模式-状态模式

    Python设计模式-状态模式 代码基于3.5.2,代码如下; #coding:utf-8 #状态模式class state():def writeProgram(self,work):raise N ...

  2. Java 设计模式——状态模式

    概述 很多人在说状态模式的时候总拿策略模式来进行对比,可能他们的类图会有一点类似,可我却不认为他们有多么相像.你可以阅读<Java设计模式--策略模式>这篇博客,并与本文对比,以找到蛛丝马 ...

  3. 设计模式状态模式uml_UML的完整形式是什么?

    设计模式状态模式uml UML:统一建模语言 (UML: Unified Modeling Language) UML is an abbreviation of Unified Modeling L ...

  4. C++设计模式——状态模式

    C++设计模式--状态模式 在实际开发中,我们经常会遇到这种情况:一个对象有多种状态,在每一个状态下,都会有不同的行为.那么在代码中我们经常是这样实现的.   1 2 3 4 5 6 7 8 9 10 ...

  5. Java 有限状态机 (设计模式——状态模式)

    Java 有限状态机 (设计模式--状态模式) 编写代码的时候,有时会遇见较为复杂的swith...case...和if...else...语句.这一刻有时会想到状态机,用有限状态机替换swith.. ...

  6. C++设计模式——状态模式(state pattern)

    一.原理讲解 别名状态对象(object for state). 1.1意图 允许一个对象在其内部状态改变时改变它的行为.对象看起来似乎修改了它的类. 1.2应用场景 一个对象的行为取决于它的状态,并 ...

  7. 设计模式——状态模式详解

    0. 前言 写在最前面,本人的设计模式类博文,建议先看博文前半部分的理论介绍,再看后半部分的实例分析,最后再返回来复习一遍理论介绍,这时候你就会发现我在重点处标红的用心,对于帮助你理解设计模式有奇效哦 ...

  8. 大话设计模式—状态模式

    在状态模式(State Pattern)中,类的行为是基于它的状态改变的.这种类型的设计模式属于行为型模式.我们创建表示各种状态的对象和一个行为随着状态对象改变而改变的 context 对象. 大话设 ...

  9. C++设计模式-状态模式

    目录 基本概念 代码与实例 基本概念 状态模式(State):当一个对象的内在状态改变时允许改变其行为,这个对象看起来就像是改变了其类: 状态模式的好处: 1. 将与特定状态相关的行为局部化,并且将不 ...

最新文章

  1. 学习官方示例 - System.TClass
  2. TWRP开启时误点了系统只读(不再提示)【解决方案】
  3. 成功解决Cannot find declaration to go to
  4. 第四范式获信通院尖峰开源项目及开源人物双料大奖
  5. PaperWeekly给您拜年啦!
  6. java 微信支付实现
  7. UILocalNotification详解
  8. java ee空指针_Java EE 7是最终版本。 思想,见解和进一步的指针。
  9. rpm -e --nodeps_微课 | rpm的思维导图
  10. python中为什么不支持char_python支持char吗
  11. 重磅:向996开炮!携程带头居家办公。
  12. arcgis利用Model Builder构建器进行批量处理数据
  13. 举办了一个如何对外协作的讲座,4人到场
  14. docker gpu 创建 训练环境_基于 Mesos、Docker 和 Nvidia GPU 的深度学习平台实践
  15. 云数据中心容灾备份方案
  16. 华为服务器维修期,拆看一台1U华为服务器RH1288 V2-8S
  17. 男神.png misc之图片lsb隐写
  18. app小窗口悬浮工具_侧边栏 app小窗口悬浮工具
  19. 专知 2019/4/24(图像填充方法大全)
  20. Win10系统搜不到airpods?

热门文章

  1. html组态插件_组态 web组态 插件 编辑器 使用说明书
  2. 骨传导耳机和普通耳机危害哪个小?骨传导耳机
  3. sap crm行业解决方案_培训机构行业crm系统解决方案
  4. 数据库SQL经典面试题详解
  5. JMeter(二十一):使用BeanShell解析Json格式的报文
  6. 26种英语形容词后缀讲解
  7. 作业成本法中的成本动因分析----by AMT 邓为民
  8. 如何阻止iCloud照片库使用手机数据
  9. c语言 结构体 ppt,第8章C语言的结构体和共同体.ppt
  10. Xshell远程连接服务器上的jupyter notebook