MODERN BT FRAMEWORK FOR UNITY

Build AI
Like You Think

A behavior tree framework that uses function closures to create simple, maintainable, and flexible AI systems.

BUY NOW → Get Started
Composition
Time Travel
Async Support

Simple. Powerful. Flexible.

Create complex AI behaviors with code that's easy to read and maintain.

Sequence(() =>
{
    Wait(1, () =>
    {
        OnEnter(() => Debug.Log("Wait Began"));
        OnSuccess(() => Debug.Log("Wait Succeeded"));
    });

    WaitUntil(() => A);
    Selector(() =>
    {
        Condition(() => B);
        Condition(() => C);
    });

    JustSuccess(() =>
    {
        OnSuccess(async ct =>
        {
            await PerformAsyncTask(cancellationToken: ct);
            Debug.Log("Completed!");
        });
    });
});
Sequence(() =>
{
    Wait(1, () =>
    {
        OnEnter(() => Debug.Log("Wait Began"));
        OnSuccess(() => Debug.Log("Wait Succeeded"));
    });

    WaitUntil(() => A);
    Selector(() =>
    {
        Condition(() => B);
        Condition(() => C);
    });

    JustSuccess(() =>
    {
        OnSuccess(async ct =>
        {
            await PerformAsyncTask(cancellationToken: ct);
            Debug.Log("Completed!");
        });
    });
});

Debug with ease

Scrub through execution history. See exactly what your AI was thinking.

Time Travel Debugging Demo
Custom Unity Inspector showing declared variables and node state

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.

OnEnabledOnEnterOnTickOnSuccessOnExitOnDisabled

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?

01

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.

02

Debug with ease

Visual debugger with time travel capabilities. Scrub through execution history to see exactly what happened and when.

03

Async Support

Built for modern C# with full async/await support. Handle complex timing and interruptions gracefully with lifecycle methods.

What You Get

  • Embedded DSL — Clean, readable syntax for declaring behavior trees
  • Composition Based API — Build complex behaviors from simple, reusable functions
  • Time Travel Debugging — Visual debugger with execution history scrubbing
  • Async/Await Support — Full UniTask integration for modern async patterns
  • Graceful Interruptions — Predictable lifecycle methods for handling interruptions
  • Parameterized Nodes — Pass parameters and return values from nodes
  • Recursion Support — Enable advanced patterns like task decomposition
START

Ready to Build Better AI?

Start building intelligent behaviors in minutes with our comprehensive crash course.

BUY NOW → Get Started