需求场景一个经典的 2D/3D 动作游戏主角。拥有待机Idle、跑动Run、跳跃Jump、攻击Attack状态。商业级痛点与解决方案垃圾回收GC卡顿新手常写ChangeState(new AttackState())这会导致疯狂产生内存垃圾触发 GC 引起掉帧。方案在初始化时预先创建所有状态并缓存状态字典。手感与动画脱节状态切换必须和动画严格对应。方案在基类中封装动画控制。1. FSM 核心基类using System.Collections.Generic; using UnityEngine; // 所有主角状态的基类 public abstract class PlayerState { protected PlayerController player; // 状态持有者 protected Animator anim; public PlayerState(PlayerController player) { this.player player; this.anim player.GetComponentAnimator(); } public virtual void Enter() {} public virtual void LogicUpdate() {} // 放在 Update 中执行 public virtual void PhysicsUpdate() {} // 放在 FixedUpdate 中执行 public virtual void Exit() {} }2. 主角上下文状态机本体[RequireComponent(typeof(Animator), typeof(Rigidbody2D))] public class PlayerController : MonoBehaviour { private PlayerState currentState; // 【商业级规范】缓存所有状态拒绝运行时 new实现 0 GC public IdleState idleState; public RunState runState; public AttackState attackState; void Awake() { // 预实例化所有状态 idleState new IdleState(this); runState new RunState(this); attackState new AttackState(this); } void Start() { ChangeState(idleState); // 初始状态 } void Update() { currentState?.LogicUpdate(); } // 核心切换逻辑 public void ChangeState(PlayerState newState) { if (currentState newState) return; currentState?.Exit(); currentState newState; currentState?.Enter(); } }3. 具体状态实现以“攻击状态”为例public class AttackState : PlayerState { private float attackDuration 0.5f; // 攻击后摇时间 private float timer; public AttackState(PlayerController player) : base(player) {} public override void Enter() { timer 0; anim.Play(Player_Attack); // 播放攻击动画 // player.audio.Play(Swing); // 播放音效 // 可以在这里开启武器的碰撞体Hitbox } public override void LogicUpdate() { timer Time.deltaTime; // 商业级细节攻击时不允许移动必须等后摇结束自动切回 Idle if (timer attackDuration) { player.ChangeState(player.idleState); } } public override void Exit() { // 可以在这里关闭武器的碰撞体 } } public class IdleState : PlayerState { public IdleState(PlayerController player) : base(player) {} public override void Enter() anim.Play(Player_Idle); public override void LogicUpdate() { // 玩家按下攻击键切入攻击状态 if (Input.GetButtonDown(Fire1)) { player.ChangeState(player.attackState); return; } // 玩家按下方向键切入跑动状态 if (Mathf.Abs(Input.GetAxis(Horizontal)) 0.1f) { player.ChangeState(player.runState); } } }总结这套 FSM 逻辑极其死板且精准玩家输入什么指令就严格执行什么状态不多走一步这就是硬核动作游戏要的“绝佳手感”。商业级案例二怪物 AI —— 基于代码的轻量级行为树BT需求场景近战哥布林。优先判断血量低血量逃跑发现玩家就追距离够了就攻击没事干就巡逻。商业级痛点与解决方案虽然商业开发常用 NodeCanvas 等可视化连线插件但底层依然是用代码驱动的。我们手写一个极简、优雅的 Fluent API链式调用行为树引擎让你彻底理解它的底层运作。1. 行为树极简底层引擎可直接复用using System; using System.Collections.Generic; using UnityEngine; public enum NodeState { Running, Success, Failure } public abstract class Node { public abstract NodeState Evaluate(); } // 选择节点Selector从左到右执行有一个成功就算成功常用于优先级判断 public class Selector : Node { protected ListNode nodes new ListNode(); public Selector(IEnumerableNode nodes) this.nodes.AddRange(nodes); public override NodeState Evaluate() { foreach (var node in nodes) { switch (node.Evaluate()) { case NodeState.Running: return NodeState.Running; case NodeState.Success: return NodeState.Success; // 有一个成功就直接返回 case NodeState.Failure: continue; // 失败了就看下一个 } } return NodeState.Failure; // 全失败才算失败 } } // 顺序节点Sequence从左到右执行必须全成功才算成功常用于条件动作组合 public class Sequence : Node { protected ListNode nodes new ListNode(); public Sequence(IEnumerableNode nodes) this.nodes.AddRange(nodes); public override NodeState Evaluate() { bool anyChildRunning false; foreach (var node in nodes) { switch (node.Evaluate()) { case NodeState.Failure: return NodeState.Failure; // 有一个失败全盘失败 case NodeState.Success: continue; // 成功了继续看下一个 case NodeState.Running: anyChildRunning true; return NodeState.Running; // 正在运行就卡在这里 } } return anyChildRunning ? NodeState.Running : NodeState.Success; } } // 动作节点叶子节点封装 public class ActionNode : Node { private FuncNodeState action; public ActionNode(FuncNodeState action) this.action action; public override NodeState Evaluate() action(); }2. 哥布林 AI 组装与实现在这段代码中我们将展示如何用搭积木的方式构建一个极其聪明的哥布林。public class GoblinAI : MonoBehaviour { public Transform player; public float health 100f; public float attackRange 2f; public float sightRange 10f; private Node rootNode; // 行为树根节点 void Start() { // 商业级采用组合模式像写诗一样构建行为树优先级自上而下。 rootNode new Selector(new ListNode { // 最高优先级血量低于 20%立刻逃跑 new Sequence(new ListNode { new ActionNode(CheckHealthLow), new ActionNode(FleeAction) }), // 次优先级如果在攻击范围内执行攻击 new Sequence(new ListNode { new ActionNode(CheckInAttackRange), new ActionNode(AttackAction) }), // 第三优先级如果在视野内追击玩家 new Sequence(new ListNode { new ActionNode(CheckInSight), new ActionNode(ChaseAction) }), // 垫底优先级上面全不满足乖乖巡逻 new ActionNode(PatrolAction) }); } void Update() { // 每一帧只需要评估这棵树即可不需要任何 if-else 乱飞的状态切换 rootNode.Evaluate(); } // 具体行为逻辑叶子节点 private NodeState CheckHealthLow() { return health 20f ? NodeState.Success : NodeState.Failure; } private NodeState FleeAction() { Debug.Log(救命啊哥布林逃跑了); // 向反方向移动逻辑... return NodeState.Running; // 逃跑是个持续动作 } private NodeState CheckInAttackRange() { float dist Vector3.Distance(transform.position, player.position); return dist attackRange ? NodeState.Success : NodeState.Failure; } private NodeState AttackAction() { Debug.Log(尝尝我的棒子砸); // 播放攻击动画逻辑... return NodeState.Success; // 假设瞬间挥舞完成 } private NodeState CheckInSight() { float dist Vector3.Distance(transform.position, player.position); return dist sightRange ? NodeState.Success : NodeState.Failure; } private NodeState ChaseAction() { Debug.Log(站住别跑追击中...); // 导航走向玩家逻辑... transform.position Vector3.MoveTowards(...) return NodeState.Running; // 追击是持续动作 } private NodeState PatrolAction() { Debug.Log(今天天气真好巡逻中...); // 巡逻逻辑... return NodeState.Running; } }核心价值对比仔细看这段GoblinAI代码如果策划说“给哥布林加个能嘲讽玩家的功能当血量80%且看到玩家时原地跳舞嘲讽”。用 FSM你要在ChaseState和PatrolState里写满打断逻辑。用这套行为树你只需要在Selector的第二个位置逃跑之下攻击之上新插进去一段Sequence(血量高, 看到玩家, 嘲讽)即可完全不需要改动原有的巡逻和追击代码