零GC有限状态机(FSM)与 基于代码的轻量级行为树
需求场景:一个经典的 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.GetComponent<Animator>(); } 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 List<Node> nodes = new List<Node>(); public Selector(IEnumerable<Node> 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 List<Node> nodes = new List<Node>(); public Sequence(IEnumerable<Node> 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 Func<NodeState> action; public ActionNode(Func<NodeState> 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 List<Node> { // 最高优先级:血量低于 20%,立刻逃跑 new Sequence(new List<Node> { new ActionNode(CheckHealthLow), new ActionNode(FleeAction) }), // 次优先级:如果在攻击范围内,执行攻击 new Sequence(new List<Node> { new ActionNode(CheckInAttackRange), new ActionNode(AttackAction) }), // 第三优先级:如果在视野内,追击玩家 new Sequence(new List<Node> { 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(血量高, 看到玩家, 嘲讽)即可!完全不需要改动原有的巡逻和追击代码!
