Porting Mistreevous to C#: A High-Performance Behavior Tree Library for Modern .NET
When I started building dedicated servers for multiplayer games, I ran into a very real problem: server-side entities needed rich, structured, and consistent AI behavior. In single-player games, the client usually handles this. In modern multiplayer titles with authoritative servers, the AI must run server-side — and that completely changes the performance and architectural demands.
Behavior Trees were the obvious choice. I fell in love with Mistreevous, a beautifully designed TypeScript library by nikkorn, thanks to its clean, modular, and highly expressive DSL. The challenge? Bring that same elegance to the .NET world — but optimized for the harsh reality of server tick loops.
This post is the story of that port: MistreevousSharp, a ground-up rewrite focused on zero-allocation, predictability, and raw performance.
Why Behavior Trees?
Behavior Trees shine in games, robotics, simulations, and any agent-based system because they offer:
- Hierarchical composition
- Excellent readability and intent clarity
- Strong modularity
- Easy debugging
- Predictable execution order
For multiplayer servers specifically, they deliver:
- Extremely low cost per tick
- Deterministic outcomes
- Clean separation of logic and state
- Horizontal scaling across hundreds of entities
Mistreevous already nailed the design in TypeScript. The C# version had to be just as expressive — but truly server-ready.
Why Mistreevous Specifically?
The original Mistreevous stands out with:
- A fluid, readable DSL
- Well-defined node types
- Simple parsing
- Full JSON compatibility
- An intuitive API
But it was built for a JavaScript runtime. In .NET (especially .NET 9 and .NET Standard 2.1), we have far more control over memory and performance.
The goal: keep the spirit and compatibility, but eliminate allocations and make it scream on the server.
Not Just a Port — A Performance-Focused Rewrite
While staying 100% semantically compatible with the original, almost every internal detail was re-engineered.
Zero-Allocation: The Core Principle
The primary objective was simple: calling Step() (one AI tick) must generate zero garbage.
On a server with dozens or hundreds of entities ticking 30–60 times per second, even tiny repeated allocations become a GC nightmare.
Key techniques applied:
Eliminated LINQ in hot paths
No enumerators, closures, or temporary collections.Reusable buffers and pools
Pre-allocated, thread-safe lists reused across ticks.Manual Span-based parsing
Character-by-character processing instead ofSplit()or regex.No captured closures
Avoided delegate allocations wherever possible.Classic
forloops
Bypassing enumerator overhead.Compact node design
Fixed arrays, lightweight structs, minimal fields.
Full DSL and JSON Compatibility
A non-negotiable requirement: any tree from the original Mistreevous must work unchanged.
Example DSL:
root {
sequence {
action [CheckHealth]
selector {
action [Flee]
action [Fight]
}
}
}
MistreevousSharp supports:
- Identical SUCCESS / FAILURE / RUNNING semantics
- Same guard evaluation
- Subtree references
- Decorator behavior
- Execution order
You can literally copy-paste trees between TypeScript and C# projects.
Project Architecture Overview
The repository is organized to mirror the conceptual structure while making optimizations explicit:
MistreevousSharp/
├── assets/ → Images and thumbnails
├── example/ → Full working demo (MyAgent + Program.cs)
├── src/Mistreevous/
│ ├── Agent.cs
│ ├── BehaviourTree.cs → Core execution engine
│ ├── BehaviourTreeBuilder.cs
│ ├── MDSLDefinitionParser.cs → Zero-alloc DSL parser
│ ├── Nodes/ → Full node hierarchy
│ │ ├── Composite/ (Sequence, Selector, Parallel, etc.)
│ │ ├── Decorator/ (Repeat, Retry, Flip, etc.)
│ │ └── Leaf/ (Action, Condition, Wait)
│ ├── Attributes/ → Guards and callbacks
│ └── Optimizations/ → Zero-allocation helpers
├── .github/workflows/ → Automated NuGet publish
└── README.md
The Optimizations/ folder is unique to the C# version — it centralizes all performance-critical tricks that don't exist in the TS original.
Rough Performance Comparison
Early benchmarks on modest trees:
| Operation | Original Mistreevous (TS) | MistreevousSharp (C#) | Improvement |
|---|---|---|---|
| DSL Parsing | ~0.8 ms | ~0.35 ms | ~2.3× faster |
| Per-tick execution | Variable (GC pauses) | Stable ~0.00x ms | Near-zero overhead |
| Allocations per tick | Multiple objects | Zero | Completely eliminated |
The real gains scale dramatically with entity count.
Usage Example in C
var definition = @"
root {
sequence {
action [CheckHealth]
selector {
action [Flee]
action [Fight]
}
}
}";
var agent = new MyAgent(); // Implements your action callbacks
var tree = new BehaviourTree(definition, agent);
while (gameIsRunning)
{
tree.Step(); // One AI tick — zero allocations
}
See the full example in the repo's example/ folder.
Where It Shines
Beyond multiplayer servers, MistreevousSharp is perfect for:
- Unity games (no GC spikes)
- Godot C# projects
- Simulations and autonomous agents
- Robotics and drone control
- Any system needing modular, readable AI
Final Thoughts
Porting Mistreevous to C# was far more than translation — it required rethinking every allocation, every loop, and every object for the .NET reality. The result is a library that keeps the original's elegance and compatibility while being truly ready for high-performance, server-scale scenarios.
If you're building anything with complex agent behavior in .NET, give it a try!
GitHub: https://github.com/guinhx/MistreevousSharp
NuGet: https://www.nuget.org/packages/MistreevousSharp
Original Mistreevous (TS): https://github.com/nikkorn/mistreevous
Feedback, stars, and contributions very welcome! Let me know if you've used behavior trees on the server side — what pains did you hit?
Tags: #dotnet #csharp #gamedev #behaviortrees #performance #opensource #multiplayer
Top comments (0)