一、状态模式

状态模式用于解决系统中复杂对象的状态转换以及不同状态下行为的封装问题。当系统中某个对象存在多个状态,这些状态之间可以进行转换,而且对象在不同状态下行为不相同时可以使用状态模式。状态模式将一个对象的状态从该对象中分离出来,封装到专门的状态类中,使得对象状态可以灵活变化,对于客户端而言,无须关心对象状态的转换以及对象所处的当前状态,无论对于何种状态的对象,客户端都可以一致处理。

  • 状态模式的类图

image-20210414200951498

二、引例

在游戏开发中,经常会遇到一下场景,按下方向键时人物左右移动,按下空格键人物跳跃,一般可以用简单的if-else语句实现

1
2
3
4
5
6
7
8
9
      velocity.x = Input.GetAxis("Horizontal") * speed;
if (Input.GetKey(right))
{
transform.rotation = Quaternion.Euler(new Vector3(0, 0, 0));
}
else if (Input.GetKey(left))
{
transform.rotation = Quaternion.Euler(new Vector3(0, 180, 0));
}

但这样有可能会出现先按下左键再按下右键人继续向左走面向却朝右的情况,所以加上一层判断

1
2
3
4
5
6
7
8
9
10
11
      velocity.x = Input.GetAxis("Horizontal") * speed;
if (Input.GetKey(right))
{
if (!Input.GetKey(left))
transform.rotation = Quaternion.Euler(new Vector3(0, 0, 0));
}
else if (Input.GetKey(left))
{
if (!Input.GetKey(right))
transform.rotation = Quaternion.Euler(new Vector3(0, 180, 0));
}

此时我们想做跳跃的动作,并且必须从地面起跳,同时限定能否多段跳

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if (desiredJump)
{
desiredJump = false;
Jump();
}

private void Jump()
{
if (onGround || jumpPhase < maxAirJumps)
{
jumpPhase++;
velocity.y = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);
}
}

最基本的移动跳跃指令就完成了,但我们还要考虑人物的动画效果,如果人物在空中漫步显然看起来很奇怪,我们必须加入额外的条件判定

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
27
28
29
30
      if (desiredJump)
{
desiredJump = false;
if (!curAnima("PlayerJump"))
ator.Play("PlayerJump");
Jump();
}
if (Input.GetKey(right))
{
if (!curAnima("PlayerJump"))
{
ator.Play("PlayerWalk");
}
if (!Input.GetKey(left))
transform.rotation = Quaternion.Euler(new Vector3(0, 0, 0));
}
else if (Input.GetKey(left))
{
if (!curAnima("PlayerJump"))
{
ator.Play("PlayerWalk");
}
if (!Input.GetKey(right))
transform.rotation = Quaternion.Euler(new Vector3(0, 180, 0));
}
else
{
if (!curAnima("PlayerJump"))
ator.Play("PlayerStay");
}

那如果继续加入跑步的指令和动画呢,想要在以上的代码修改必须再次添加标志位。显然利用if-else来写功能问题很多,每次添加新状态都会使原有的一些功能出现漏洞。

三、使用有限状态机

有限状态机FSM的要点是:

  • 拥有一组状态,并且可以在这组状态之间进行切换。在我们的例子中,是行走,跳跃,跑步。
  • 状态机同时只能在一个状态。人物不可能同时处于跳跃和站立。事实上,防止这点是使用FSM的理由之一。
  • 一连串的输入或事件被发送给机器。在我们的例子中,就是按键按下和松开。
  • 每个状态都有一系列的转换,转换与输入和另一状态相关。当输入进来,如果它与当前状态的某个转换匹配,机器转为转换所指的状态。

举个例子,在走路状态随时都可以按下shift切换到跑步状态

目前而言,游戏编程中状态机的实现方式,有两种可以选择:

  • 用枚举配合switch case语句。
  • 用多态与虚函数(也就是状态模式)。

用枚举和switch case语句实现状态机

首先定义枚举

1
2
3
4
5
6
7
enum State
{
State_Stay,
State_Walk,
State_Jump,
State_Run
}
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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
private void HandleInput()
{
velocity.x = Input.GetAxis("Horizontal") * speed;
if (Input.GetKey(right))
{
if (!Input.GetKey(left))
transform.rotation = Quaternion.Euler(new Vector3(0, 0, 0));
}
else if (Input.GetKey(left))
{
if (!Input.GetKey(right))
transform.rotation = Quaternion.Euler(new Vector3(0, 180, 0));
}
switch (state)
{
case State.State_Stay:
if (Input.GetKey(right) || Input.GetKey(left))
{
state = State.State_Walk;
ator.Play("PlayerWalk");
}
if (desiredJump)
{
desiredJump = false;
Debug.Log(state + "JUMP");
state = State.State_Jump;
Jump();
ator.Play("PlayerJump");
}
break;

case State.State_Walk:
if (!Input.GetKey(right) && !Input.GetKey(left))
{
state = State.State_Stay;
ator.Play("PlayerStay");
}
else if (desiredJump)
{
desiredJump = false;
state = State.State_Jump;
Jump();
ator.Play("PlayerJump");
}
break;

case State.State_Jump:
if (desiredJump)
{
desiredJump = false;
Jump();
}
if (onGround)
{
state = State.State_Stay;
}
break;
}
}

