【Unity】构建超实用的有限状态机管理类

1.什么是有限状态机?

有限状态机(Finite State Machine, 简称 FSM)。在游戏开发领域,它能够将一个游戏对象的所有行为拆分成一个个的"状态"。

比如,在游戏场景中正在巡逻的敌人,它可能会有"巡逻"、"追踪玩家"、"攻击玩家"、"死亡"等状态,状态与状态之间可以相互切换,同时,敌人在任何时刻都只会处于一个明确的状态,要么它处于巡逻状态,要么它处于追踪玩家状态......。

总结起来,有限状态机有如下的特征/优点:

1.状态唯一性:一个游戏对象在任何时刻只能处于一种明确的状态

2.逻辑清晰:你不再需要写一个巨大的 Update() 函数,里面塞满了 if-elseswitch 语句。每个状态都是一个独立的模块,只关心自己的事情。

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类里面一般只需要初始化和启动有限状态机足矣~

相关推荐
AA陈超5 小时前
虚幻引擎5 GAS开发俯视角RPG游戏 P05-01.创建游戏玩法标签
c++·游戏·ue5·游戏引擎·虚幻
EQ-雪梨蛋花汤6 小时前
【Unity笔记】Unity Lighting Settings 全解析:一文读懂烘焙光照的每个参数(VR项目Lighting优化)
笔记·unity·vr
WangMing_X7 小时前
《使用模块化分层来达到企业级项目要求》
开发语言·c#
c#上位机10 小时前
wpf之ToggleButton控件
c#·wpf
mit6.82419 小时前
[C# starter-kit] 命令/查询职责分离CQRS | MediatR |
java·数据库·c#
beyond谚语1 天前
C#学习小笔记(完整版)—— Patience
c#
爱吃小胖橘1 天前
Unity网络开发--超文本传输协议Http(1)
开发语言·网络·网络协议·http·c#·游戏引擎
IT小农工1 天前
Word 为每一页设置不同页边距(VBA 宏)
开发语言·c#·word
sali-tec1 天前
C# 基于halcon的视觉工作流-章42-手动识别文本
开发语言·人工智能·算法·计算机视觉·c#·ocr