Debug with ease
Scrub through execution history. See exactly what your AI was thinking.
Declare Variables Right Where You Need Them
No blackboards required. Declare variables inline with Variable() and they automatically appear in the inspector with full state tracking.
No reflection. No magic strings. Full generic support.
Node AcquireItem(Func<string> getItemID, Action lifecycle = null) => Selector("Acquire Item", () =>
{
var itemID = Variable(getItemID);
var item = Variable(() => Item.FindWithID(itemID.Value));
var recipe = Variable(() => ItemDatabase.GetFromID(itemID.Value));
} Node AcquireItem(Func<string> getItemID, Action lifecycle = null) => Selector("Acquire Item", () =>
{
var itemID = Variable(getItemID);
var item = Variable(() => Item.FindWithID(itemID.Value));
var recipe = Variable(() => ItemDatabase.GetFromID(itemID.Value));
} Every variable appears in the inspector with its current value and type. See your AI's state in real-time without touching a blackboard.
Lifecycle Methods Designed For Change
You're prototyping fast. Your AI works. Then design says: "Add equip animations." Audio needs: "Hook footstep sounds." VFX wants: "Particle bursts on state changes." Each request normally means cracking open your state machine, moving logic, introducing bugs. Lifecycle hooks eliminate that friction — inject logic at any phase without restructuring. Your tree stays clean. Zero refactoring.
Equip animation before attack? OnEnter(() => PlayAnimation()). Combat particle burst? OnSuccess(async ct => await SpawnVFX(ct)). Cleanup on interrupt? OnExit has you covered. Every phase is an injection point—start brutally simple,
layer complexity organically, never break what works.
OnEnabled → OnEnter → OnTick → OnSuccess → OnExit → OnDisabled
Guaranteed order of execution throughout your entire behavior tree.
Node UseTool(Action lifecycle = null) => Leaf("Use Tool", () =>
{
OnEnter(async ct => await PlayEquipToolAnimation(ct));
OnSuccess(async ct => await Celebrate(ct));
OnExit(async ct => await PutAwayToolAnimation(ct));
OnBaseTick(() =>
{
PerformToolTask();
return ToolTaskCompleted ? Status.Success : Status.Running;
});
lifecycle?.Invoke()
});
Node AI() => Reactive * Sequence("AI", () =>
{
Sequence("Perform Task", () =>
{
OnSuccess(() => Debug.Log("Perform Task Sequence Succeeded"));
Condition("Has Task", HasTask);
UseTool(() =>
{
OnEnter(() => Debug.Log("Starting Tool Task");
OnSuccess(() => Debug.Log("Tool Task Completed Successfully"));
});
});
// ...
}); Node UseTool(Action lifecycle = null) => Leaf("Use Tool", () =>
{
OnEnter(async ct => await PlayEquipToolAnimation(ct));
OnSuccess(async ct => await Celebrate(ct));
OnExit(async ct => await PutAwayToolAnimation(ct));
OnBaseTick(() =>
{
PerformToolTask();
return ToolTaskCompleted ? Status.Success : Status.Running;
});
lifecycle?.Invoke()
});
Node AI() => Reactive * Sequence("AI", () =>
{
Sequence("Perform Task", () =>
{
OnSuccess(() => Debug.Log("Perform Task Sequence Succeeded"));
Condition("Has Task", HasTask);
UseTool(() =>
{
OnEnter(() => Debug.Log("Starting Tool Task");
OnSuccess(() => Debug.Log("Tool Task Completed Successfully"));
});
});
// ...
}); Reactive * Sequence("Guard Dog", () =>
{
var distance = Variable(() => 0f);
OnTick(() => distance.Value = DistanceToPlayer());
D.ConditionLatch("Too Close", () => distance.Value < 3f);
D.Until("Far Enough", () => distance.Value > 5f);
Chase(() => Player, () =>
{
OnEnter(() => Debug.Log("Guard Dog began chasing player!"));
OnExit(() => Debug.Log("Guard Dog stopped chasing player."));
});
Sequence(() =>
{
WaitUntil("Return To Post", () => MoveTo(Post.transform.position));
JustRunning("Idle");
});
}); Reactive * Sequence("Guard Dog", () =>
{
var distance = Variable(() => 0f);
OnTick(() => distance.Value = DistanceToPlayer());
D.ConditionLatch("Too Close", () => distance.Value < 3f);
D.Until("Far Enough", () => distance.Value > 5f);
Chase(() => Player, () =>
{
OnEnter(() => Debug.Log("Guard Dog began chasing player!"));
OnExit(() => Debug.Log("Guard Dog stopped chasing player."));
});
Sequence(() =>
{
WaitUntil("Return To Post", () => MoveTo(Post.transform.position));
JustRunning("Idle");
});
}); Reactive & Adaptive Re-Evaluate Automatically
Trees automatically detect when conditions change and gracefully exit and re-enter nodes without manual polling. No need to constantly check state changes—the tree adapts intelligently.
When a condition invalidates, all downstream nodes are reset gracefully and the tree re-enters at the exact point needed. Perfect for dynamic game states.
Mark composites as Reactive and they automatically monitor completed children for invalidation. The tree handles cleanup, re-entry, and handling lifecycle methods for you.
Runtime Subtree Insertion
Dynamic Trees
Yield defers node creation until runtime, unlocking powerful patterns impossible with static trees. Build recursive task decomposition systems, swap entire behavior subtrees based on conditions, or construct state machines that adapt to game state.
The example demonstrates recursive task decomposition—nodes call themselves to resolve complex dependencies.
Create reusable behavior functions that compose together like building blocks. Your AI systems become modular libraries where complex behaviors emerge from simple, testable components. No need to rebuild entire trees — just reference the behaviors you need, when you need them.
Node AcquireItem(Func<string> getItemID) => Selector(() =>
{
var itemID = Variable(getItemID);
// Already have it?
Condition(() => Inventory.Contains(itemID.Value));
// Can collect it from world?
D.Condition(() => ItemExists(itemID.Value));
YieldSimpleCached(() => CollectItem(itemID));
// Need to craft it? (RECURSION!)
D.Condition(() => IsCraftable(itemID.Value));
YieldSimpleCached(() => CraftItem(itemID));
});
Node CraftItem(Func<string> getItemID) => Sequence(() =>
{
var recipe = Variable(() => GetRecipe(getItemID()));
// Acquire each ingredient recursively!
D.ForEach(() => recipe.Value.Ingredients, out var item);
YieldSimpleCached(() => AcquireItem(item));
Wait(() => recipe.Value.CraftTime);
}); Node AcquireItem(Func<string> getItemID) => Selector(() =>
{
var itemID = Variable(getItemID);
// Already have it?
Condition(() => Inventory.Contains(itemID.Value));
// Can collect it from world?
D.Condition(() => ItemExists(itemID.Value));
YieldSimpleCached(() => CollectItem(itemID));
// Need to craft it? (RECURSION!)
D.Condition(() => IsCraftable(itemID.Value));
YieldSimpleCached(() => CraftItem(itemID));
});
Node CraftItem(Func<string> getItemID) => Sequence(() =>
{
var recipe = Variable(() => GetRecipe(getItemID()));
// Acquire each ingredient recursively!
D.ForEach(() => recipe.Value.Ingredients, out var item);
YieldSimpleCached(() => AcquireItem(item));
Wait(() => recipe.Value.CraftTime);
}); Node Cook(Func<string[]> getIngredients, out Func<Food> getResultFood)
{
VariableType<Food> resultFood = null;
getResultFood = () => resultFood.Value;
return Leaf("Cook", () =>
{
var timeBegan = Variable(0f);
OnEnter(() =>
{
resultFood.Value = Food.FromIngredients(getIngredients());
timeBegan.Value = Time.time;
BeginCookingAnimation();
});
OnExit(() => StopCookingAnimation());
OnBaseTick(() =>
{
return Time.time - timeBegan.Value >= resultFood.Value.cookTime
? Status.Success
: Status.Running;
});
});
}
Node Eat(Func<Food> getFood) => Leaf("Eat Meal", () =>
{
OnSuccess(() =>
{
var food = getFood();
Health += food.healthRestored;
Hunger = 0;
});
OnBaseTick(() => PerformEatingAnimation() ? Status.Success : Status.Running);
});
Sequence(() =>
{
Condition("Is Hungry", () => Satiation < 50);
Cook(RandomIngredientsFromInventory, out var getFood);
// Cooked food flows directly to Eat - seamless chaining!
Eat(getFood);
}); Node Cook(Func<string[]> getIngredients, out Func<Food> getResultFood)
{
VariableType<Food> resultFood = null;
getResultFood = () => resultFood.Value;
return Leaf("Cook", () =>
{
var timeBegan = Variable(0f);
OnEnter(() =>
{
resultFood.Value = Food.FromIngredients(getIngredients());
timeBegan.Value = Time.time;
BeginCookingAnimation();
});
OnExit(() => StopCookingAnimation());
OnBaseTick(() =>
{
return Time.time - timeBegan.Value >= resultFood.Value.cookTime
? Status.Success
: Status.Running;
});
});
}
Node Eat(Func<Food> getFood) => Leaf("Eat Meal", () =>
{
OnSuccess(() =>
{
var food = getFood();
Health += food.healthRestored;
Hunger = 0;
});
OnBaseTick(() => PerformEatingAnimation() ? Status.Success : Status.Running);
});
Sequence(() =>
{
Condition("Is Hungry", () => Satiation < 50);
Cook(RandomIngredientsFromInventory, out var getFood);
// Cooked food flows directly to Eat - seamless chaining!
Eat(getFood);
}); Parameters & Return Values Chain Behaviors Together
Create reusable node functions that accept dynamic parameters and return values. Pass live data using Func<T> to ensure parameters stay fresh—not frozen at creation time. Your AI
adapts in real-time as conditions change.
Return values using out Func<T> flow seamlessly between nodes — one behavior's output becomes another's
input. Chain behaviors together like building blocks where complex AI emerges
from simple, composable functions.
Build once, reuse everywhere. Create libraries of parameterized nodes that compose together like LEGO bricks. No copying code, no inheritance hierarchies—just pure composition. Write small, focused behaviors that snap together to form complex AI systems naturally.
Reactive Data Streams Data Flows Like Water
Variables aren't just storage—they're live data streams that automatically update downstream. Chain transformations together and watch changes flow through your pipeline without manual updates.
Build smooth camera systems, input filtering, animation blending—anywhere you need clean data transformation. Throttle noisy sensors, buffer recent values, transform on-the-fly. Your data pipeline becomes declarative and self-updating.
Think spreadsheet formulas. Change one cell, dependent cells recalculate instantly. Your variables work the same way—update the source and transformations propagate automatically.
// Raw mouse position updates every tick
var mousePos = UseEveryTick(MouseWorldPosition);
// Build a transformation pipeline
var smoothPos = UsePipe(mousePos,
v => UseThrottle(0.025f, v), // Limit updates
v => UseRollingBuffer(15, false, v), // Keep last 15
v => UseSelect(v, buffer => // Average them
{
var avg = Vector3.zero;
foreach (var pos in buffer) avg += pos;
return avg / buffer.Count;
})
);
// smoothPos.Value is always the smoothed mouse position!
// No manual updates. No polling. Just works. // Raw mouse position updates every tick
var mousePos = UseEveryTick(MouseWorldPosition);
// Build a transformation pipeline
var smoothPos = UsePipe(mousePos,
v => UseThrottle(0.025f, v), // Limit updates
v => UseRollingBuffer(15, false, v), // Keep last 15
v => UseSelect(v, buffer => // Average them
{
var avg = Vector3.zero;
foreach (var pos in buffer) avg += pos;
return avg / buffer.Count;
})
);
// smoothPos.Value is always the smoothed mouse position!
// No manual updates. No polling. Just works. Why ClosureBT?
Write Like You Think
No more visual spaghetti or disconnected nodes. Write behavior trees as code using an intuitive DSL that mirrors your mental model.
Debug with ease
Visual debugger with time travel capabilities. Scrub through execution history to see exactly what happened and when.
Async Support
Built for modern C# with full async/await support. Handle complex timing and interruptions gracefully with lifecycle methods.
The HFSM+BT Hybrid
Combine the strengths of Hierarchical Finite State Machines and Behavior Trees in a unified architecture—the same pattern used in AAA games and validated by robotics research.
Remedy's AI team uses HFSM+BT hybrid for enemy AI. Watch the GDC-style breakdown →
IROS 2022 paper advocating for hybrid FSM+BT architectures in robotics →
State Machines Excel At
- → High-level mode switching (Idle, Combat, Flee)
- → Cyclic transitions between states
- → Mutually exclusive behaviors
- → "What should I be doing?"
Behavior Trees Excel At
- → Task decomposition and sequencing
- → Reactive condition monitoring
- → Hierarchical fallback strategies
- → "How do I accomplish this?"
ClosureBT: Both Paradigms, One API
Use YieldSimple as a state machine. Cache states with ??= and wire transitions via lifecycle callbacks.
Node AI = YieldSimple("Enemy AI", () =>
{
var state = Variable(() => 0); // 0=Idle, 1=Combat, 2=Flee
Node idleNode = null, combatNode = null, fleeNode = null;
OnTick(() => // Global Interrupt
{
if (Health < 20)
state.Value = 2;
});
return () => state.Value switch
{
0 => idleNode ??= IdlePatrol(() =>
{
OnTick(() =>
{
if (HasTargetInSight())
state.Value = 1;
});
}),
1 => combatNode ??= CombatBehavior(() =>
{
OnSuccess(() => state.Value = 0); // Target down
OnFailure(() => state.Value = 2); // Can't win → Flee
}),
2 => fleeNode ??= FleeBehavior(() => OnSuccess(() => state.Value = 0)),
_ => null
};
}); Node AI = YieldSimple("Enemy AI", () =>
{
var state = Variable(() => 0); // 0=Idle, 1=Combat, 2=Flee
Node idleNode = null, combatNode = null, fleeNode = null;
OnTick(() => // Global Interrupt
{
if (Health < 20)
state.Value = 2;
});
return () => state.Value switch
{
0 => idleNode ??= IdlePatrol(() =>
{
OnTick(() =>
{
if (HasTargetInSight())
state.Value = 1;
});
}),
1 => combatNode ??= CombatBehavior(() =>
{
OnSuccess(() => state.Value = 0); // Target down
OnFailure(() => state.Value = 2); // Can't win → Flee
}),
2 => fleeNode ??= FleeBehavior(() => OnSuccess(() => state.Value = 0)),
_ => null
};
});