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

相关推荐
世洋Blog4 小时前
更好的利用ChatGPT进行项目的开发
人工智能·unity·chatgpt
Eiceblue8 小时前
通过 C# 将 HTML 转换为 RTF 富文本格式
开发语言·c#·html
IUGEI8 小时前
synchronized的工作机制是怎样的?深入解析synchronized底层原理
java·开发语言·后端·c#
czhc114007566310 小时前
C# 1124 接收
开发语言·c#
时光追逐者12 小时前
C#/.NET/.NET Core技术前沿周刊 | 第 62 期(2025年11.17-11.23)
c#·.net·.netcore
司铭鸿12 小时前
祖先关系的数学重构:从家谱到算法的思维跃迁
开发语言·数据结构·人工智能·算法·重构·c#·哈希算法
evolution_language13 小时前
Unity场景(Scene)的注意事项和易错点
unity·游戏引擎·scene
宝桥南山15 小时前
.NET 10 - Blazor web assembly应用的一些诊断方式
microsoft·微软·c#·asp.net·.net·.netcore
EQ-雪梨蛋花汤15 小时前
【AI工具】使用 Doubao-Seed-Code 优化 Unity 编辑器插件:从功能实现到界面美化的完整实践
人工智能·unity·编辑器
m0_6265352017 小时前
代码分析
开发语言·c#