Joker的有限状态机

这几天我在试图学习Unity里的动作系统,了解到“有限状态机”这个东西,之前一直有听说过,以前总感觉很高大上,花了一节水课的时间大概弄懂了,于是分享一下。

温馨提示:在开始阅读本文之前,请确保你了解C#语言中接口、继承、抽象类等相关知识点,否则可能会看不懂)。

什么是有限状态机?

对于玩家和怪物,总会存在几个状态:比如待机、跑步、攻击、巡逻...这些都可以称之为状态。我们在Unity开发中最常接触的有限状态机就是我们的Animator窗口——它就是一个有限状态机。 使用有限状态机,可以更方便我们去进行人物行为动作管理和逻辑管理,比如玩家处于待机状态,按下W键切换到跑步状态、跳跃时进入跳跃状态,我们把不同状态用状态机进行管理,就会好很多。

通用的有限状态机框架

在我观看数位、海内海外的博主的视频,发现他们都会采用同一套框架,逻辑、代码都是极其相似的,于是进行总结。
首先我们新建一个名为IState.cs:
1
2
3
4
5
6
7
8
using UnityEngine;
public interface IState
{
public void Enter();
public void Exit();
public void Update();
}

作用是实现一个接口,相当于一套代码规范,我们的所有状态:待机、跑步、跳跃等都要实现接口里的方法。


第二步,我们开始写状态机主体框架,新建FSM.cs文件:

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
using System.Collections.Generic;
using Unity.IO.LowLevel.Unsafe;
using UnityEngine;

//当距离太大对象直接待机,到一定范围对峙,对峙完一会儿直接跑过来,到一定距离再攻击
public enum StateType
{
idle,
stand,
run,
attack
}
public class FSM
{
private IState currentIState;
public Dictionary<StateType, IState> states;
public FSM()
{
this.states = new Dictionary<StateType, IState>();
}

//添加状态
public void AddState(StateType state,IState istate)
{
if (states.ContainsKey(state))
{
Debug.Log("请勿重复添加状态");
return;
}
states.Add(state, istate);
}
//转换状态
public void TransformState(StateType state)
{
if (!states.ContainsKey(state))
{
Debug.Log("没有找到此状态");
return;
}
if (currentIState != null)
{
currentIState.Exit();
}
currentIState = states[state];
currentIState.Enter();
}

void OnStart()
{
//注册状态
}

void OnUpdate()
{
//状态每帧执行的函数
currentIState.Update();
}
}


第三步,我们拿IdleState.cs举例子

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

public class IdleState : StateBase
{
// 假设我们IDLE状态需要用到某些组件比如Animator中的动画
private Animator animator; // 声明一个Animator类型的私有变量animator
private FSMControl fsm; // 声明一个FSMControl类型的私有变量fsm
private float deltaTime = 5f; // 声明一个私有浮点变量deltaTime并初始化为5秒

public IdleState(Animator animator, FSMControl fsm)
{
this.animator = animator; // 初始化animator变量
this.fsm = fsm; // 初始化fsm变量
}

public override void Enter()
{
Debug.Log("开工!"); // 当状态进入时打印日志
}

public override void Update()
{
Debug.Log("我在工作!"); // 每帧更新时打印日志

if (deltaTime >= 0) // 如果deltaTime大于或等于0
{
deltaTime -= Time.deltaTime; // 减少deltaTime的值
if (deltaTime <= 0) // 如果deltaTime小于或等于0
{
// 转换状态
fsm.TransformState(StateType.attack);
}
}
}

public override void Exit()
{
Debug.Log("逻辑执行完毕!");
}
}

最后调用:

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

public class TestFSM : MonoBehaviour
{
private FSMControl fsm; // 声明一个FSMControl类型的私有变量fsm
private Animator animator; // 声明一个Animator类型的私有变量animator

private void Awake()
{
fsm = new FSMControl(); // 在Awake方法中实例化FSMControl
animator = GetComponentInChildren<Animator>(); // 获取子对象中的Animator组件

// 添加状态
fsm.AddState(StateType.IDLE, new IdleState(animator, this.fsm)); // 添加IDLE状态,this.fsm可以减少内存占用
fsm.AddState(StateType.MOVE, new MoveState()); // 添加MOVE状态
fsm.SetState(StateType.IDLE); // 设置初始状态为IDLE
}

private void Update()
{
fsm.OnTick(); // 在Update方法中调用fsm的OnTick方法,更新状态机
}
}

让我们重新理清逻辑:
IState.cs:所有状态都有“进入状态”“状态执行中”“退出状态”三个方法,于是我们用代码写个接口,所有状态均继承这个接口。
FSM.cs:状态机的核心部分,首先用枚举定义我们有几种状态,接着我们用IState的一个实例currentState,根据“父装子”原则,用于代表我们当前的状态。
同时,我们定义一个字典states,用于存储我们的状态,接下来构造函数。然后我们写添加状态和转换状态的方法,在Start函数里注册完我们的所有方法,Update函数里去进行状态调用。


那么状态机用来干什么?主要是提升我们的开发效率、简化代码吧,它可以用来制作敌人AI逻辑、也可以用来丰富我们的角色控制器,用处还是很大的。