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.
public 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));
} public 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.
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.