有限动画状态机FSM

适合中小型项目


🎮 从零实现一个轻量级 FSM 状态机:让 Unity 角色行为更清晰

在游戏开发中,角色往往需要在“待机”、“奔跑”、“跳跃”、“攻击”等状态间切换。如果用大量 if-elseswitch 来管理,代码很快会变得混乱难维护。
本文将带你基于接口与泛型,实现一个轻量、类型安全、易于扩展的有限状态机(FSM)系统,并完整演示如何用它控制 Unity 玩家的行为与动画。


🔧 一、为什么需要 FSM?

想象一个简单的玩家逻辑:

void Update()
{
    if (isIdle) { /* 待机逻辑 */ }
    else if (isRunning) { /* 奔跑逻辑 */ }
    else if (isJumping) { /* 跳跃逻辑 */ }
    
    if (Input.GetButtonDown("Jump") && isGrounded && !isJumping)
        StartJump();
}

问题很明显:

  • 逻辑耦合:所有状态挤在一个脚本里;
  • 难以扩展:加个“滑铲”状态?代码更乱;
  • 生命周期模糊:何时初始化?何时清理?

FSM(Finite State Machine) 的核心思想是:

每个状态是一个独立对象,只关心自己的进入、退出和更新逻辑。状态切换由统一的状态机调度。


📦 二、FSM 核心组件设计

我们只需要两个核心文件,就能搭建一个通用 FSM 框架。

1. 状态接口:IFSMState.cs

public interface IFSMState
{
    void Enter();          // 进入状态(一次)
    void Exit();           // 离开状态(一次)
    void LogicalUpdate();  // 每帧更新
}

✅ 定义了所有状态必须遵守的“合同”。

2. 通用状态机:FSM.cs

using System.Collections.Generic;

/// <summary>
/// 有限状态机(FSM)泛型实现类
/// 用于管理状态的切换、更新和状态间的流转
/// </summary>
/// <typeparam name="T">状态类型,必须实现IFSMState接口</typeparam>
public class FSM<T> where T : IFSMState
{
    /// <summary>
    /// 状态注册表,存储所有已注册的状态实例
    /// Key:状态类型(Type),Value:状态实例(T)
    /// </summary>
    public Dictionary<System.Type, T> StateTable { get; protected set; }

    /// <summary>
    /// 上一个激活的状态(只读)
    /// 用于状态回退等场景
    /// </summary>
    public T PrevState { get; protected set; }

    /// <summary>
    /// 当前激活的状态(受保护,仅内部或子类可直接修改)
    /// </summary>
    protected T curState;

    /// <summary>
    /// 有限状态机构造函数
    /// 初始化状态表和状态变量
    /// </summary>
    public FSM()
    {
        // 初始化状态字典
        StateTable = new Dictionary<System.Type, T>();
        // 初始化当前状态和上一状态为默认值(null)
        curState = PrevState = default;
    }

    /// <summary>
    /// 向状态机注册状态
    /// </summary>
    /// <param name="state">要注册的状态实例</param>
    public void AddState(T state)
    {
        // 以状态类型为键,将状态实例存入注册表
        StateTable.Add(state.GetType(), state);
    }

    /// <summary>
    /// 启动状态机并进入指定初始状态(直接传入状态实例)
    /// </summary>
    /// <param name="startState">初始状态实例</param>
    public void SwitchOn(T startState)
    {
        // 设置当前状态为初始状态
        curState = startState;
        // 执行状态进入逻辑
        curState.Enter();
    }

    /// <summary>
    /// 启动状态机并进入指定初始状态(通过状态类型)
    /// 需确保该状态已通过AddState注册
    /// </summary>
    /// <param name="startState">初始状态的Type类型</param>
    public void SwitchOn(System.Type startState)
    {
        // 从状态表中获取对应类型的状态实例
        curState = StateTable[startState];
        // 执行状态进入逻辑
        curState.Enter();
    }

    /// <summary>
    /// 切换到指定的下一个状态(直接传入状态实例)
    /// 会先执行当前状态的退出逻辑,再执行新状态的进入逻辑
    /// </summary>
    /// <param name="nextState">要切换到的目标状态实例</param>
    public void ChangeState(T nextState)
    {
        // 记录当前状态为上一状态
        PrevState = curState;
        // 执行当前状态的退出逻辑
        curState.Exit();
        // 更新当前状态为目标状态
        curState = nextState;
        // 执行目标状态的进入逻辑
        curState.Enter();
    }

    /// <summary>
    /// 切换到指定的下一个状态(通过状态类型)
    /// 需确保该状态已通过AddState注册
    /// </summary>
    /// <param name="nextState">要切换到的目标状态Type类型</param>
    public void ChangeState(System.Type nextState)
    {
        // 记录当前状态为上一状态
        PrevState = curState;
        // 执行当前状态的退出逻辑
        curState.Exit();
        // 从状态表中获取目标状态实例并更新当前状态
        curState = StateTable[nextState];
        // 执行目标状态的进入逻辑
        curState.Enter();
    }

