Leaf Nodes

Leaf nodes are the fundamental building blocks that perform actions, check conditions, and control timing in your behavior trees.

Overview

Leaf nodes are the atomic units of behavior trees—they have no children and perform specific tasks like evaluating conditions, executing actions, or waiting for events. Every behavior tree is ultimately composed of leaf nodes orchestrated by composites and decorators.

This page covers all the core leaf nodes available in ClosureBT, organized by their primary purpose.

Quick Reference

Summary of all leaf nodes and their primary use cases:

Node Returns Primary Use Case
Condition Success/Failure Evaluate boolean conditions
Do Success Execute actions that always succeed
Wait Running → Success Wait for a time duration
WaitUntil Running → Success Wait until condition becomes true
WaitWhile Running → Success Wait while condition is true
JustRunning Running (forever) Placeholder, continuous invalidation
JustSuccess Success Unconditional success, fallback
JustFailure Failure Force failure, testing
JustOnTick Running (forever) Continuous per-tick updates

Condition(string, Func<bool>) / Condition(Func<bool>)

Evaluates a boolean condition and returns Success if true, Failure if false. This node is reactive-aware and will invalidate when the condition value changes.

Sequence(() =>
{
    Condition("Enemy in Range", () => Vector3.Distance(enemy.position, transform.position) < 10f);
    Do("Attack", () => AttackEnemy());
});
Sequence(() =>
{
    Condition("Enemy in Range", () => Vector3.Distance(enemy.position, transform.position) < 10f);
    Do("Attack", () => AttackEnemy());
});

Reactive Behavior

When used in a reactive tree, Condition nodes signal invalidation when the condition changes, causing parent sequences/selectors to re-evaluate.

Do(string, Action) / Do(Action)

Executes an action and immediately returns Success. Completes in a single tick and always succeeds.

Sequence(() =>
{
    Do("Log Start", () => Debug.Log("Starting sequence"));
    Do("Set Flag", () => isActive = true);
    Do("Play Sound", () => audioSource.Play());
});
Sequence(() =>
{
    Do("Log Start", () => Debug.Log("Starting sequence"));
    Do("Set Flag", () => isActive = true);
    Do("Play Sound", () => audioSource.Play());
});

Note: If you need an action that can fail, use a custom Leaf node with custom OnBaseTick logic instead.

Wait(string, float) / Wait(string, Func<float>)

Waits for a specified duration before succeeding. The duration can be a constant or dynamically evaluated each tick.

// Fixed duration
Sequence(() =>
{
    Do("Fire Weapon", () => Fire());
    Wait("Cooldown", 2f);
    Do("Ready", () => isReady = true);
});

// Dynamic duration
Sequence(() =>
{
    Do("Start Spell", () => CastSpell());
    Wait("Cast Time", () => spellData.castTime); // Duration can change
    Do("Complete Spell", () => CompleteSpell());
});
// Fixed duration
Sequence(() =>
{
    Do("Fire Weapon", () => Fire());
    Wait("Cooldown", 2f);
    Do("Ready", () => isReady = true);
});

// Dynamic duration
Sequence(() =>
{
    Do("Start Spell", () => CastSpell());
    Wait("Cast Time", () => spellData.castTime); // Duration can change
    Do("Complete Spell", () => CompleteSpell());
});

WaitUntil(string, Func<bool>) / WaitUntil(Func<bool>)

Waits until a condition becomes true, then succeeds. Returns Running while the condition is false, Success once it becomes true.

Sequence(() =>
{
    Do("Start Animation", () => animator.Play("Attack"));
    WaitUntil("Animation Done", () => !animator.IsPlaying("Attack"));
    Do("Finish", () => CompleteAttack());
});
Sequence(() =>
{
    Do("Start Animation", () => animator.Play("Attack"));
    WaitUntil("Animation Done", () => !animator.IsPlaying("Attack"));
    Do("Finish", () => CompleteAttack());
});

Reactive Behavior

When used in a reactive tree, WaitUntil will signal invalidation if the condition becomes false after succeeding.

WaitWhile(string, Func<bool>) / WaitWhile(Func<bool>)

Waits while a condition remains true, then succeeds when it becomes false. This is the inverse of WaitUntil.

