【Unity】Animancer状态机源码学习笔记(二)

本章将详细介绍StateMachine<TState>,官方的轻量免费版也提供了源代码,路径:Assets/Plugins/Animancer/Utilities/FSM
该类由partial修饰,在四个文件中存在
- StateMachine1.cs
- StateMachine1.InputBuffer.cs
- StateMachine1.StateSelector.cs
- StateMachine1.WithDefault.cs
StateMachine1.cs
1 | [] |
实现接口、限制泛型类
1 | [] |
序列化当前状态
1 | public TState PreviousState => StateChange<TState>.PreviousState; |
引用静态访问点存储的上一个状态和正在进入的状态
1 | public StateMachine() { } // 因为是基类,这个方法只是起到一个占位的作用 |
构造函数,创建一个新的状态机,初始化_CurrentState的值,并立即进入这个状态
因为后续会频繁的使用using(new StateChange<TState>()){},这里解释一下:只有发起改变状态请求的时候才会使用到这个结构体,其他时候StateChange<TState>()._Current都是只有起到一个存储状态的作用
1 | public virtual void InitializeAfterDeserialize() |
在序列化之后,尽快调用此方法,以正确的初始化_CurrentState。可以看到和上面的构造函数类似,因为序列化在Inspector上引用了CurrentState所以这里可以直接将其传递进来并初始化
拓展:
UnityEngine.ISerializationCallbackReceiver接口无法实现自动初始化,这个接口的回调有很多unity的方法用不了,如.OnEnterState()里的Behaviour.enabled
1 | public bool CanSetState(TState state) // 判断当前状态机是否可进入指定状态 |
提一嘴:这里很容易将状态和状态机搞混。
CanSetState是针对状态机的,而.CanExitState和.CanEnterState是针对状态的。如果对
状态机和状态的方法还有什么不清楚的,可以查看IStateMachine和IState接口
1 | public TState CanSetState(IList<TState> states) { } // 这个方法就是遍历并调用了上面的CanSetState方法,就不贴代码了 |
1 | public void ForceSetState(TState state) // 强制转换状态 |
1 | public bool TrySetState(TState state) {} |
这些方法都是在CanSetState和ForceSetState之上扩展,就不分析代码了。总的来说,一个判断,三种转换
1 | public bool CanSetState(TState state) {} // 判断当前状态是否能退出,目标状态是否能进入,并返回bool值结果 |
最后是一个
Unity.Editor在Inspector上显示的方法,暂时没看到效果,后续再补
StateMachine1.InputBuffer.cs
这个文件夹中存放的是InputBuffers,即输入缓冲器。
作用:不是简单的改变状态失败就直接放弃了,而是还会尝试一小段时间。
类比:连续跳跃的时候,快要落地但实际上还没有落地的时候按下跳跃键,也能进入跳跃状态。
InputBuffer<TStateMachine>
包含在StateMachine<TState>类中的一个泛型类(并不是子类,只是包含关系,两个甚至可以说没有任何关系)
缓存一个状态,每当Update(float)的时候尝试进入这个状态,直到TimeOut超时
1 | public class InputBuffer<TStateMachine> where TStateMachine : StateMachine<TState> |
限制传进来的类
1 | private TStateMachine _StateMachine; // 缓存目标状态机 |
1 | public TState State { get; set; } // 需要进入的目标状态 |
1 | public InputBuffer() { } // 构造函数,占位用 |
1 | public void Buffer(TState state, float timeOut) |
1 | protected virtual bool TryEnterState() => StateMachine.TryResetState(State); // 虚函数,可重构添加其他条件 |
1 | public virtual void Clear() // 清除任务 |
InputBuffer
1 | public class InputBuffer : InputBuffer<StateMachine<TState>> |
继承自InputBuffer<TStateMachine>,并确定了泛型参数为StateMachine<TState>,这个没好说的,就是指定了状态机
使用方法:
- 需要在
update()中调用输入缓冲器的_InputBuffer.Update() - 在需要改变状态的时候使用
_InputBuffer.Buffer(_Equip, _InputTimeOut)
StateMachine1.StateSelector.cs
这个文件夹中放置的是StateSelector, 即状态选择器
该类提供了一种简单的方法来管理潜在状态的优先级列表
ReverseComparer<T>
一个泛型类(并不是子类,只是包含关系,两个甚至可以说没有任何关系)
1 | public class ReverseComparer<T> : IComparer<T> |
这里需要注意的是
不需要用户实例这个类,所以将构造函数私有化了
比较的方法实用的是
Compare(),在传参的时候,将两个参数的位置换了(第一个参数y小于x时返回-1)也就是说最终的效果是返回-1,y小x大;返回-1时,x小y大
StateSelector
包含在StateMachine<TState>类中的类
1 | public class StateSelector : SortedList<float, TState> // 继承`SortedList<float, TState>`类 |
- 继承
SortedList<float, TState>类,所以拥有这个基类的所有属性,如Add
1 | public virtual void Add(object key, object value); // 基类SortedList的Add方法 |
实际上在使用的时候可以使用简单的枚举来配分动作的优先级,没必要用这种
StateMachine1.WithDefault.cs
默认状态机,其实就是添加了一个默认状态,然后针对这个默认状态写了初始化和转换成默认状态方法
就是方便用户手册介绍产品使用的,如果自己使用的话完全可以重新写一个
1 | [] |
StateChange<TState>结构体
作用:查看状态变化细节的静态访问点
要看懂这个结构体,得先搞清楚以下几点:
- 他的核心是
_Current这个线程静态属性,所有其他属性都是围绕他而展开的 - 这个结构体的用法:只有在
StateMachine使用IState方法(即正在改变状态)的时候才需要创建这个结构体,结束后就会弃用掉这个临时的结构体(但由于_Current是静态的,所以_Current是还存在的)
1 | public struct StateChange<TState> : IDisposable where TState : class, IState |
限制类型参数,继承IDisposable接口
1 | [] |
当前状态变化,设置成了线程静态,所以每个线程都有自己的副本,使得整个系统是线程安全的
线程静态成员特点:
- 多个线程访问并改变 _Current 的值时,每个线程看到的是它自己的 _Current 副本,因此一个线程对 _Current 的修改不会影响其他线程。
- 每个线程在其生命周期内对 _Current 的任何修改只对其自身有效。当线程执行完毕后,该线程的 _Current 副本就会被销毁。
- 当所有线程都执行完毕后 _Current 的最终值取决于最后一个修改它的线程的状态,如果没有任何线程正在进行状态更改 _Current 将保持其默认值(通常是 null 或者初始状态)。
1 | private StateMachine<TState> _StateMachine; // 当前发生状态变化的状态机实例 |
1 | public static bool IsActive => _Current._StateMachine != null; // 是否正在发生变化 |
这里可以看到只提供了PreviousState和NextState两个状态供外界访问,外界没有访问_Current的方法,因为没有必要。
通过在CanExitState的打印这三个可以看出,_Current就是PreviousState。如Idle->Jump:
在按下空格的一瞬间:
PreviousState是Idle,NextState是Jump;起跳后系统每帧都在判断是否能从
Jump->Idle,这段期间PreviousState一直是Jump,NextState一直是Idle;如果在空中的时候又按了一下空格键,系统会判断能否
Jump->Jump,在你按下的这一帧PreviousState和NextState都是Jump
可以看出来这里的状态是相对于帧的状态,并不是指上一个状态块


