游戏编程模式 - 状态模式

自然界中 状态 属于物体本身。猫咪正在睡觉、吃、喝水,这些动作都属于猫咪自己,它通过自己的大脑调整改变自己每刻的状态。

游戏世界中,每个物体也是如此。他们都有一系列状态,通过状态之间的切换,去描述本身的行为。所以,游戏中每个物体都可能都会存在大量的状态,在编程过程中,我们也需要控制物体不同状态间的切换。

看看设计需求

在某一款游戏中,存在这样一个角色: 它能够走、跑、跳跃和滑行四个状态。通常情况下我们控制这个角色的状态切换,会使用条件语句,如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
void SwitchState()
{
if (Input.GetKeyDown(KeyCode.RightArrow))
{
_player.Walk();
}
else if (Input.GetKeyDown(KeyCode.LeftShift))
{
_player.Run();
}
else if (Input.GetKeyDown(KeyCode.Space))
{
_player.Jump();
}
else if (Input.GetKeyDown(KeyCode.RightShift))
{
_player.Slide();
}
}

上面的代码某些情况下会出现一些问题:

  • 在上面的代码中,我们将角色的行为和输入控制器绑定在了一起,角色的状态改变全听从用户的输入,但有些时候角色也需要自己改变自己的状态,而不需要外部干预。比如:角色执行完 "滑倒" 动作越过障碍之后,需要自己立马切换回 'Run' 的状态。

因此一个好的 "角色" 设计应该是它的状态既可以对外被更新,也可以对内被更新,控制权由设计者决定。

  • 每次输入对应着一个状态的切换,往往一个角色执行一个状态时会需要一定时间,比如说 滑倒 动作,角色可能需要播放一个滑倒的动画。但此时若紧接着按下 Space 按键,那么角色就处于 "滑倒式跳跃" 的动画中,是不是很怪异。有问题就有解决方案,加入一个控制变量控制某个状态执行的时候不能执行其他状态,如下面的代码:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private bool _isSliding = false;
void SwitchState()
{
if (Input.GetKeyDown(KeyCode.Space))
{
if (!_isSliding)
{
_player.Jump();
}
}
else if (Input.GetKeyDown(KeyCode.RightShift))
{
_isSliding = true;
_player.Slide();
}
}

通过加入一个控制变量,问题确实得到了解决。但是,随着状态的增多,你的控制变量也能会随之增多。条件语句嵌套会更加 "彻底"。这样不好,代码不易维护扩展也不够优雅。

也许状态模式就是为这些扰人的需求情况而诞生的吧!

定义

《游戏编程模式》一书中这样定义状态模式:

允许对象在当内部状态改变时改变其行为,就好像此对象改变了自己的类一样。状态模式属于一种行为模式。

实现

  • 引入抽象类或接口,声明状态类基本该有的功能
1
2
3
4
public interface State
{
void Handle();
}
  • 实现具体状态类,继承或实现状态抽象类和接口,实现当前具体状态类需要实现的行为
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class JumpState : State
{
protected Player _player;
public JumpState(Player player)
{
_player = player;
}
public void Handle()
{
// Execute state
_player.Jump();
// Change state
TimerInvoke("JumpFinished", 2000);
// Do nothing for input
}
private void JumpFinished(object obj, ElapsedEventArgs args)
{
_player.State = new IdleState(_player);
Debug.Log("Jump Done! Switch to Run state...");
}
}

JumpState 类的 Handle 方法中,实现了 "跳跃" 状态所需做的事情(也许是一个动画,或者是播放一段音效)。动作完成之后,由 "跳跃" 态进入到 "Idle" 态。状态的切换隐匿在了这个状态类内部,改变的是 _player 所拥有的 State 变量;当切换成功后又会开始执行下一个状态所处的行为(另一个实现了 State 接口的具体状态类的 Handle 方法被执行)。

不仅仅是自身主动切换状态,我们还可以在 Handle 方法内部根据玩家的输入,来控制状态切换:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public void Handle()
{
_player.Idle();
if (Input.GetKey(KeyCode.RightArrow))
{
_player.State = new WalkState(_player);
_player.SwitchState();
}
else if (Input.GetKey(KeyCode.LeftShift) && Input.GetKey(KeyCode.RightArrow))
{
_player.State = new RunState(_player);
_player.SwitchState();
}
else if (Input.GetKeyDown(KeyCode.Space))
{
_player.State = new JumpState(_player);
_player.SwitchState();
}
}

上面是 IdleState 类(源码)的 Handle 方法。可以看到,我们将玩家的输入也添加了进来,从而玩家可以主动控制状态切换,也就是状态 "对外" 可被更新。

玩家的在具体状态类中对状态的更新,主要是通过 "角色" 类,毕竟这才是玩家看的到的。紧接着,就来看看角色类吧。

  • 状态的拥有者(角色)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class Player : MonoBehaviour
{
private State _state;
public void Jump()
{
Debug.Log("Jump...!");
}
public void SwitchState()
{
_state.Handle();
}
private void Update()
{
SwitchState();
}
// ...
}

可以看出,角色直接面向玩家。角色拥有状态,并且掌控者玩家的输入 "大权"。状态被改变,同时角色行为也就发生改变,角色持有的是 State 接口类变量,它不需要知道自己究竟需要切换到 "哪个状态",但它知道自己需要切换 "状态",同时它自己也定义了一些列状态的实现供状态类帮它执行。在不同状态类内部主动切换至其它状态(或是响应输入而切换),切换后的状态也可以切换去任意其它所定义的状态类。状态属于物体,就像我们的实现中,状态属于 Player 类,所有的状态切换都服务于它。

  • 带有控制变量的状态类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class JumpState : State
{
private static bool _sIsJumping = false;
public void Handle()
{
if (_sIsJumping)
{
return;
}
_sIsJumping = true;
// Execute state...
// Change state...
}
private void JumpFinished(object obj, ElapsedEventArgs args)
{
_sIsJumping = false;
// Do change state
}
}

在原来 JumpState 中我们增加了 _sIsJumping 用来保证 "跳跃" 状态不被重复执行(处于 "跳跃" 状态不能再跳跃)。

多状态之间切换控制问题,最初我们为了控制某个状态 "不变形",而加入了很多控制变量。后面随着状态增多而增多了控制变量,导致代码难以维护。现在引入了状态模式后,我们可以为每个具体状态类引入对应的控制变量,这样无论是对于以后的需求扩展还是代码得维护都会变得更加容易。而每个单独的状态类都能够限制此个状态下能否响应哪些输入、能切换哪些状态,这正是我们需要的!

同时,新增状态类也就意味着你的项目中的具体状态类可能无限增长(属于行为模式的设计模式好像都无法绕过这一点,因为设计初衷就是为此而生);在我们的实现中,所有的状态切换都是先 new 出来新状态类再切换,这有可能导致对象数量增多(对于不具有垃圾回收的语言,切记做好内存释放工作),不过也可以对相同的状态实现状态类实例共享来减少 GC。

看完上面关于状态模式的定义、实现介绍等,感觉似曾相识。多个状态间切换,物体始终处于某一态,等待着某些指令进入下一态,这不就是状态机吗? 是的,这就是一个简单的有限状态机。状态模式将状态切换内部封装,状态自己拥有自己的控制变量从而和外部解耦;由于所有的具体状态类实现了通用状态接口且对象持有接口类,对象不需要关注下一步我是哪个状态,只需要执行状态就行;对于新增状态只需要实现接口并做好对象状态切换工作即可。

示例源码

参考