什么是有限状态机?

有限状态机(Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
FSM是一种算法思想,简单而言,有限状态机由一组状态、一个初始状态、输入和根据输入及现有状态转换为下一个状态的转换函数组成。其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。

做需求时,需要了解以下六种元素:起始、终止、现态、次态(目标状态)、动作、条件,我们就可以完成一个状态机图了:

state-flowchart.png

当然,毕竟我们是写代码,不是做需求,所以还是要抽象出来:

2024-05-16_17.52.18.png

在游戏开发中,状态机(State Machine)经常被用来管理角色、敌人、NPC等游戏对象的行为。状态机是一种非常有用的编程模式,它可以帮助我们清晰地定义对象的各种状态以及状态之间的转换。在Unity中,我们可以使用代码实现一个简单的状态机。

FSM框架

1.StateType 枚举

首先,我们需要定义状态机中的状态。每个状态都应该继承自一个基类,这个基类可以是一个接口或者一个抽象类。在这个基类中,我们需要定义状态的进入、退出和更新方法。

1
2
3
4
5
6
7
8
9
10
public enum StateType
{
Idle,
Find_Enemy,
Attack,
Die,
Move,
Success
//更多状态
}

这些状态类型可以用于标识不同的行为状态,如站立,寻找敌人,攻击等等,需要什么状态依照游戏需要实现的功能而定。

2.**IState** 接口

接下来,我们可以创建具体的状态类。每个状态类都需要实现基类中的方法

1
2
3
4
5
6
7
8
public interface IState
{
void OnEnter();
void OnExit();
void OnUpdate();
// void OnCheck();
// void OnFixedUpdate();
}

该接口包含五个方法:

  • OnEnter(): 进入状态时调用。

  • OnExit(): 退出状态时调用。

  • OnUpdate(): 每帧更新时调用。

  • OnCheck()条件检查时调用。

  • OnFixedUpdate(): 固定更新时调用。

3.**Blackboard**

这个类不是必需的,主要是用于存储共享数据或配置数据:

1
2
3
4
5
[Serializable]
public class Blackboard
{
// 此处存储共享数据,或者向外展示的数据,可配置的变量数据
}

此类标记为 **[Serializable]**,意味着它可以被序列化,适用于 Unity 的 Inspector 界面。

4.**FSM**类

在游戏对象的脚本中,我们可以创建一个状态机对象,并在Update方法中更新状态机:

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
public class FSM
{
public IState curState;
public Dictionary<StateType, IState> states;
public Blackboard blackboard;

public FSM(Blackboard blackboard)
{
this.states = new Dictionary<StateType, IState>();
this.blackboard = blackboard;
}

//添加状态
public void AddState(StateType stateType, IState state)
{
if (states.ContainsKey(stateType))
{
Debug.Log("[AddState] >>>>>>>>>>> map has contain key:" + stateType);
return;
}
states.Add(stateType, state);
}

//切换状态
public void SwitchState(StateType stateType)
{
if (!states.ContainsKey(stateType))
{
Debug.Log("[SwitchState] >>>>>>>>>>>> not contain key:" + stateType);
return;
}
if (curState != null)
{
curState.OnExit();
}
curState = states[stateType];
curState.OnEnter();
}

public void OnUpdate()
{
curState.OnUpdate();
}

public void OnFixUpdate()
{
//curState.OnFixUpdate
}

public void OnCheck()
{
//curState.OnCheck();
}
}

属性和构造函数

属性
  • **curState**: 当前状态。

  • **states**: 一个字典,存储状态类型和对应的状态对象。

  • **blackboard**: 共享数据的黑板对象。

构造函数

1
2
3
4
5
6
public FSM(Blackboard blackboard)
{
//初始化状态字典和黑板
this.states = new Dictionary<StateType, IState>();
this.blackboard = blackboard;
}

方法

  • **AddState(StateType stateType, IState state)**: 添加状态到字典中,如果状态类型已存在则输出日志信息。

  • **SwitchState(StateType stateType)**: 切换当前状态到指定状态,如果当前状态存在则调用其 **OnExit()** 方法,切换到新状态并调用其 **OnEnter()** 方法。

  • **OnUpdate()**: 调用当前状态的 **OnUpdate()** 方法。

未实现的方法: **OnFixedUpdate()****OnCheck()** 被注释掉了,可能在未来的扩展中会用到。

FSM框架源码

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
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System;

// 状态接口
public interface IState
{
void OnEnter(); // 进入状态时调用的方法
void OnExit(); // 退出状态时调用的方法
void OnUpdate(); // 每帧更新时调用的方法
// void OnCheck();
// void OnFixedUpdate();
}

// 共享数据类
[Serializable]
public class Blackboard
{
// 在此处存储共享数据,或者向外展示的数据,可配置的变量数据
}

// 有限状态机类
public class FSM
{
public IState curState; // 当前状态
public Dictionary<StateType, IState> states; // 存储<状态类型,状态操作>的字典
public Blackboard blackboard; // 共享数据的黑板

public FSM(Blackboard blackboard)
{
this.states = new Dictionary<StateType, IState>(); // 初始化状态字典
this.blackboard = blackboard; // 初始化共享数据
}

// 添加状态到状态字典
public void AddState(StateType stateType, IState state)
{
if (states.ContainsKey(stateType))
{
Debug.Log("[AddState] >>>>>>>>>>> map has contain key:" + stateType);
return;
}
states.Add(stateType, state);
}

// 切换状态
public void SwitchState(StateType stateType)
{
if (!states.ContainsKey(stateType))
{
Debug.Log("[SwitchState] >>>>>>>>>>>> not contain key:" + stateType);
return;
}
if (curState != null)
{
curState.OnExit(); // 退出当前状态
}
curState = states[stateType]; // 切换到新状态
curState.OnEnter(); // 进入新状态
}

// 更新当前状态
public void OnUpdate()
{
curState.OnUpdate();
}

// public void OnFixedUpdate()
// {
// //curState.OnFixedUpdate
// }

// public void OnCheck()
// {
// //curState.OnCheck();
// }
}

用户定义脚本(使用案例)

温馨提示:如果代码量较大,建议分为多个脚本,这样可读性会好很多,管理起来也方便。

1.ObjectBlackboard

游戏物体的黑板类,存放在各个状态之间共享,可在检视器中配置FSM组件的变量,这里之所以选择继承是为了只针对一类游戏物体,以便于隔离区分不同种游戏物体的黑板类。

AI托管的游戏物体可以是小怪,可以是NPC,甚至可以是小道具或者建筑,这里以敌人Enemy为例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Random = UnityEngine.Random;

// 敌人的AI
[Serializable]
public class EnemyBlackboard : Blackboard
{
public float idleTime; // 空闲时间
public float moveSpeed; // 移动速度
public Transform transform; // 敌人的 Transform
}

2.状态枚举

1
2
3
4
5
6
public enum StateType
{
Idle,
Move
// 更多状态
}

3.状态类(对IState的实现)

实现IState中的状态的处理逻辑,比如进入状态时做什么,退出状态时做什么,在状态中做什么等。

Idle状态

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
//Idle的处理逻辑
public class AI_IdleState : IState
{
private float idleTimer; // 空闲计时器
private FSM fsm; // 有限状态机
private EnemyBlackboard blackboard; // 敌人的黑板

public AI_IdleState(FSM fsm)
{
// 初始化状态
this.fsm = fsm;
// 将黑板数据转换为 EnemyBlackboard
this.blackboard = fsm.blackboard as EnemyBlackboard;
}

public void OnEnter()
{
// 进入状态时重置计时器
idleTimer = 0;
}

public void OnExit()
{
// 退出状态时不需要执行任何操作
}

public void OnUpdate()
{
// 更新空闲计时器
idleTimer += Time.deltaTime;
// 如果空闲时间超过预设值,切换到移动状态
if (idleTimer > blackboard.idleTime)
{
this.fsm.SwitchState(StateType.Move);
}
}
}

Move状态

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
//Move的处理逻辑
public class AI_MoveState : IState
{
private FSM fsm; // 有限状态机
private EnemyBlackboard blackboard; // 敌人的黑板
private Vector2 targetPos; // 移动目标位置

public AI_MoveState(FSM fsm)
{
// 初始化状态
this.fsm = fsm;
// 将黑板数据转换为 EnemyBlackboard
this.blackboard = fsm.blackboard as EnemyBlackboard;
}
public void OnEnter()
{
// 进入状态时随机选择一个移动目标位置
float randomX = Random.Range(-10, 10);
float randomY = Random.Range(-10, 10);
targetPos = new Vector2(blackboard.transform.position.x + randomX, blackboard.transform.position.y + randomY);
}

public void OnExit()
{
// 退出状态时不需要执行任何操作
}

public void OnUpdate()
{
// 如果到达目标点,切换到空闲状态
if (Vector2.Distance(blackboard.transform.position, targetPos) < 0.1f)
{
fsm.SwitchState(StateType.Idle);
}
else
{
// 向目标位置移动
blackboard.transform.position = Vector2.MoveTowards(blackboard.transform.position, targetPos, blackboard.moveSpeed * Time.deltaTime);
}
}
}

状态模版

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
//XXXX的处理逻辑
public class AI_XXXXState : IState
{
private XXX XXXX; // 该状态需要的变量

private FSM fsm; // 有限状态机
private XXXXBlackboard blackboard; // XXXX的黑板

public AI_IdleState(FSM fsm)
{
// 初始化状态
this.fsm = fsm;
// 将黑板数据转换为 XXXXBlackboard
this.blackboard = fsm.blackboard as XXXXBlackboard;
}

public void OnEnter()
{
//进入状态执行的操作逻辑
}

public void OnExit()
{
//退出状态执行的操作逻辑
}

public void OnUpdate()
{
//处状态中执行的操作逻辑
}
}

状态机组件类

在本文的所有脚本中,只有这个脚本是继承了MonoBehaviour的。因此,只需要将这个脚本挂载到想要使用状态机的游戏物体上即可。检视器中,该组件的可配置变量在黑板类中被定义。

状态机需要拥有什么状态,直接用一行代码通过AddState函数添加即可。状态的具体切换逻辑基本上都可以在状态类中进行实现,除设置初始状态外,大部分情况下状态切换都无需在组件类中添加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
using UnityEngine;

public class AI_Enemy : MonoBehaviour
{
private FSM fsm; // 有限状态机
public EnemyBlackboard blackboard; // 敌人的黑板
void Start()
{
fsm = new FSM(blackboard); // 创建有限状态机并传入黑板数据
// 添加 IdleState 和 MoveState 到状态机中
fsm.AddState(StateType.Idle, new AI_IdleState(fsm));
fsm.AddState(StateType.Move, new AI_MoveState(fsm));
// 初始状态为 Idle
fsm.SwitchState(StateType.Idle);
}
void Update()
{
fsm.OnUpdate();
//fsm.OnCheck();
}
}