什么是有限状态机?
有限状态机(Finite-state machine,FSM),又称有限状态自动机,简称状态机,是表示有限个状态以及在这些状态之间的转移和动作等行为的数学模型。
FSM是一种算法思想,简单而言,有限状态机由一组状态、一个初始状态、输入和根据输入及现有状态转换为下一个状态的转换函数组成。其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。
做需求时,需要了解以下六种元素:起始、终止、现态、次态(目标状态)、动作、条件,我们就可以完成一个状态机图了:
当然,毕竟我们是写代码,不是做需求,所以还是要抽象出来:
在游戏开发中,状态机(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(); }
|
该接口包含五个方法:
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() { }
public void OnCheck() { } }
|
属性和构造函数
属性
构造函数
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(); }
[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(); }
}
|
用户定义脚本(使用案例)
温馨提示:如果代码量较大,建议分为多个脚本,这样可读性会好很多,管理起来也方便。
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;
[Serializable] public class EnemyBlackboard : Blackboard { public float idleTime; public float moveSpeed; public 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
| public class AI_IdleState : IState { private float idleTimer; private FSM fsm; private EnemyBlackboard blackboard;
public AI_IdleState(FSM fsm) { this.fsm = fsm; 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
| public class AI_MoveState : IState { private FSM fsm; private EnemyBlackboard blackboard; private Vector2 targetPos;
public AI_MoveState(FSM fsm) { this.fsm = fsm; 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
| public class AI_XXXXState : IState { private XXX XXXX; private FSM fsm; private XXXXBlackboard blackboard;
public AI_IdleState(FSM fsm) { this.fsm = fsm; 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); fsm.AddState(StateType.Idle, new AI_IdleState(fsm)); fsm.AddState(StateType.Move, new AI_MoveState(fsm)); fsm.SwitchState(StateType.Idle); } void Update() { fsm.OnUpdate(); } }
|