Sequence(() =>
{
    Do("Engage Shield", () => shield.Activate());
    WaitWhile("Shield Active", () => shield.IsActive);
    Do("Shield Depleted", () => OnShieldDepleted());
});
Sequence(() =>
{
    Do("Engage Shield", () => shield.Activate());
    WaitWhile("Shield Active", () => shield.IsActive);
    Do("Shield Depleted", () => OnShieldDepleted());
});

Difference from WaitUntil: WaitUntil succeeds when condition becomes true, WaitWhile succeeds when condition becomes false.

JustRunning(string) / JustRunning()

Always returns Running and marks itself as always invalid, forcing re-entry in reactive trees each tick. Never completes on its own.

// Keep a race running indefinitely
Race(() =>
{
    JustRunning(); // Keeps the race running
    WaitUntil(() => shouldStop); // Wins when condition becomes true
});

// Placeholder for ongoing behavior
Sequence(() =>
{
    Do("Setup", () => Initialize());
    JustRunning("Patrol"); // Placeholder until patrol is implemented
});
// Keep a race running indefinitely
Race(() =>
{
    JustRunning(); // Keeps the race running
    WaitUntil(() => shouldStop); // Wins when condition becomes true
});

// Placeholder for ongoing behavior
Sequence(() =>
{
    Do("Setup", () => Initialize());
    JustRunning("Patrol"); // Placeholder until patrol is implemented
});

JustSuccess(string) / JustSuccess()

Always returns Success immediately. Completes in a single tick and does not perform any actions.

// Optional task in sequence
Selector(() =>
{
    Condition("Has Weapon", () => hasWeapon);
    JustSuccess(); // Succeed anyway if no weapon (optional weapon system)
});

// Fallback success in selector
Selector(() =>
{
    Sequence("Try Primary", () => { /* ... */ });
    Sequence("Try Secondary", () => { /* ... */ });
    JustSuccess(); // Always succeed as last resort
});
// Optional task in sequence
Selector(() =>
{
    Condition("Has Weapon", () => hasWeapon);
    JustSuccess(); // Succeed anyway if no weapon (optional weapon system)
});

// Fallback success in selector
Selector(() =>
{
    Sequence("Try Primary", () => { /* ... */ });
    Sequence("Try Secondary", () => { /* ... */ });
    JustSuccess(); // Always succeed as last resort
});

JustFailure(string) / JustFailure()

Always returns Failure immediately. Completes in a single tick and does not perform any actions.

// Force trying next option in selector
Selector(() =>
{
    JustFailure(); // Force trying next option
    Do("Fallback Action", () => PerformFallback());
});

// Testing failure paths
Sequence(() =>
{
    Do("Setup", () => Initialize());
    JustFailure(); // Force sequence to fail for testing
    Do("Never Runs", () => UnreachableCode());
});
// Force trying next option in selector
Selector(() =>
{
    JustFailure(); // Force trying next option
    Do("Fallback Action", () => PerformFallback());
});

// Testing failure paths
Sequence(() =>
{
    Do("Setup", () => Initialize());
    JustFailure(); // Force sequence to fail for testing
    Do("Never Runs", () => UnreachableCode());
});

JustOnTick(string, Action) / JustOnTick(Action)

Shorthand for JustRunning with an OnTick callback. Executes an action every tick and always returns Running. Never completes unless externally reset or interrupted.

Sequence(() =>
{
    WaitUntil(() => enemySpotted);
    JustOnTick("Track Enemy", () =>
    {
        transform.LookAt(enemy.position);
        distanceToEnemy = Vector3.Distance(transform.position, enemy.position);
    });
});

// Continuous monitoring with termination condition
D.Until(() => targetLost);
JustOnTick("Update Tracking", () =>
{
    aimPosition = PredictTargetPosition(target);
    UpdateCrosshair(aimPosition);
});
Sequence(() =>
{
    WaitUntil(() => enemySpotted);
    JustOnTick("Track Enemy", () =>
    {
        transform.LookAt(enemy.position);
        distanceToEnemy = Vector3.Distance(transform.position, enemy.position);
    });
});

// Continuous monitoring with termination condition
D.Until(() => targetLost);
JustOnTick("Update Tracking", () =>
{
    aimPosition = PredictTargetPosition(target);
    UpdateCrosshair(aimPosition);
});

Next: Decorator Nodes

Modify node behavior with decorator patterns.

Read about Decorator Nodes →