ET 框架 AI 框架设计:基于条件判断与可中断并发行为的节点轮询方案

发布时间:2026/9/15 23:38:47
ET 框架 AI 框架设计:基于条件判断与可中断并发行为的节点轮询方案 ET 框架 AI 框架设计基于条件判断与可中断并发行为的节点轮询方案【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET导读本文围绕 ET 框架Unity3D Client And C# Server Framework的设计者所撰写的 AI 框架设计文档展开完整阐述其AI 根据当前状态执行相应行为的核心思想给出一种不同于状态机与行为树的节点轮询 可中断并发行为AI 方案抽象出带条件判断Check与行为执行Run的AINode节点配合每秒一次的调度循环在节点间切换并利用协程取消令牌实现行为中断。读者学完后将能理解状态机与行为树的复杂度缺陷、掌握 ET 风格的 AI 节点编写与调度套路并能在当前仓库源码中定位到支撑该方案的全部底层设施ETCancellationToken、TimerComponent、ETTask。1. 传统 AI 方案的两种思路及其缺陷设计者从多年游戏 AI 开发经验中总结出一个观点AI 难写的本质不是开发者水平问题而是工具设计模式选错了。在引入 ET 方案之前业界主流的 AI 写法主要有两种。a. 状态机节点间转换关系呈 N² 复杂度的网状结构状态机把对象上的任意数据都当作状态并将一些状态抽象成一种新类型的节点当对象状态发生变化时触发节点间转换并执行对应的OnEnter、OnExit等回调。以怪物为例可以把怪物划分为巡逻Patrol、攻击Attack、追击Chase、返回Return等状态其状态转换关系为转换触发条件Patrol → Chase巡逻时发现远处敌人进入追击Patrol → Attack巡逻时发现可攻击的敌人进入攻击Attack → Chase攻击时发现敌人距离过远改为追击Attack → Return攻击时发现敌人距离过远进入返回Chase → Return追击时发现与敌人距离过远进入返回问题立刻显现仅仅 4 个状态就需要罗列如此多的转换关系稍不留神就会遗漏。当节点增多后任意两个节点之间都可能需要连线状态机退化为一张超复杂的网状结构维护复杂度与节点数的平方N²成正比。后来出现的层次状态机Hierarchical State Machine等补丁方案本质上只是在打补丁并未解决状态转换关系爆炸这一根本问题。文档中的结论是状态机不好用不是使用者的责任而是状态机这种模式本身的问题。b. 行为树复杂度降为 N但难以处理持久化并发行为行为树是响应式 AI树从上到下或从左到右本文按从上到下讨论执行本质上是给动作节点排优先级——上面的动作先判断条件是否满足满足则执行。行为树的复杂度为 N相比状态机大幅简化但仍存在明显缺陷AI 过于复杂时树会变得非常庞大难以重新配置。文档以挂机机器人为例要做一个类人的机器人 AI自动做任务、打怪、玩系统、聊天甚至攻击他人这样的树复杂度将难以想象。行为树对持久化过程并发过程管理不佳。例如移动到目标身边这个动作是做成并发过程还是每帧移动一步两种做法都不舒服——这正是行为树处理持续性行为的痛点。2. ET 的核心 AI 思想状态判断 行为执行在批判完传统方案后文档给出 ET 方案的两句灵魂总结AI 就是不断根据当前状态执行相应的行为。这两句话被拆成两个部分状态判断与行为执行。状态判断检查当前条件是否满足行为执行执行对应的行为逻辑。沿用怪物 AI 的例子行为条件Check行为内容Run巡逻在巡逻范围内、周围无敌人选择下一个巡逻点并移动过去到达后停留一段时间再选下一个点攻击敌人在警戒范围内发现敌人攻击距离足够则攻击不足则移动过去再攻击返回与出生点距离超过阈值加上无敌 buff移动到出生点到达后移除无敌 buff关键区别在于与状态机不同这三个状态的切换完全不关心上一个状态是什么只关心当前条件是否满足满足就执行对应行为。行为可以瞬间完成也可以是持续过程如巡逻是选点 → 移动 → 到达 → 停留 → 再选点的循环。3. AINode 抽象节点条件判断 可取消并发行为基于上述思想AI 框架设计变得非常简洁抽象一个 AI 节点每个节点包含一个条件判断附带一个行为实现行为方法应是一个并发过程。文档中的第一版节点定义public class AINode { public virtual bool Check(Unit unit) // 测试条件是否满足 { } public virtual ETTask Run(Unit unit) { } }进一步思考怪物在巡逻时发现敌人应当中断当前巡逻转去执行攻击行为。因此行为必须支持被中断即行为并发过程必须支持取消。这里文档特别强调行为 Run 方法中的任何并发过程都必须支持取消操作于是节点定义升级为public class AINode { public virtual bool Check(Unit unit) { } public virtual ETVoid Run(Unit unit, ETCancelToken cancelToken) { } }3.1 命名演进的源码佐证ETCancelToken → ETCancellationToken文档写作时使用的类型名为ETCancelToken与TimeComponent属于早期版本命名。在当前仓库中该类型已演进为ETCancellationToken位于 ETCancellationToken.cs其核心实现为public class ETCancellationToken { private HashSetComponentAction actions HashSetComponentAction.Create(); public object Context; // 可以带一个数据 public void Add(Action callback) { // 如果action是null绝对不能添加要抛异常说明有协程泄漏 this.actions.Add(callback); } public void Remove(Action callback) { this.actions?.Remove(callback); } public bool IsDispose() { return this.actions null; } public void Cancel() { if (this.actions null) { return; } this.Invoke(); } // ... }从源码可以看到Cancel()的内部机制取消令牌内部维护一个HashSetComponentAction回调集合Cancel()一次性取出全部回调并逐个Invoke()之后将集合置空IsDispose()为 true。这意味着每个并发过程在开始前通过Add(callback)注册自己的取消回调取消时所有注册的回调被同步触发从而打断对应的await取消后令牌不可复用actions null这也是文档调度循环中每次切换节点前都new 一个新令牌的原因。配套的辅助扩展位于 ETCancellationTokenHelper.cs提供了IsCancel()判断是否已被取消、TimeoutAsync超时自动取消等能力均可用于 AI 节点内部的并发控制。4. 实战实现巡逻 / 攻击 / 返回三个 AI 节点在明确了节点抽象后文档演示了三个具体节点XunLuoNode巡逻、GongjiNode攻击、FanHuiNode返回。巡逻节点完整实现如下public class XunLuoNode: AINode { public virtual bool Check(Unit unit) { if (not in patrol range) // 不在巡逻范围内 { return false; } if (there are enemies around) // 周围有敌人 { return false; } return true; } public virtual ETVoid Run(Unit unit, ETCancelToken cancelToken) { while (true) { Vector3 nextPoint FindNextPoint(); bool ret await MoveToAsync(nextPoint, cancelToken); // 移动到目标点返回 false 表示过程被取消 if (!ret) { return; } // 停留两秒注意这里任何并发过程都必须能被取消 bool ret await TimeComponent.Instance.Wait(2000, cancelToken); if (!ret) { return; } } } }攻击、返回两个节点可用同样的套路实现。需要注意这段示例中的两个写法在仓库中的对应关系TimeComponent.Instance.Wait(time, cancelToken)对应当前仓库的 TimerComponent.WaitAsync。其源码显示WaitAsync内部会通过ETTask.GetContextAsyncETCancellationToken()从当前协程上下文取出取消令牌并向令牌注册CancelActionpublic static async ETTask WaitAsync(this TimerComponent self, long time) { if (time 0) { return; } long timeNow self.GetNow(); ETTask tcs ETTask.Create(true); TimerAction timer self.CreateTimerAction(TimerClass.OnceWaitTimer, timeNow, time, 0, tcs); long timerId timer.Id; ETCancellationToken cancellationToken await ETTask.GetContextAsyncETCancellationToken(); try { cancellationToken?.Add(CancelAction); await tcs; } finally { cancellationToken?.Remove(CancelAction); } return; void CancelAction() { if (!self.Remove(timerId)) { return; } tcs.SetResult(); } }这段源码从底层印证了文档强调的规则只要把 cancelToken 传入并发方法取消时定时器会被立即移除、等待立即返回——这正是任何并发过程必须可取消的实现保证。ETTask/ETVoid/Coroutine()等类型与扩展则位于 ETTask.cs 与 ETTaskExtensions.cs。移动并发示例中的MoveToAsync(nextPoint, cancelToken)在移动模块cn.etetet.move中有对应实现AI 节点只负责调用不重复实现移动逻辑这也呼应了后文并发方法共享复用的原则。5. AI 调度循环每秒轮询节点并切换设计好节点还不够还需要把节点串联起来让 AI 能轮转起来。文档给出的调度核心代码AINode[] aiNodes {xunLuoNode, gongjiNode, fanHuiNode}; AINode current; ETCancelToken cancelToken; while(true) { // 每秒重新判断是否有新行为满足条件这个时间可自行设置 await TimeComponent.Instance.Wait(1000); AINode next; foreach(var node in aiNodes) { if (node.Check()) { next node; break; } } if (next null) { continue; } // 如果下一个节点与当前节点相同则不执行 if (next current) { continue; } // 停止当前并发过程 cancelToken.Cancel(); // 执行下一个并发过程 cancelToken new ETCancelToken(); next.Run(unit, cancelToken).Coroutine(); }这段代码逻辑非常清晰可以拆解为四个步骤周期判断每隔 1 秒时间可自行配置重新遍历一次节点数组取第一个条件满足的节点作为next——数组顺序即行为优先级这一点与行为树上面优先的语义一致空转保护没有任何节点满足条件则continue保持当前行为不动去重优化next current时直接跳过避免对正在执行的行为做无意义的重复启动中断切换先cancelToken.Cancel()停掉当前并发过程再new一个全新令牌并Run(...).Coroutine()启动新行为。Run(...).Coroutine()即 ETTask.cs 中定义的火并忘fire-and-forget启动方式让行为以协程方式异步运行不阻塞调度循环。6. 使用中的三个误区要点总结文档最后总结了使用该方案时必须注意的三个要点这也是可取消并发设计最容易踩坑的地方行为中存在并发过程则必须可取消并传入 cancelToken。否则一旦怪物满足下一个节点的执行条件就无法中断当前并发过程AI 将卡死或错乱。这是本方案成立的前提由 TimerComponentSystem.cs 的WaitAsync实现从底层保证。与行为树、状态机不同节点只是一段逻辑节点本身不需要共享共享的是并发方法。例如MoveToAsync怪物的巡逻节点可以用攻击节点追击敌人时同样可以使用。这大大降低了代码重复也降低了节点间的耦合。节点可以做得很大。例如自动做任务节点移动到 NPC → 接任务 → 根据任务的子任务逐个完成移动到怪物点打怪、移动到采集点采集→ 全部完成后移动到任务 NPC 交任务。整段逻辑只需写在一个while循环里用一段并发串协程序列串联起来即可。7. 延伸用节点拼装挂机机器人文档在结尾抛出一个大问题如何设计挂机机器人bot挂机机器人需要的能力包括自动做任务、自动玩各种系统、自动攻击敌人、被攻击会反击、会找人聊天等。答案在本文的框架下变得异常简单——把上面提到的每一项能力各做成一个 AI 节点自动做任务 →TaskNode自动玩系统 →SystemNode自动攻击敌人 →AttackNode被攻击反击 →CounterAttackNode找人聊天 →ChatNode再将这些节点按优先级放进调度循环的节点数组配合各自的条件判断Check机器人就能像怪物 AI 一样自主轮转任务没做完时执行任务节点周围有敌人时切换攻击节点空闲时找人聊天……正如文档所言兄弟AI 简单还是不简单——当 AI 被拆解为条件判断 行为并发的节点组合后复杂度被分解到单个节点内部整体维护成本远低于状态机的 N² 网状结构和行为树的巨型树。8. 小结ET 的这套 AI 方案核心可以概括为一句话用条件判断代替状态转换用可取消的并发行为代替状态回调用每秒优先级轮询代替树遍历。复杂度可控节点间无显式连线切换关系收敛为数组顺序即优先级新增/删除行为只需增删数组元素并发友好行为天然是协程配合ETCancellationToken实现干净的中断语义底层由 TimerComponent.WaitAsync 提供取消保证可组合可复用节点即逻辑、并发方法即共享资产小到怪物 AI大到挂机机器人都是同一套节点 调度循环的拼装。感兴趣的读者可以在仓库中继续深入阅读 ETCancellationToken.cs、ETCancellationTokenHelper.cs 与 TimerComponentSystem.cs并结合 cn.etetet.move 模块的移动实现将本文的示例落地为可直接运行的怪物 AI。【免费下载链接】ETUnity3D Client And C# Server Framework项目地址: https://gitcode.com/GitHub_Trending/et/ET创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考