学习 Unity Joker的有限状态机 理理 2024-12-23 2024-12-23 这几天我在试图学习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逻辑、也可以用来丰富我们的角色控制器,用处还是很大的。