将 Mistreevous 移植到 C#:面向现代 .NET 的高性能行为树库

发布: (2025年12月14日 GMT+8 08:02)
7 min read
原文: Dev.to

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 中构建任何具有复杂代理行为的项目,强烈推荐试一试!欢迎提供反馈、点星和贡献代码。也请告诉我你在服务器端使用行为树时遇到的痛点是什么?

Back to Blog

相关文章

阅读更多 »