当前位置: 首页 > news >正文

零GC有限状态机(FSM)与 基于代码的轻量级行为树

需求场景:一个经典的 2D/3D 动作游戏主角。拥有待机(Idle)、跑动(Run)、跳跃(Jump)、攻击(Attack)状态。
商业级痛点与解决方案

  1. 垃圾回收(GC)卡顿:新手常写ChangeState(new AttackState()),这会导致疯狂产生内存垃圾,触发 GC 引起掉帧。方案:在初始化时预先创建所有状态并缓存(状态字典)。
  2. 手感与动画脱节:状态切换必须和动画严格对应。方案:在基类中封装动画控制。
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:你要在ChaseStatePatrolState里写满打断逻辑。
  • 用这套行为树:你只需要在Selector的第二个位置(逃跑之下,攻击之上),新插进去一段Sequence(血量高, 看到玩家, 嘲讽)即可!完全不需要改动原有的巡逻和追击代码!
http://www.cnnetsun.cn/news/2146450.html

相关文章:

  • Python 新手入门,第一个排序算法怎么写
  • 【无标题政企携手谋新篇:清溪镇委领导与光电通讯协会代表莅临金利威调研座谈】
  • 终极指南:5分钟快速掌握TensorFlow Lite Micro嵌入式AI部署
  • 别再买分立元件了!用Matlab脚本快速设计微带线等效电感电容(附ADS验证)
  • ProperTree:3步快速上手跨平台plist编辑神器
  • 【图像加密】基于一维增强Log-logistic混沌映射与改进型重力扩散的图像加密解密(含信息熵)附Matlab代码和参考文献
  • NetBeans 8.2 效率翻倍:除了Ctrl+/,这15个冷门但超实用的快捷键你用过几个?
  • 别再只盯着ChatGLM3-6B了!手把手教你用BGE大模型为你的AI应用注入‘记忆’
  • 银威云进销存ERP系统|PHP多仓管理+双端APP(PC/手机)|小微商家专用进销存软件
  • AM32电调盲启动与堵转保护:从代码看如何让你的穿越机电机稳定起转
  • 别再写一堆if-else了!Matlab的piecewise函数,5分钟搞定复杂分段函数
  • IntelliJ IDEA 2021.1安装后必做的10项高效设置(含Maven/Tomcat/数据库连接)
  • PDF提取效率提升:MinerU 2.5镜像实测,三步完成文档结构化
  • 2026年权威发布:杭州AI搜索优化解决方案如何选?深度测评AI搜索优化服务商避坑指南
  • 终极Obsidian标题自动编号指南:3分钟让笔记结构瞬间专业化 [特殊字符]
  • 如何用Layerdivider将单张图片一键转换为专业PSD分层文件
  • PHP 9.0 Fiber + WeakMap + JIT优化AI机器人性能提升470%?——GitHub未公开的v9-alpha.3内核源码逐行注释版(限时开源)
  • 保姆级教程:在紫光同创PGL50H开发板上,用Verilog手撸一个HDMI彩条发生器
  • Nginx-RTMP-Win32实战:Windows平台流媒体服务器深度配置指南
  • {{date}} 日程模板
  • 苏州大学联合阿里云:让AI“情感支持师“学会同时用多种招式安慰人
  • Word保留格式翻译工具:功能配置与使用指南
  • 7-Zip完全指南:免费开源压缩软件从入门到精通
  • 2026 实测:免费换背景证件照用什么工具?工具推荐:微信里这个“抠图喵”小程序,一键换底色太香了
  • 为什么你的车载C#中控总在高速行驶时断连?揭秘CAN总线抖动与.NET GC暂停的致命耦合(附实时GC调优清单)
  • Java 开发者必知的 5 个 AI 编程工具:效率提升 10 倍的实战指南
  • UVM仿真总在奇怪的地方卡住?手把手教你用Objection机制精准控制Phase结束
  • 消除人声工具
  • 基于YOLOv8的AI自动瞄准工具完整使用指南:让FPS游戏体验更智能
  • 工业级形状匹配实战指南:shape_based_matching的7大技术优势