    /// <summary>
    /// 回退到上一个状态
    /// 若上一状态为null(无历史状态),则不执行任何操作
    /// </summary>
    public void RevertToPrevState()
    {
        // 检查上一状态是否有效
        if (PrevState != null)
        {
            // 切换回上一状态
            ChangeState(PrevState);
        }
    }

    /// <summary>
    /// 状态机逻辑更新方法
    /// 需在每一帧调用,执行当前状态的逻辑更新
    /// </summary>
    public void OnUpdate()
    {
        // 执行当前状态的逻辑更新
        curState.LogicalUpdate();
    }
}

 

✅ 泛型设计确保类型安全;自动管理状态生命周期。


🎮 三、实战:用 FSM 控制 Unity 玩家

步骤 1:创建玩家状态基类

// PlayerState.cs
public abstract class PlayerState : IFSMState
{
    protected PlayerController player;
    public PlayerState(PlayerController p) => player = p;

    public abstract void Enter();
    public abstract void Exit();
    public abstract void LogicalUpdate();
}

步骤 2:实现具体状态

待机状态

public class IdleState : PlayerState
{
    public IdleState(PlayerController p) : base(p) { }

    public override void Enter()
    {
        player.animator.Play("Idle"); // 👈 动画在这里切换!
    }

    public override void LogicalUpdate()
    {
        if (Mathf.Abs(player.inputHorizontal) > 0.1f)
            player.fsm.ChangeState(new RunState(player));
    }

    public override void Exit() { }
}

奔跑状态

public class RunState : PlayerState
{
    public RunState(PlayerController p) : base(p) { }

    public override void Enter()
    {
        player.animator.Play("Run");
    }

    public override void LogicalUpdate()
    {
        player.transform.Translate(Vector3.right * player.moveSpeed * Time.deltaTime * player.inputHorizontal);
        
        if (Mathf.Abs(player.inputHorizontal) <= 0.1f)
            player.fsm.ChangeState(new IdleState(player));
    }

    public override void Exit() { }
}

💡 关键点:动画切换写在 Enter() 中,确保每次进入状态时播放正确动画。

步骤 3:特化玩家状态机

// PlayerFSM.cs
public class PlayerFSM : FSM<PlayerState>
{
    public PlayerState CurState => curState; // 安全暴露当前状态
}

步骤 4:挂载到 Unity GameObject

// PlayerController.cs
public class PlayerController : MonoBehaviour
{
    public Animator animator;
    public float moveSpeed = 5f;
    public float inputHorizontal { get; private set; }
    
    public PlayerFSM fsm;

    void Start()
    {
        fsm = new PlayerFSM();
        
        // 创建并注册状态(每个状态只实例化一次!)
        var idle = new IdleState(this);
        var run = new RunState(this);
        
        fsm.AddState(idle);
        fsm.AddState(run);
        
        fsm.SwitchOn(idle); // 启动状态机
    }

    void Update()
    {
        inputHorizontal = Input.GetAxisRaw("Horizontal");
        fsm.OnUpdate(); // 驱动状态更新
    }
}

🔗 四、各脚本关系图

PlayerController (MonoBehaviour)
       │
       └── 拥有 → PlayerFSM : FSM<PlayerState>
                     │
                     ├── 管理 → IdleState : PlayerState
                     └── 管理 → RunState  : PlayerState
                                   │
                                   └── 通过 player 引用操作 Animator/Transform
  • 状态机不包含逻辑,只负责调度;
  • 状态类不控制切换,只负责行为;
  • 动画、物理、输入全部由状态类通过 PlayerController 访问。

✅ 五、优势总结

特性说明
高内聚低耦合 每个状态逻辑独立,修改互不影响
类型安全 泛型约束防止状态混用(玩家状态不会误加到敌人机)
易于扩展 新增状态只需继承 PlayerState 并注册
生命周期清晰 Enter/Exit/Update 分离,避免资源泄漏
表现与逻辑分离 动画切换由状态控制,但 FSM 本身不依赖 Unity

🚀 六、后续可扩展方向

  • 支持 带参数的 Enter/Exit(如传递伤害值);
  • 实现 状态过渡表(数据驱动允许哪些状态能互相切换);
  • 加入 协程支持(用于等待动画结束再切换);
  • 构建 分层状态机(HFSM)(如“移动”下包含“走路/跑步”)。

💬 结语

这个轻量 FSM 系统仅需 200 行左右核心代码,却能极大提升角色行为系统的可维护性。它不依赖 Unity 特性,也可用于服务器逻辑、UI 状态管理等场景。

好的架构不是一开始就复杂,而是让复杂的事情变得简单。

如果你觉得有用,欢迎点赞、收藏或分享!也欢迎在评论区讨论你的 FSM 实践经验 😊


附:完整项目结构建议

Scripts/
├── FSM/
│   ├── IFSMState.cs
│   └── FSM.cs
├── Player/
│   ├── PlayerController.cs
│   ├── PlayerFSM.cs
│   ├── States/
│   │   ├── PlayerState.cs
│   │   ├── IdleState.cs
│   │   └── RunState.cs

希望这篇博客对你有帮助!如需 GitHub 示例工程或视频讲解,也可以告诉我~

 

posted @ 2025-12-22 23:14  好人就是我啦  阅读(1)  评论(0)    收藏  举报