有限动画状态机FSM
适合中小型项目
🎮 从零实现一个轻量级 FSM 状态机:让 Unity 角色行为更清晰
在游戏开发中,角色往往需要在“待机”、“奔跑”、“跳跃”、“攻击”等状态间切换。如果用大量
if-else或switch来管理,代码很快会变得混乱难维护。
本文将带你基于接口与泛型,实现一个轻量、类型安全、易于扩展的有限状态机(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 示例工程或视频讲解,也可以告诉我~

浙公网安备 33010602011771号