1 | internal StateChange(StateMachine<TState> stateMachine, TState previousState, TState nextState) |
internal,只允许在Animancer.FSM内访问
构造函数用于设置当前状态变化的信息。它首先复制当前的StateChange<TState>到this,然后更新_Current以反映新的状态变化。
1 | public void Dispose() |
实现IDisposable接口,在StateMachine<TState>会常使用using来创建结构体,在using结束时将自己再存储在线程静态中。
其实整个的作用就是为了保证有且只有一个静态_Current并且其状态是最新的
1 | using (new StateChange<TState>(this, null, state)) |
拓展:
IDsposable的作用:using块结束或其中的代码抛出异常 ,Dispose方法将被自动调用
IStateMachine接口
1 | public interface IStateMachine |
void ForceSetState(object state):强制改变状态
调用CurrentState的IState.OnExitState,然后将CurrentState改变成参数状态,并调用其IState.OnEnterState
IPrioritizable接口
状态选择器使用的
1 | public interface IPrioritizable : IState |
总结:
第一次研究源码,开始的时候确实会被吓着,觉得有点困难什么的。但实际看完下来发现和之前学的状态机核心工作原理是差不多的,只是在这基础上完善了很多方法,如:设定进入、离开状态的方法;使用接口规范代码。看完之后发现其实理解的还是很通透的。
这次的奇妙之旅最大的搜获可能就是理解了一个完整的项目应该是怎么样的框架结构。要尽可能的使用接口和继承,达到解耦的效果,使代码更容易维护。
学习的路还很长,这次状态机的源码并不是Animancer的核心源码,只是其中的一个小部分而已,并且有限状态机也并不是很难的一个模型。后续还需要继续研究源码,了解更多的编程技巧和模型框架。
示例补充
除了 StateMachine<TState>外,还提供了一个StateMachine<TKey, TState>
后者得花费更多时间和精力维护,但他的优势在于可以抽象和需要序列化当前状态
1 | public class Character : MonoBehaviour |
确实比较鸡肋
1 | public class Character : MonoBehaviour |