将 Mistreevous 移植到 C#:面向现代 .NET 的高性能行为树库
Source: Dev.to
当我开始为多人游戏构建专用服务器时,遇到了一个非常真实的问题:服务器端实体需要丰富、结构化且一致的 AI 行为。在单人游戏中,这通常由客户端处理,但在现代多人游戏采用权威服务器的情况下,AI 必须在服务器端运行——这彻底改变了性能和架构的需求。
行为树是显而易见的选择。我爱上了 Mistreevous,这是一款由 nikkorn 编写的设计精美的 TypeScript 库,因其简洁、模块化且高度表达性的 DSL 而备受推崇。挑战在于:将同样的优雅带入 .NET 世界——但要针对服务器 tick 循环的苛刻现实进行优化。
为什么选择行为树?
行为树在游戏、机器人、仿真以及任何基于代理的系统中表现出色,因为它们提供:
- 层次化组合
- 极佳的可读性和意图清晰度
- 强大的模块化
- 易于调试
- 可预测的执行顺序
对于多人服务器而言,它们还能带来:
- 每个 tick 的极低开销
- 确定性的结果
- 逻辑与状态的清晰分离
- 横向扩展到数百个实体
Mistreevous 已经在 TypeScript 中实现了这些设计。C# 版必须同样具备表达力——但必须 真正适配服务器。
为什么特别选择 Mistreevous?
原始的 Mistreevous 之所以突出,是因为:
- 流畅、易读的 DSL
- 明确定义的节点类型
- 简单的解析方式
- 完全兼容 JSON
- 直观的 API
它是为 JavaScript 运行时构建的。在 .NET(尤其是 .NET 9 和 .NET Standard 2.1)中,我们对内存和性能拥有更大的控制权。目标是:保持精神和兼容性,同时消除分配,让它在服务器上发挥极致性能。
不仅是移植——一次面向性能的重写
在保持 100 % 语义兼容的前提下,几乎所有内部细节都被重新设计。
零分配:核心原则
主要目标很简单:调用 Step()(一次 AI tick)时必须 不产生任何垃圾。在一个拥有数十甚至数百个实体、每秒 tick 30–60 次的服务器上,哪怕是极小的重复分配也会导致 GC 噩梦。
采用的关键技术包括:
- 在热点路径中去除 LINQ——没有枚举器、闭包或临时集合。
- 可复用缓冲区和池——预分配、线程安全的列表在 tick 之间复用。
- 手动基于 Span 的解析——逐字符处理,取代
Split()或正则表达式。 - 不捕获闭包——尽可能避免委托分配。
- 经典
for循环——绕过枚举器开销。 - 紧凑的节点设计——固定数组、轻量结构体、最少字段。
完整 DSL 与 JSON 兼容性
这是不可妥协的要求:原始 Mistreevous 中的任何树都必须能够不经修改直接使用。
root {
sequence {
action [CheckHealth]
selector {
action [Flee]
action [Fight]
}
}
}
MistreevousSharp 支持:
- 完全相同的
SUCCESS/FAILURE/RUNNING语义 - 相同的 guard(守卫)评估
- 子树引用
- 装饰器行为
- 执行顺序
你可以在 TypeScript 和 C# 项目之间直接复制粘贴树定义。
项目架构概览
仓库的结构映射了概念模型,同时将优化点显式化:
MistreevousSharp/
├── assets/ # 图片和缩略图
├── example/ # 完整可运行示例(MyAgent + Program.cs)
├── src/Mistreevous/
│ ├── Agent.cs
│ ├── BehaviourTree.cs # 核心执行引擎
│ ├── BehaviourTreeBuilder.cs
│ ├── MDSLDefinitionParser.cs # 零分配 DSL 解析器
│ ├── Nodes/
│ │ ├── Composite/ (Sequence, Selector, Parallel, …)
│ │ ├── Decorator/ (Repeat, Retry, Flip, …)
│ │ └── Leaf/ (Action, Condition, Wait)
│ ├── Attributes/ # Guard 与回调
│ └── Optimizations/ # 零分配辅助工具
├── .github/workflows/ # 自动化 NuGet 发布
└── README.md
Optimizations/ 文件夹是 C# 版独有的——它集中存放所有在 TypeScript 原版中不存在的性能关键技巧。
粗略的性能对比
| 操作 | 原始 Mistreevous (TS) | MistreevousSharp (C#) | 提升幅度 |
|---|---|---|---|
| DSL 解析 | ~0.8 ms | ~0.35 ms | 约 2.3 倍更快 |
| 每 tick 执行 | 结果不定(GC 暂停) | 稳定约 0.00 ms | 几乎为零开销 |
| 每 tick 分配数量 | 多个对象 | 零 | 完全消除 |
实际收益会随实体数量呈指数级增长。
C# 使用示例
var definition = @"
root {
sequence {
action [CheckHealth]
selector {
action [Flee]
action [Fight]
}
}
}";
var agent = new MyAgent(); // 实现你的动作回调
var tree = new BehaviourTree(definition, agent);
while (gameIsRunning)
{
tree.Step(); // 一次 AI tick —— 零分配
}
完整示例请参见仓库的 example/ 文件夹。
适用场景
除了多人服务器,MistreevousSharp 还非常适合:
- Unity 游戏(无 GC 峰值)
- Godot C# 项目
- 仿真与自主代理
- 机器人与无人机控制
- 任何需要模块化、可读 AI 的系统
结语
将 Mistreevous 移植到 C# 远不止翻译——它要求我们重新思考每一次分配、每一个循环以及每一个对象,以适配 .NET 的现实。最终得到的库既保留了原作的优雅与兼容性,又真正具备高性能、服务器级的扩展能力。
如果你在 .NET 中构建任何具有复杂代理行为的项目,强烈推荐试一试!欢迎提供反馈、点星和贡献代码。也请告诉我你在服务器端使用行为树时遇到的痛点是什么?