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
Successonly if all children succeed - Returns
Failureas soon as any child fails (short-circuits) - Returns
Runningwhile 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
Successonly if ALL children succeeded - Returns
Runningwhile 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
Successas soon as any child succeeds (short-circuits) - Returns
Failureonly if all children fail - Returns
Runningwhile 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
Runningwhile any child is still running - Returns
Successonly 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
Successas soon as any child succeeds (wins the race) - Returns
Failureonly when all children have failed - Returns
Runningwhile 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 →