状态机是对状态模式的一种使用。用于独立处理在各个状态的具体情况和转换关系。
# 开始
当我们需要处理带流程响应的问题时,可以将其抽象为状态问题。
例如下图的我们需要处理 OnEnter、OnUpdate、OnExit 三个部分。
# 有限状态机
有且仅有可能处于一种状态,解决状态区分和转换问题。
有限状态机是下列几种状态机的基础,下面的状态机都基于有限状态机进行相关功能的扩充。
# 转换方式
在状态机中,我们需要使用一种转换方式来进行状态切换,在 Unity 的 Animator 中使用条件参数进行跳转。我们可以使用类似事件的方式来跳转、或者使用任意能够保证正确进行 原状态到目标状态 转换的方式。
# 实现方式
对于有限状态机来说,我们只需要保证具有
状态
、转换关系
即可。
# 状态
状态是我们需要管理的核心,状态中可以包含转换关系 Dict。
/// <summary> | |
/// 简单 Fsm 状态 | |
/// </summary> | |
public class FsmState | |
{ | |
public string Name { get; private set; } | |
public Dictionary<string, FsmTranslation> translationDict; | |
public FsmState(string name) | |
{ | |
this.Name = name; | |
translationDict = new Dictionary<string, FsmTranslation>(); | |
} | |
public override string ToString() | |
{ | |
return Name; | |
} | |
} |
# 转换条件
转换条件包括了起止状态和状态切换时要执行的回调函数。
/// <summary> | |
/// 简单 Fsm 转换条件 | |
/// </summary> | |
public class FsmTranslation | |
{ | |
public FsmState fromState; | |
public string name; | |
public FsmState toState; | |
public FsmTranslationCallBack callBack; | |
/// <summary> | |
/// 创建条件 | |
/// </summary> | |
/// <param name="fromState"> 开始状态 & lt;/param> | |
/// <param name="name"> 响应事件 & lt;/param> | |
/// <param name="toState"> 目标状态 & lt;/param> | |
/// <param name="callBack"> 转换回调 & lt;/param> | |
public FsmTranslation(FsmState fromState, string name, FsmState toState, FsmTranslationCallBack callBack) | |
{ | |
this.fromState = fromState; | |
this.toState = toState; | |
this.name = name; | |
this.callBack = callBack; | |
} | |
} |
# 控制机器
我们需要一个控制机器来管理我们的状态和条件,让整个系统能够自动化的运转起来,这个部分我将其称之为状态机器。
状态机器存储了当前的状态情况,以及根据条件转变条件的方法 HandleEvent
。
// 当前状态 | |
public FsmState CurState { get; private set; } | |
Dictionary<string, FsmState> StateDict = new Dictionary<string, FsmState>(); | |
/// <summary> | |
/// 添加状态 | |
/// </summary> | |
/// <param name="state">State.</param> | |
public void AddState(FsmState state) | |
{ | |
StateDict[state.Name] = state; | |
} | |
/// <summary> | |
/// 添加条转 | |
/// </summary> | |
/// <param name="translation">Translation.</param> | |
public void AddTranslation(FsmTranslation translation) | |
{ | |
StateDict[translation.fromState.Name].translationDict[translation.name] = translation; | |
} | |
/// <summary> | |
/// 启动状态机 | |
/// </summary> | |
/// <param name="state">State.</param> | |
public void Start(FsmState state) | |
{ | |
CurState = state; | |
} | |
/// <summary> | |
/// 处理事件 | |
/// </summary> | |
/// <param name="name">Name.</param> | |
public void HandleEvent(string name) | |
{ | |
if (CurState != null && CurState.translationDict.ContainsKey(name)) | |
{ | |
UnityEngine.Debug.Log("fromState:" + CurState.Name); | |
CurState.translationDict[name].callBack(); | |
CurState = CurState.translationDict[name].toState; | |
UnityEngine.Debug.LogWarning("toState:" + CurState.Name); | |
} | |
} |
# 思考
通过上面这一步我们已经搭建了一个基本的状态机模板,上面使用的是非继承的模式,当然我们可以将状态机接口抽象出去,成为一个接口。
/// <summary> | |
/// FSM 有限状态机接口 | |
/// </summary> | |
public interface IFsmState | |
{ | |
void OnEnter(); | |
void OnUpdate(); | |
void OnExit(); | |
} |
如上所示这种形式,我们的状态类通过继承接口来外挂状态行为,这种方式有好有坏,好处是:可以结构更加严谨的使用状态机接口,逻辑清晰。
坏处是:在使用时也许我们会为了一个状态而去创建一个子类,导致子类过多,而子类的功能本身存在重合的情况。
# 并发状态机
相当于同时运行多个有限状态机,解决多状态并行问题。
在 Unity 的 Animator Controller 中,我们每一个创建的控制器就是一个 FSM 状态机,但我们使用多个时,这些状态机并不会相互干扰,独立并发的运行,此时我们可以将这种多个状态机同时运行的情况称之为是并发状态机。
# 分层状态机
相当于在有限状态机中添加层级划分,通过层级来指定当前使用的状态机。
在动画系统中可以看到有 Layer 的概念,这就是使用分层状态机的概念,不过在我们的普通分层状态中,我们的每个层级权重都为 1,也就是说每次运行一个层级时有且只运行这一个层级,而 Unity 增加了权重的概念,这个概念让状态层级之间可以可以状态和层级的混合。
# 下推状态机
相当于使用一个栈来存储状态的历史记录,以便在需要时回到原先的状态。
在退出一个状态时,自动恢复到之前的状态。
如图所示:每个次级状态都与前一个状态存在回溯关系(这个回溯关系是不需要条件的),当我们退出 C 状态时,我们会自动回到 B 状态,退出 B 状态时自动回到 A 状态。
状态机会将每一个使用的状态放入状态栈中,在退出当前状态时自动回溯到上一个状态。
# 相关阅读
- NodeCanvas 之 FSM 有限状态机 - NodeCanvas - 游戏开发 | Fasty Blog = 指尖小屋