Unity状态机FSM

一:状态机介绍

有限状态机,也称为 FSM(Finite State Machine) ,这些状态是有限的、不重叠的,其在任意时刻都处于有限状态集合中的某一状态。当其获得特定输入时,将从当前状态转换到另一个状态 ,或者仍然保持在当前状态。

状态机的应用领域

--- 玩家动作控制:比如一个玩家动作较多,我们可以使用状态机进行管理

--- UI界面的切换与管理

--- 怪物AI的设计

 

二:状态机设计实例

我们以案例为基础,设计一个人物有三种状态,在三种状态之间自由切换。

我先把里面的几个类描述一下

StateBase:所有状态基类

StateMachine:状态机类,负责管理所有的状态以及切换

PlayerCtrl:玩家控制类,包含一个状态机以及自身控制逻辑

IdleState,RunState,AttackState三种状态

StateTemplate<T>:泛型类,为了解决人物和怪物都存在状态机的时候,可以指定对应的所有者

 

 

 

2.1 设计状态机基类 

一个状态机里面,有很多种状态

,每一种状态都有很多相似的特征,这里我们需要一个状态基类。基类里面包含所有状态的基本信息。
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public abstract class StateBase
{
/// <summary>
/// 每个状态对应不同的ID号
/// </summary>
public int ID { get; private set; }
/// <summary>
/// 状态机
/// </summary>
public StateMachine machine;
/// <summary>
/// Construtor 
/// </summary>
/// <param name="id">状态的id号(id号有对应的枚举)</param>
public StateBase(int id)
    {
        ID = id;
}
//进入状态
public virtual void OnEnter(params object[] args) { }
//状态停留
public virtual void OnStay(params object[] args) { }
//状态退出
public virtual void OnExit(params object[] args) { }
//检查状态
public virtual void OnCheck(params object[] args) { }
 
}
public class StateTemplate<T> : StateBase
{
/// <summary>
/// 状态的拥有者
/// </summary>
public T m_owner;
/// <summary>
/// Constructor 
/// </summary>
/// <param name="id">状态的id号</param>
/// <param name="owner">状态的拥有者</param>
public StateTemplate(int id, T owner) : base(id)
    {
        m_owner = owner;
    }
}
 
 
2.2 状态机类
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
/// 状态机
/// </summary>
public class StateMachine
{
/// <summary>
/// 状态缓存
/// </summary>
public Dictionary<int, StateBase> m_StateCache;
/// <summary>
/// 当前状态
/// </summary>
public StateBase m_CurrentState;
/// <summary>
/// 当前状态的前一个状态
/// </summary>
public StateBase m_PreviousState;
 
#region StateMachine  Constructor
/// <summary>
/// Constructor
/// </summary>
/// <param name="beginState">开始状态</param>
public StateMachine(StateBase beginState)
    {
        m_PreviousState = null;
        m_CurrentState = beginState;
        m_StateCache = new Dictionary<int, StateBase>();
//注册状态
        RegisterState(beginState);
        m_CurrentState.OnEnter();
    }
#endregion
 
#region FSMUpdate  状态机监测状态
/// <summary>
/// 状态机监测状态变化
/// </summary>
public void FSMUpdate()
    {
if (m_CurrentState != null)
        {
            m_CurrentState.OnStay();
            m_CurrentState.OnCheck();
        }
    }
#endregion
 
#region TranslateToState 状态切换
/// <summary>
/// 状态切换
/// </summary>
/// <param name="id">目标状态的id号</param>
/// <param name="args">可变参数</param>
public void TranslateToState(int id, params object[] args)
    {
int key_id = id;
if (!m_StateCache.ContainsKey(key_id))
        {
            Debug.LogError("The key is not Exist");
return;
        }
//当前状态退出
        m_CurrentState.OnExit();
//保存当前状态为下一个新状态的前一个状态
        m_PreviousState = m_CurrentState;
//当前状态更新到下一个新状态
        m_CurrentState = m_StateCache[key_id];
//新的状态开始进入
        m_CurrentState.OnEnter(args);
    }
#endregion
 
#region RegisterState   注册一个新的状态到缓存中
/// <summary>
/// 注册一个新的状态到缓存中
/// </summary>
/// <param name="aState">新状态</param>
public void RegisterState(StateBase aState)
    {
int id = aState.ID;
//状态是否缓存了
if (m_StateCache.ContainsKey(id))
        {
            Debug.LogError("The State has been added the Cache");
return;
        }
//缓存aState状态
        m_StateCache.Add(id, aState);
//设置aState状态的状态机对象
        aState.machine = this;
    }
#endregion
}
 
