Composite Nodes

Composite nodes orchestrate the execution of child nodes, defining control flow patterns like sequences, fallbacks, and parallel execution.

Overview

Composite nodes are the structural backbone of behavior trees. Unlike leaf nodes that perform actions, composites define how and when their children execute. They control the flow of execution through the tree, implementing patterns like sequential execution, fallback chains, and concurrent operations.

ClosureBT provides five core composite types, each with distinct execution semantics that solve different design problems.

Quick Comparison

Here's a quick comparison to help you choose the right composite:

Composite Execution Success When Failure When Short-circuits
Sequence Sequential All succeed Any fails ✓ (on fail)
Selector Sequential Any succeeds All fail ✓ (on success)
Parallel Simultaneous All complete N/A
Race Simultaneous Any succeeds All fail ✓ (on success)
SequenceAlways Sequential All succeed All fail

Sequence(string, Action) / Sequence(Action)

Executes children sequentially until one fails or all succeed. This is an "all must succeed" pattern that short-circuits on the first failure.

Behavior:

  • Returns Success only if all children succeed
  • Returns Failure as soon as any child fails (short-circuits)
  • Returns Running while the current child is running
  • Skips remaining children after a failure
  • Returns Success immediately if no children exist
Reactive * Sequence("Attack Enemy", () =>
{
    Condition("Has Target", () => target != null);
    Condition("In Range", () => Vector3.Distance(transform.position, target.position) < attackRange);
    Do("Perform Attack", () => Attack(target));
    Wait("Attack Cooldown", 1.5f);
});
// All steps must succeed in order. If target becomes null or moves out of range,
// the reactive system will reset and restart from the invalidated condition.
Reactive * Sequence("Attack Enemy", () =>
{
    Condition("Has Target", () => target != null);
    Condition("In Range", () => Vector3.Distance(transform.position, target.position) < attackRange);
    Do("Perform Attack", () => Attack(target));
    Wait("Attack Cooldown", 1.5f);
});
// All steps must succeed in order. If target becomes null or moves out of range,
// the reactive system will reset and restart from the invalidated condition.

Common Use Cases

Sequential tasks (e.g., "open door → enter room → close door"), multi-step actions, guarded behaviors, initialization sequences.

SequenceAlways(string, Action) / SequenceAlways(Action)

Executes all children sequentially regardless of their success or failure. Unlike regular Sequence, this node always continues to the next child even if one fails.

Behavior:

  • ALWAYS continues to the next child, even if one fails
  • Returns Success only if ALL children succeeded
  • Returns Running while children are still executing
  • Does NOT short-circuit on failure (key difference from Sequence)
SequenceAlways("Cleanup", () =>
{
    Do("Stop Movement", () => StopMoving());        // Runs even if this fails
    Do("Clear Target", () => target = null);        // Runs even if previous failed
    Do("Reset Animation", () => ResetAnimator());   // Runs even if previous failed
    Do("Play Idle", () => PlayIdleAnimation());     // Runs even if previous failed
});
// All cleanup steps execute regardless of individual failures
SequenceAlways("Cleanup", () =>
{
    Do("Stop Movement", () => StopMoving());        // Runs even if this fails
    Do("Clear Target", () => target = null);        // Runs even if previous failed
    Do("Reset Animation", () => ResetAnimator());   // Runs even if previous failed
    Do("Play Idle", () => PlayIdleAnimation());     // Runs even if previous failed
});
// All cleanup steps execute regardless of individual failures

Selector(string, Action) / Selector(Action)

Executes children sequentially until one succeeds. This is a "try until success" pattern that short-circuits on the first successful child.

Behavior:

  • Returns Success as soon as any child succeeds (short-circuits)
  • Returns Failure only if all children fail
  • Returns Running while the current child is running
  • Skips remaining children after a success
Selector("Acquire Item", () =>
{
    Sequence("Pick Up", () =>
    {
        Condition(() => itemNearby);
        Do(() => PickUpItem());
    });

    Sequence("Craft", () =>
    {
        Condition(() => canCraft);
        Do(() => CraftItem());
    });

    Do("Buy", () => BuyItem()); // Last resort
});
Selector("Acquire Item", () =>
{
    Sequence("Pick Up", () =>
    {
        Condition(() => itemNearby);
        Do(() => PickUpItem());
    });

    Sequence("Craft", () =>
    {
        Condition(() => canCraft);
        Do(() => CraftItem());
    });

    Do("Buy", () => BuyItem()); // Last resort
});

Common Use Cases

Fallback chains (e.g., "try melee OR ranged OR flee"), priority selection, conditional branching, error recovery.

