当前位置: 首页 >  关注  >  正文

游戏编程模式(五):状态模式
2023-07-30 22:20:03 哔哩哔哩
状态模式在AI、编译器和游戏开发领域应用广泛。游戏开发引擎unity中也有内置的动画控制器,其原理就是状态模式。接下来的讨论将会一步步将状态模式的演化过程展示出来。

一、混乱的控制代码

首先考虑一个简单的“按B键触发玩家跳跃的场景”:

好,它拥有了基本的跳跃功能,但是会有一些问题:当你疯狂的按跳跃键时,主角就会一直浮空。最容易想到的处理办法就是添加一个状态字段,用来判断主角是否处于跳跃状态:


(资料图)

现在如果想添加另外一个动作:当玩家按下方向键时,如果角色在地上,我们想要她卧倒,而松开按键时站起来:

好,现在又出现了另外一个问题:玩家可以先按下键卧倒,再按跳跃键跳起,在空中放开下键,这将导致主角在跳一半时贴图变成了站立时的贴图。如果我们要修复,很容易想到:再加一个状态字段用来标识:

这看起来目前还算过得去,但是一个游戏主角能做的动作可能多达十多个几十个,每加入一个动作都会使代码数量增加一个级别,使代码可读性和可维护性、可拓展性减少一个量级,最重要的是,它会带来一堆BUG。

二、有限状态机

有限状态机(FSMs)可以很好地解决这一类问题,它有几个关键点:

状态机拥有所有可能状态的集合

状态机同时只能处于一个状态

一连串的输入和事件被发送给状态机

每个状态都有一系列的转移,每个转移与输入和另一状态相关

将上面的玩家控制的例子用状态机来表示:

将上述的有限状态机用代码来表示,首先想到的会是用enum和switch来实现:

四、状态模式

用枚举和分支来实现有限状态机明显会导致可读性和可维护性差,对于对OOP非常熟悉的人来说,每一个case分支都可能是一个使用面向对象的机会,由此产生了状态模式。在GoF中这样描述状态模式;

允许一个对象在其内部状态发生变化时改变自己的行为,该对象看起来好像修改了它的类型

把之前的枚举都转化成一个状态类,把switch的每个分支转换成一个共用方法(这里给卧倒状态新添加了一个chargeTime来实现蓄力释放技能的功能):

再将HeroineState作为Heroine类的一个字段,并将输入参数委托给HeroineState的handleInput和update方法来处理:

好,现在我们要改变主角Heroine的状态,只需要将HeroineState字段指向不同的HeroineState对象。让主对象通过改变委托的对象,来改变它的行为,这就是状态模式的基本思想了。

五、关于状态类的实例化

我们现在需要考虑如何来实例化状态类,用于状态之间的转换。

静态状态:如果状态对象没有其他数据字段,那么每个状态类就只需要一个实例,因为每个实例都将完全一样(享元模式)。当然,当状态对象中有其他状态相关的字段时,就不得不为这个状态类实例化多个对象。

六、入口行为和出口行为

状态模式的目标是将状态的行为和数据封装到单一类中。我们现在已经将状态的行为封装好,但是在状态的数据上还有一些问题。比如:

修改贴图的代码和状态耦合在了一起,在DuckingState类中出现了IMAGE_STAND的贴图代码,此时可以给状态一个入口行为来解决这个问题(出口行为同理):

七、并发状态机

假设现在要给主角一个拿枪的能力,并且在拿枪的时候它也同时能做完全一样的行为:跑动、跳跃、跳斩、卧倒、站立等,现在如何来设计状态类?或许需要翻倍的状态类:站立,持枪站立,跳跃,持枪跳跃……,此时不仅增加了大量的状态,而且增加了大量的冗余,持枪和不持枪的状态是完全一样的,只是多了一点负责射击的代码。

问题在于我们将两种状态绑定在了一个状态机上,所以处理方法就很明显了:使用两个单独的状态机:

如果在做什么有n个状态,而携带了什么有m个状态,要塞到一个状态机中,则需要n × m个状态。使用两个状态机,就只有n + m个

实现很简单,只是在原有基础上添加了几乎完全一样的少量代码:

八、分层状态机

如果对OOP敏感,很容易发现可以继续进行优化,站立、行走、奔跑、滑铲这些状态或许有很多重复的代码来处理与地面碰撞和跳跃的行为,此时可以定义一个地面类来处理这些行为,然后将它们继承自这个类(当然继承也会在这些类之间建立一些耦合,此时就需要进行平衡取舍):

九、下推自动机

考虑一个场景:给主角添加了一个射击状态,不管现在是什么状态,都能在按下开火按钮时跳转为射击状态,但关键问题是,如何在射击后转换为之前的状态。

解决方法是下推自动机,有限状态机有一个指向状态的指针,下推自动机有一个栈指针,它也可以实现状态之间的转换,并且:

可以将新状态压入栈中

可以弹出最上面的状态

热门推荐