1.什么是有限状态机?
有限状态机(Finite State Machine, 简称 FSM)。在游戏开发领域,它能够将一个游戏对象的所有行为拆分成一个个的"状态"。
比如,在游戏场景中正在巡逻的敌人,它可能会有"巡逻"、"追踪玩家"、"攻击玩家"、"死亡"等状态,状态与状态之间可以相互切换,同时,敌人在任何时刻都只会处于一个明确的状态,要么它处于巡逻状态,要么它处于追踪玩家状态......。
总结起来,有限状态机有如下的特征/优点:
1.状态唯一性:一个游戏对象在任何时刻只能处于一种明确的状态
2.逻辑清晰:你不再需要写一个巨大的 Update()
函数,里面塞满了 if-else
或 switch
语句。每个状态都是一个独立的模块,只关心自己的事情。
3.易于扩展:如果想创建新的状态,你只需要再添加一个状态子类,然后根据新的状态需要实现的行为,重写父类的方法即可。
2.构建有限状态机!
在学习如何构建有限状态机之前,我建议你对C#的"类的继承"的知识有一定的基础。
2.1 创建状态基类
状态基类不实现任何逻辑,它仅提供可重写的方法,即OnEnter、OnUpdate、OnLeave。状态基类的派生类(子类)需要重写这三个方法,以实现不同的状态。
cs
/// <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 创建有限状态机
cs
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 创建敌人类
cs
using UnityEngine;
public class Enemy : MonoBehaviour
{
}
3.2 创建状态派生类(子类)
这里我就创建两个状态吧!
cs
//敌人的巡逻状态
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 初始化状态机
cs
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在启动/切换状态的时候会自动执行。
cs
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 再加两个调试函数测试一下~
cs
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类里面一般只需要初始化和启动有限状态机足矣~