Parallel(string, Action) / Parallel(Action)

Executes all child nodes simultaneously every tick. Returns Success only when all children have completed successfully.

Behavior:

  • Ticks all children every tick, regardless of their status
  • Re-enters children that have completed but are now invalid (reactive)
  • Returns Running while any child is still running
  • Returns Success only when all children are Done
  • Exits all children in parallel when the node exits
// Succeeds only when position reached AND animation finished AND 2 seconds elapsed
Parallel(() =>
{
    WaitUntil("Reached Position", () => Vector3.Distance(transform.position, target) < 0.1f);
    WaitUntil("Finished Animation", () => !animator.IsPlaying("Move"));
    Wait("Minimum Duration", 2f);
});
// Succeeds only when position reached AND animation finished AND 2 seconds elapsed
Parallel(() =>
{
    WaitUntil("Reached Position", () => Vector3.Distance(transform.position, target) < 0.1f);
    WaitUntil("Finished Animation", () => !animator.IsPlaying("Move"));
    Wait("Minimum Duration", 2f);
});

Common Use Cases

Running multiple independent behaviors simultaneously (e.g., "move AND rotate AND play animation"), waiting for multiple conditions to all be true, coordinating concurrent tasks.

Race(string, Action) / Race(Action)

Executes all children in parallel and succeeds as soon as any child succeeds. This is a "first-to-succeed wins" pattern.

Behavior:

  • Ticks all children every tick simultaneously
  • Returns Success as soon as any child succeeds (wins the race)
  • Returns Failure only when all children have failed
  • Returns Running while at least one child is still running
  • Re-enters children that completed but are now invalid
Race(() =>
{
    Sequence("Complete Mission", () =>
    {
        MoveTo(() => objective);
        Do(() => CompleteObjective());
    });
    WaitUntil("Enemy Spotted", () => enemyDetected); // Wins if enemy spotted first
    Wait("Timeout", 30f); // Wins after 30 seconds
});
// Succeeds with whichever child succeeds first
Race(() =>
{
    Sequence("Complete Mission", () =>
    {
        MoveTo(() => objective);
        Do(() => CompleteObjective());
    });
    WaitUntil("Enemy Spotted", () => enemyDetected); // Wins if enemy spotted first
    Wait("Timeout", 30f); // Wins after 30 seconds
});
// Succeeds with whichever child succeeds first

Common Use Cases

Alternative win conditions (e.g., "reach goal OR timer expires"), interrupt patterns (e.g., "patrol UNTIL enemy spotted"), timeout alternatives, first-response patterns.

Reactive Behavior

When marked with the Reactive decorator, composite nodes gain the ability to dynamically respond to changing conditions:

  • Checks all previously completed children for invalidation each tick
  • If a previous child invalidates, resets all subsequent children gracefully
  • Restarts execution from the invalidated child (with allowReEnter=true)
  • Enables truly dynamic behaviors that adapt to world state changes
// Without Reactive: Locks in conditions once checked
Sequence("Attack", () =>
{
    WaitUntil("In Range", () => InRange(target));
    Do("Fire", () => Fire());
    Wait(1f);
});
// If target moves out of range after "In Range" succeeds, continues anyway

// With Reactive: Re-evaluates conditions continuously
Reactive * Sequence("Attack", () =>
{
    WaitUntil("In Range", () => InRange(target));
    Do("Fire", () => Fire());
    Wait(1f);
});
// If target moves out of range during Fire or Wait, invalidates and restarts
// Without Reactive: Locks in conditions once checked
Sequence("Attack", () =>
{
    WaitUntil("In Range", () => InRange(target));
    Do("Fire", () => Fire());
    Wait(1f);
});
// If target moves out of range after "In Range" succeeds, continues anyway

// With Reactive: Re-evaluates conditions continuously
Reactive * Sequence("Attack", () =>
{
    WaitUntil("In Range", () => InRange(target));
    Do("Fire", () => Fire());
    Wait(1f);
});
// If target moves out of range during Fire or Wait, invalidates and restarts

Reactive Pattern

See the Reactive Pattern documentation for a deep dive into reactive behavior trees.

Choosing the Right Composite

Use this decision tree to select the appropriate composite:

Do children need to run at the same time?

Yes →

  • All must complete? → Use Parallel
  • First to succeed wins? → Use Race

No → Run sequentially

  • All must succeed?
    • Stop on first failure? → Use Sequence
    • Run all regardless? → Use SequenceAlways
  • Any can succeed? → Use Selector

Next: Leaf Nodes

Fundamental building blocks that perform actions, check conditions, and control timing.

Read about Leaf Nodes →