以上代码实现了走路,站立和跳跃的切换,但想要加入跑步,我们必须改变State.State_Walk的代码加入判断是否切换为跑步,并在switch中添加case State.State_Run部分。但我们真正想要实现的是将这些与状态相关的代码封装起来。

使用状态设计模式来实现状态机

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
using UnityEngine;

//状态类
public abstract class MoveState
{
protected Rigidbody2D _body;//角色的刚体
protected Animator _ator;//角色的动画器
protected GameObject _player;//角色的GameObject
protected SetMoveState _state = null;//状态的上下文
protected float speed = 3;
protected Vector2 velocity;

public MoveState(SetMoveState state, GameObject player)
{
_state = state;
_player = player;
_body = player.GetComponent<Rigidbody2D>();
_ator = player.GetComponent<Animator>();
velocity = _body.velocity;
PlayAnima();
PlayEffect();
}

public abstract void PlayEffect();

public abstract void PlayAnima();

public abstract void HandleInput();

public void Move()
{
velocity = _body.velocity;
velocity.x = Input.GetAxis("Horizontal") * speed;
if (Input.GetKey(KeyCode.D))
{
if (!Input.GetKey(KeyCode.A))
_player.transform.rotation = Quaternion.Euler(new Vector3(0, 0, 0));
}
else if (Input.GetKey(KeyCode.A))
{
if (!Input.GetKey(KeyCode.D))
_player.transform.rotation = Quaternion.Euler(new Vector3(0, 180, 0));
}
_body.velocity = velocity;
}

public void Update()
{
Move();
}
}

public class SetMoveState
{
protected MoveState moveState;//持有状态类的引用

public SetMoveState(GameObject player)
{
moveState = new CharacterStay(this, player);
}

//切换状态
public void SetNewState(MoveState newState)
{
moveState = newState;
}

public void Update()
{
moveState.HandleInput();
moveState.Update();
}
}

//移动状态
public class CharacterMove : MoveState
{
public CharacterMove(SetMoveState state, GameObject player) : base(state, player)
{
}

public override void HandleInput()
{
if (Input.GetButtonDown("Jump"))
{
_state.SetNewState(new CharacterJump(_state, _player));
}
if (!Input.GetKey(KeyCode.A) && !Input.GetKey(KeyCode.D))
{
_state.SetNewState(new CharacterStay(_state, _player));
}
}

public override void PlayAnima()
{
_ator.Play("PlayerWalk");
}

public override void PlayEffect()
{
Game.Instance.sound.StopPlayEffect();
Game.Instance.sound.PlayEffect("Walk");
}
}

//跳跃状态
public class CharacterJump : MoveState
{
private float jumpPhase = 0;
private float maxAirJumps = 3;
private float jumpHeight = 2;

public CharacterJump(SetMoveState state, GameObject player) : base(state, player)
{
Jump();
}

public override void HandleInput()
{
if (jumpPhase < maxAirJumps)
{
if (Input.GetButtonDown("Jump"))
{
Jump();
}
}
}

private void Jump()
{
PlayEffect();
jumpPhase++;
velocity.y = Mathf.Sqrt(-2f * Physics.gravity.y * jumpHeight);
_body.velocity = velocity;
}

public override void PlayAnima()
{
_ator.Play("PlayerJump");
}

public override void PlayEffect()
{
Game.Instance.sound.StopPlayEffect();
Game.Instance.sound.PlayEffect("Jump");
}
}

//待机状态
public class CharacterStay : MoveState
{
public CharacterStay(SetMoveState state, GameObject player) : base(state, player)
{
}

public override void HandleInput()
{
if (Input.GetKey(KeyCode.A) || Input.GetKey(KeyCode.D))
{
_state.SetNewState(new CharacterMove(_state, _player));
}
if (Input.GetButtonDown("Jump"))
{
_state.SetNewState(new CharacterJump(_state, _player));
}
}

public override void PlayAnima()
{
_ator.Play("PlayerStay");
}

public override void PlayEffect()
{
Game.Instance.sound.StopPlayEffect();
}
}

using UnityEngine;

public class TestPlayerMover : MonoBehaviour
{
private SetMoveState moveState;

private void Start()
{
moveState = new SetMoveState(gameObject);
}

private void Update()
{
moveState.Update();
}

private void OnCollisionEnter2D(Collision2D collision)
{
if (collision.collider.tag == Tag.Ground)
{
//落地切换待机状态
moveState.SetNewState(new CharacterStay(moveState, gameObject));
}
}
}