【Unity】构建超实用的有限状态机管理类
1.什么是有限状态机?
有限状态机(Finite State Machine, 简称 FSM)。在游戏开发领域,它能够将一个游戏对象的所有行为拆分成一个个的“状态”。
比如,在游戏场景中正在巡逻的敌人,它可能会有“巡逻”、“追踪玩家”、“攻击玩家”、“死亡”等状态,状态与状态之间可以相互切换,同时,敌人在任何时刻都只会处于一个明确的状态,要么它处于巡逻状态,要么它处于追踪玩家状态……。
总结起来,有限状态机有如下的特征/优点:
1.状态唯一性:一个游戏对象在任何时刻只能处于一种明确的状态
2.逻辑清晰:你不再需要写一个巨大的 Update()
函数,里面塞满了 if-else
或 switch
语句。每个状态都是一个独立的模块,只关心自己的事情。
3.易于扩展:如果想创建新的状态,你只需要再添加一个状态子类,然后根据新的状态需要实现的行为,重写父类的方法即可。
2.构建有限状态机!
在学习如何构建有限状态机之前,我建议你对C#的“类的继承”的知识有一定的基础。
2.1 创建状态基类
状态基类不实现任何逻辑,它仅提供可重写的方法,即OnEnter、OnUpdate、OnLeave。状态基类的派生类(子类)需要重写这三个方法,以实现不同的状态。
/// <summary>
/// 有限状态机状态基类
/// 说明:该类用于创建子类,实现有限状态机的状态行为
/// </summary>
public class FSMStateBase
{protected FSM fSM; //状态机引用/// <summary>/// 构造函数/// </summary>/// <param name="target">状态机的所属对象</param>public FSMStateBase(object target, FSM fSM){this.fSM = fSM; }#region 状态方法public virtual void OnEnter() { }public virtual void OnUpdate() { }public virtual void OnLeave() { }#endregion
}
2.2 创建有限状态机
using System.Collections.Generic;
using UnityEngine;/// <summary>
/// 有限状态机类
/// 说明:该类用于控制状态机的运行
/// </summary>
public class FSM
{#region 私有变量private List<FSMStateBase> _fsmStates;private FSMStateBase _currentState;#endregion/// <summary>/// 构造函数/// </summary>public FSM(){_currentState = null;}/// <summary>/// 添加状态机状态/// </summary>/// <param name="fSMStates">有限状态机的状态列表</param>public void AddFSMStates(params FSMStateBase[] fSMStates){//处理状态列表空值if (fSMStates == null){Debug.LogWarning("至少要有一个状态实例");return;}//添加状态实例到列表_fsmStates = new List<FSMStateBase>();foreach (var state in fSMStates){_fsmStates.Add(state);}}/// <summary>/// 启动状态/// </summary>/// <typeparam name="T"></typeparam>public void StartState<T>() where T : FSMStateBase{//阻止重复启动状态if (_currentState != null && _currentState.GetType() == typeof(T)){return;}//先停止上一个状态if (_currentState != null && _currentState.GetType() != typeof(T)){_currentState.OnLeave();}//从状态类的列表里面寻找指定类型的状态实例for (int i = 0; i < _fsmStates.Count; i++){var state = _fsmStates[i];if (state.GetType() == typeof(T)){_currentState = state;_currentState.OnEnter();return;}}Debug.LogWarning($"没有找到{typeof(T)}类型的状态实例,无法启动状态机");}/// <summary>/// 检查当前状态是否为指定的状态/// </summary>/// <typeparam name="T"></typeparam>/// <returns></returns>public bool CheckIsState<T>() where T : FSMStateBase{if (_currentState == null)return false;return _currentState.GetType() == typeof(T);}/// <summary>/// 获得当前的状态机的状态/// </summary>/// <returns></returns>public FSMStateBase GetCurrentState(){return _currentState;}/// <summary>/// 停止状态/// </summary>public void StopState(){if (_currentState == null) return;_currentState.OnLeave();_currentState = null;}#region 状态机状态函数/// <summary>/// 状态函数:进入状态/// </summary>public void OnEnter(){if (_currentState == null) return;_currentState.OnEnter();}/// <summary>/// 状态函数:帧更新/// </summary>public void OnUpdate(){if (_currentState == null) return;_currentState.OnUpdate();}/// <summary>/// 状态函数:离开状态/// </summary>public void OnLeave(){if (_currentState == null) return;_currentState.OnLeave();}#endregion
}
3.使用有限状态机(以敌人为例)
3.1 创建敌人类
using UnityEngine;public class Enemy : MonoBehaviour
{}
3.2 创建状态派生类(子类)
这里我就创建两个状态吧!
//敌人的巡逻状态
public class EnemyState_Patrol : FsmStateBase
{//假设我有一个敌人类private Enemy enemy;public EnemyState_Patrol(object target, FSM fSM) : base(target, fSM){//这个状态机是属于enemy的状态机enemy = target as Customer;}public override void OnEnter(){Debug.Log("进入巡逻状态");}public override void OnUpdate(){Debug.Log("巡逻状态更新");}public override void OnLeave(){Debug.Log("离开巡逻状态");}
}//敌人的追踪玩家状态
public class EnemyState_TracePlayer : FsmStateBase
{//假设我有一个敌人类private Enemy enemy;public EnemyState_Patrol(object target, FSM fSM) : base(target, fSM){//这个状态机是属于enemy的状态机enemy = target as Customer;}public override void OnEnter(){Debug.Log("进入追踪玩家状态");}public override void OnUpdate(){Debug.Log("追踪玩家状态更新");}public override void OnLeave(){Debug.Log("离开追踪玩家状态");}
}
3.3 初始化状态机
public class Enemy : MonoBehaviour
{public Fsm fsm;private void Awake(){fsm = new FSM();fsm.AddFSMStates(new EnemyState_Patrol(this, fsm), //巡逻状态new EnemyState_TracePlayer(this, fsm), //追踪玩家状态);}
}
3.4 接入状态机的函数
最主要的还是将OnUpdate函数接入到Update生命周期函数中。OnEnter和OnLeave在启动/切换状态的时候会自动执行。
using UnityEngine;public class Enemy : MonoBehaviour
{public Fsm fsm;private void Awake(){fsm = new FSM();fsm.AddFSMStates(new EnemyState_Patrol(this, fsm), //巡逻状态new EnemyState_TracePlayer(this, fsm), //追踪玩家状态);}private void Start(){//启动状态fsm.StartState<EnemyState_Patrol>();}private void Update(){ //更新fsm.OnUpdate();}
}
3.5 再加两个调试函数测试一下~
using UnityEngine;public class Enemy : MonoBehaviour
{public Fsm fsm;private void Awake(){fsm = new FSM();fsm.AddFSMStates(new EnemyState_Patrol(this, fsm), //巡逻状态new EnemyState_TracePlayer(this, fsm), //追踪玩家状态);}private void Start(){//启动状态fsm.StartState<EnemyState_Patrol>();}private void Update(){ //更新fsm.OnUpdate();}[ContextMenu("切换到巡逻状态")]public void SwitchToPatrol(){fsm.StartState<EnemyState_Patrol>();}[ContextMenu("切换到追踪玩家状态")]public void SwitchToTracePlayer(){fsm.StartState<EnemyState_TracePlayer>();}
}
3.6 说明
在实际的有限状态机使用中,是在状态派生类里面进行状态的切换(不然你以为状态基类的构造函数里面为什么要引用状态机实例?就是用来给你切换状态的!),而在MonoBehaviour类里面一般只需要初始化和启动有限状态机足矣~