2.3 每种状态设计
AttackState代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class AttackState : StateTemplate<PlayerCtrl>
{
public AttackState(int id, PlayerCtrl owner) : base(id, owner) { 
 
    }
public override void OnEnter(params object[] args) {
base.OnEnter(args);
        Debug.Log("AttackState Enter");
        m_owner.GetComponent<MeshRenderer>().material.color = Color.red;
    }
public override void OnStay(params object[] args) {
base.OnExit(args);
        machine.TranslateToState(3);
    }
public override void OnExit(params object[] args) {
base.OnExit(args);
        Debug.Log("Attack Exit");
    }
public override void OnCheck(params object[] args)
    {
base.OnCheck(args);
    }
}
 
 
IdleState状态代码如下:
using UnityEngine;
using System.Collections;
public class IdleState : StateTemplate<PlayerCtrl>
{
public IdleState(int id, PlayerCtrl owner) : base(id, owner)
    { }
public override void OnEnter(params object[] args)
    {
base.OnEnter(args);
        Debug.Log("IdleState Enter");
        m_owner.GetComponent<MeshRenderer>().material.color = Color.blue;
 
    }
public override void OnStay(params object[] args)
    {
base.OnStay(args);
    }
public override void OnExit(params object[] args)
    {
base.OnExit(args);
        Debug.Log("IdleState OnExit");
    }
public override void OnCheck(params object[] args) 
    {
 
    }
}
 
RunState代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class RunState : StateTemplate<PlayerCtrl>
{
public RunState(int id, PlayerCtrl owner) : base(id, owner)
    { }
public override void OnEnter(params object[] args)
    {
base.OnEnter(args);
        Debug.Log("RunState Enter");
        m_owner.GetComponent<MeshRenderer>().material.color = Color.green;
    }
public override void OnStay(params object[] args)
    {
base.OnStay(args);
    }
public override void OnExit(params object[] args)
    {
base.OnExit(args);
        Debug.Log("RunState OnExit");
    }
public override void OnCheck(params object[] args)
    {
 
    }
}
2.4 玩家控制类实现
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
 
public class PlayerCtrl : MonoBehaviour
{
    StateMachine m_stateMachine;
// Start is called before the first frame update
void Start()
    {
//初始化状态机
        m_stateMachine = new StateMachine(new IdleState(1,this));
//注册人物的所有状态
        InitState();
    }
void InitState() {
        m_stateMachine.RegisterState(new RunState(2,this));
        m_stateMachine.RegisterState(new AttackState(3, this));
    }
void LateUpdate()
{
        m_stateMachine.FSMUpdate();
    }
// Update is called once per frame
void Update()
    {
if (Input.GetKeyDown(KeyCode.A))
        {
            m_stateMachine.TranslateToState(2);
        }
if (Input.GetKeyDown(KeyCode.B))
        {
            m_stateMachine.TranslateToState(3);
        }
if (Input.GetKeyDown(KeyCode.C))
        {
            m_stateMachine.TranslateToState(1);
        }
    }
}

将该脚本挂载在立方体身上进行测试,按键盘上的ABC可以实现红绿蓝三种状态之间切换。

2.5 总结变化

(1)当把颜色变化,切换为Animator变化。其实本质上是一样的,在进入的时候播放对应的动画即可。在OnCheck的时候检查动画是否播放完毕,进行对应的状态切换。记住:在状态内部也是可以调用TranslateToState的,因为本身每个状态里面包含了对应的状态机

(2)如果是满足每个状态2s时间,进行切换怎么办?其实也是类似的,在OnCheck函数内部不停检测时间即可,根据自己具体的逻辑实现即可。

(3)不管状态机写法怎么变化,本质思想是一样的。

(4)将状态的ID从int类型修改为枚举更容易表达对应的含义。