其他 · 2023-12-26

我的救星——FSM有限状态机

近期在重构自己项目代码,感受到了角色控制的动画、逻辑的耦合,继续有效的控制框架。并且后续可以扩展至其他对象控制器,于是就发现了有限状态机的程序架构思路(其实和unity中的动画状态机是一种思路)

总的来说,有限状态机提供了一种直观且有效的方法来描述系统的行为,并且可以帮助我更好地组织和管理系统的状态转移逻辑,驱动数据变化,动画变化。

有限状态机(Finite State Machine,FSM)是一种数学模型和计算机科学中常用的概念,用于描述具有有限个状态以及在这些状态之间转移的系统。有限状态机由一组状态、初始状态、转移条件和动作组成。


有限状态机通常包含以下几个要素:

  • 状态(States):系统可以处于的不同状态。每个状态代表系统在某个时刻的特定情况或条件。
  • 转移(Transitions):描述系统从一个状态转移到另一个状态的条件。转移通常与特定的输入或事件相关联。
  • 初始状态(Initial State):系统在开始时所处的状态。
  • 转移规则(Transition Rules):定义了系统从一个状态转移到另一个状态的条件和动作。
  • 动作(Actions):与状态转移相关联的操作或行为。

首先,我们需要创建一个事件管理器单例,用于管理和触发事件(当作简易的通信系统):

public class EventManager : MonoBehaviour
{
    public static EventManager instance;
    public event Action<object> OnDataReceived;
    private void Awake()
    {
        if (instance == null){instance = this;}
        else{Destroy(gameObject);}
    }

    public void ChangeState(State newState)
    {
        OnStateChange?.Invoke(newState);
    }
}

 
其次则核心的有限状态机,处理传输的数据变更逻辑,并且修改状态。并且后续可以在其中加入独立的动画管理模块,成功将数据、逻辑、动画三者分离

public class CharacterFSM : MonoBehaviour
{
    //枚举有限状态机状态
    private enum State { Idle, Walking, Running }
    private State currentState;
    private void Start()
    {
        // 订阅事件
        EventManager.instance.OnDataReceived += HandleDataReceived;
    }

    private void HandleDataReceived(object data)
    {
        // 处理接收到的数据
        if (data is int)
        {
            int intValue = (int)data;
            Debug.Log("Received int data: " + intValue);
        }
        else if (data is string)
        {
            string stringValue = (string)data;
            Debug.Log("Received string data: " + stringValue);
        }
        ChangeState(data.State);
        // 可以根据需要处理其他类型的数据
    }
    ChangeState(State state){
    switch (currentState)
        {
            case State.Idle:
                // 在Idle状态下的行为
                Debug.Log("Idle state");
                break;

            case State.Walking:
                // 在Walking状态下的行为
                Debug.Log("Walking state");
                break;

            case State.Running:
                // 在Running状态下的行为
                Debug.Log("Running state");
                break;
        }
    }
}

 
最后,只要在其他触发事件的代码块中调用现事件管理单例的函数传递任意类型的数据至有限状态机中,就可以改变状态了

// 在其他脚本中传递数据
// 这里的obj对象内容就根据自己的项目需求来设计结构就好了
EventManager.instance.SendData(obj);

 


虽然fsm解决了单个游戏对象的状态管理,但是实际在某个场景状态下,fsm直接状态也互相影响。例如敌人的状态会受到玩家状态的影响,需要一定的通信方式。

如果依旧依赖事件通信,则不好溯源通信来源。如果通过改变SO进行存储则过于吃资源,而且本身也是存在于某个状态下的零时数据,过于吃资源。

因此学到了一招数据黑板!
数据黑板(Blackboard)是一种用于在复杂系统中共享和存储信息的技术。在有限状态机(FSM)架构中引入数据黑板可以帮助在状态之间共享信息,使系统更灵活和可扩展。

// 数据黑板类用于存储和共享系统中的数据
public class Blackboard
{
    private Dictionary<string, object> data = new Dictionary<string, object>();
    // 设置指定键的值
    public void SetValue(string key, object value)
    {
        if (data.ContainsKey(key)){ data[key] = value; }
        else { data.Add(key, value); }
    }
    // 获取指定键的值
    public T GetValue<T>(string key)
    {
        if (data.ContainsKey(key)) { return (T)data[key]; }
        return default(T);
    }
}

 
在有限状态机类中,创建一个数据黑板实例,并将其用于存储和共享数据。

public class CharacterFSM : MonoBehaviour
{
    //在unity中监视面版中拖入提前实例化的黑板类
    public Blackboard blackboard;
    private void Start()
    {
        // 设置数据到数据黑板
        blackboard.SetValue("Health", 100);

        // 从数据黑板获取数据
        int health = blackboard.GetValue<int>("Health");
        Debug.Log("Health: " + health);
    }
}

 
之后就可以将数据存储在数据黑板中,并在需要时从数据黑板中读取数据。
这样的通信方式不止可以在FSM之间使用!但是用多了依旧容易耦合,所以落码时得尽可能的模块化!


项目中实际的事件通信现状,让我绝望….