Behavior Trees

Behavior trees define the decision-making logic for characters, institutions, and other entities. They model how an entity chooses actions, responds to conditions, and adapts to its environment. Storybook uses behavior trees for character AI, NPC routines, and complex reactive behavior.

What is a Behavior Tree?

A behavior tree is a hierarchical structure that executes from root to leaves, making decisions based on success/failure of child nodes:

  • Composite nodes (choose, then) have multiple children and control flow
  • Condition nodes (if, when) test predicates
  • Action nodes execute concrete behaviors
  • Decorator nodes (repeat, invert, timeout, etc.) modify child behavior
  • Subtree nodes (include) reference other behavior trees

Behavior trees are evaluated every tick (frame), flowing through the tree from root to leaves until a node returns success or failure.

Syntax

<behavior-decl> ::= "behavior" <identifier> <body>

<body> ::= "{" <prose-blocks>* <behavior-node> "}"

<behavior-node> ::= <selector>
                  | <sequence>
                  | <condition>
                  | <action>
                  | <decorator>
                  | <subtree>

<selector> ::= "choose" <label>? "{" <behavior-node>+ "}"

<sequence> ::= "then" <label>? "{" <behavior-node>+ "}"

<condition> ::= ("if" | "when") "(" <expression> ")"

<action> ::= <identifier> ( "(" <param-list> ")" )?

<param-list> ::= <field> ("," <field>)*

<decorator> ::= <decorator-type> ("(" <params> ")")? "{" <behavior-node> "}"

<subtree> ::= "include" <qualified-path>

<label> ::= <identifier>

Composite Nodes

Selector: choose

A selector tries its children in order until one succeeds. Returns success if any child succeeds, failure if all fail.

Execution:

  1. Try first child
  2. If succeeds -> return success
  3. If fails -> try next child
  4. If all fail -> return failure

Use case: “Try A, if that fails try B, if that fails try C…”

behavior GuardBehavior {
    choose guard_actions {
        AttackIntruder      // Try first
        SoundAlarm          // If attack fails, sound alarm
        FleeInPanic         // If alarm fails, flee
    }
}

Named selectors:

choose service_options {
    then serve_regular {
        CustomerIsWaiting
        TakeOrder
    }

    then restock_display {
        DisplayIsLow
        FetchFromKitchen
    }
}

Labels are optional but recommended for complex trees–they improve readability and debugging.

Sequence: then

A sequence runs its children in order until one fails. Returns success only if all children succeed, failure if any fails.

Execution:

  1. Run first child
  2. If fails -> return failure
  3. If succeeds -> run next child
  4. If all succeed -> return success

Use case: “Do A, then B, then C… all must succeed”

behavior BrewingSequence {
    then brew_potion {
        GatherIngredients   // Must succeed
        MixIngredients      // Then this must succeed
        Boil                // Then this must succeed
        BottlePotion        // Finally this
    }
}

Named sequences:

then prepare_sourdough {
    MixDough
    KneadDough
    ShapeLoaves
}

Nesting Composites

Composite nodes can nest arbitrarily deep:

behavior ComplexAI {
    choose root_decision {
        // Combat branch
        then engage_combat {
            if(enemy_nearby)
            choose combat_tactics {
                AttackWithSword
                AttackWithMagic
                DefendAndWait
            }
        }

        // Exploration branch
        then explore_area {
            if(safe)
            choose exploration_mode {
                SearchForTreasure
                MapTerritory
                Rest
            }
        }

        // Default: Idle
        Idle
    }
}

Condition Nodes

Conditions test expressions and return success/failure based on the result.

if vs. when

Both are semantically identical–use whichever reads better in context:

behavior Example {
    choose options {
        // "if" for state checks
        then branch_a {
            if(player_nearby)
            Attack
        }

        // "when" for event-like conditions
        then branch_b {
            when(alarm_triggered)
            Flee
        }
    }
}

Condition Syntax

if(health < 20)                          // Comparison
when(is_hostile)                         // Boolean field
if(distance > 10 and has_weapon)         // Logical AND
when(not is_stunned or is_enraged)       // Logical OR with NOT

See Expression Language for complete expression syntax.

Action Nodes

Actions are leaf nodes that execute concrete behaviors. They reference action implementations in the runtime.

Basic Actions

behavior SimpleActions {
    then do_things {
        MoveForward
        TurnLeft
        Attack
    }
}

Actions with Parameters

Actions can have named parameters using parenthesis syntax:

behavior ParameterizedActions {
    then patrol {
        MoveTo(destination: "Waypoint1", speed: 1.5)
        WaitFor(duration: 5s)
        MoveTo(destination: "Waypoint2", speed: 1.5)
    }
}

Parameter passing:

  • Parameters are passed as fields (name: value)
  • All value types supported
  • Runtime validates parameter types
behavior RichParameters {
    then complex_action {
        CastSpell(spell: "Fireball", target: enemy_position, power: 75, multicast: false)
        Heal(amount: 50, target: self)
    }
}

Decorator Nodes

Decorators wrap a single child node and modify its behavior. See Decorators for complete reference.

Common Decorators

behavior DecoratorExamples {
    choose {
        // Repeat infinitely
        repeat {
            PatrolRoute
        }

        // Repeat exactly 3 times
        repeat(3) {
            CheckDoor
        }

        // Repeat 2 to 5 times
        repeat(2..5) {
            SearchArea
        }

        // Invert success/failure
        invert {
            IsEnemyNearby  // Returns success if enemy NOT nearby
        }

        // Retry up to 5 times on failure
        retry(5) {
            OpenLockedDoor
        }

        // Timeout after 10 seconds
        timeout(10s) {
            SolveComplexPuzzle
        }

        // Cooldown: only run once per 30 seconds
        cooldown(30s) {
            FireCannon
        }

        // If: only run if condition true
        if(health > 50) {
            AggressiveAttack
        }

        // Always succeed regardless of child result
        succeed_always {
            AttemptOptionalTask
        }

        // Always fail regardless of child result
        fail_always {
            ImpossipleTask
        }
    }
}

Subtree References

The include keyword references another behavior tree, enabling modularity and reuse.

Basic Subtree

behavior PatrolRoute {
    then patrol {
        MoveTo(destination: "Waypoint1")
        MoveTo(destination: "Waypoint2")
        MoveTo(destination: "Waypoint3")
    }
}

behavior GuardBehavior {
    choose guard_ai {
        then combat {
            if(enemy_nearby)
            include combat::engage
        }

        include PatrolRoute  // Reuse patrol behavior
    }
}

Qualified Subtree References

behavior ComplexAI {
    choose ai_modes {
        include behaviors::combat::melee
        include behaviors::combat::ranged
        include behaviors::exploration::search
        include behaviors::social::greet
    }
}

Named Nodes

Both composite nodes (choose, then) can have optional labels:

behavior NamedNodeExample {
    choose daily_priority {                  // Named selector
        then handle_special_orders {         // Named sequence
            CheckOrderQueue
            PrepareSpecialIngredients
            BakeSpecialItem
        }

        choose regular_work {               // Named selector
            then morning_bread {             // Named sequence
                MixSourdough
                BakeLoaves
            }
        }
    }
}

Benefits:

  • Readability: Tree structure is self-documenting
  • Debugging: Named nodes appear in execution traces
  • Narrative: Labels can be narrative (“handle_special_orders” vs. anonymous node)

Guidelines:

  • Use labels for important structural nodes
  • Use descriptive, narrative names
  • Omit labels for trivial nodes

Complete Examples

Simple Guard AI

behavior GuardPatrol {
    choose guard_logic {
        // Combat if enemy nearby
        then combat_mode {
            if(enemy_nearby and has_weapon)
            Attack
        }

        // Sound alarm if see intruder
        then alarm_mode {
            if(intruder_detected)
            SoundAlarm
        }

        // Default: Patrol
        then patrol_mode {
            include PatrolRoute
        }
    }
}

Complex NPC Behavior

behavior Innkeeper_DailyRoutine {
    ---description
    The innkeeper's behavior throughout the day. Uses selectors for
    decision-making and sequences for multi-step actions.
    ---

    choose daily_activity {
        // Morning: Prepare for opening
        then morning_prep {
            when(time_is_morning)
            then prep_sequence {
                UnlockDoor
                LightFireplace
                PrepareBreakfast
                WaitForGuests
            }
        }

        // Day: Serve customers
        then day_service {
            when(time_is_daytime)
            choose service_mode {
                // Serve customer if present
                then serve {
                    if(customer_waiting)
                    GreetCustomer
                    TakeOrder
                    ServeMeal
                    CollectPayment
                }

                // Restock if needed
                then restock {
                    if(inventory_low)
                    ReplenishInventory
                }

                // Default: Clean
                CleanTables
            }
        }

        // Night: Close up
        then evening_close {
            when(time_is_evening)
            then close_sequence {
                DismissRemainingGuests
                LockDoor
                CountMoney
                GoToBed
            }
        }
    }
}

Morning Baking Routine

behavior Baker_MorningRoutine {
    ---description
    Martha's morning routine: prepare dough step by step,
    from mixing to shaping to baking.
    ---

    then morning_baking {
        // Start with sourdough
        then prepare_starter {
            CheckStarter
            FeedStarter
            WaitForActivity
        }

        // Mix the dough
        then mix_dough {
            MeasureFlour
            AddWater
            IncorporateStarter
        }

        // Knead and shape
        then shape_loaves {
            KneadDough
            FirstRise
            ShapeLoaves
        }

        // Bake
        then bake {
            PreHeatOven
            LoadLoaves
            MonitorBaking
        }
    }
}

Repeating Behavior

behavior Bakery_CustomerServiceLoop {
    ---description
    The bakery's continuous customer service loop. Uses infinite
    repeat decorator to serve customers throughout the day.
    ---

    repeat {
        then service_cycle {
            // Check for customers
            choose service_mode {
                then serve_waiting {
                    if(customer_waiting)
                    GreetCustomer
                    TakeOrder
                }

                then restock_display {
                    if(display_low)
                    FetchFromKitchen
                    ArrangeOnShelves
                }
            }

            // Process payment
            CollectPayment
            ThankCustomer

            // Brief pause between customers
            timeout(5s) {
                CleanCounter
            }

            PrepareForNextCustomer
        }
    }
}

Retry Logic for Delicate Recipes

behavior Baker_DelicatePastry {
    ---description
    Elena attempting a delicate pastry recipe that requires
    precise technique. Uses retry decorator for persistence.
    ---

    choose baking_strategy {
        // Try when conditions are right
        then attempt_pastry {
            if(oven_at_temperature)
            // Try up to 3 times
            retry(3) {
                then make_pastry {
                    RollDoughThin
                    ApplyFilling
                    FoldAndSeal
                    CheckForLeaks
                }
            }
        }

        // If oven not ready, prepare meanwhile
        then prepare_meanwhile {
            if(not oven_at_temperature)
            then prep_sequence {
                PrepareIngredients
                MixFilling
                ChillDough
            }
        }
    }
}

Integration with Characters

Characters link to behaviors using the uses behaviors clause:

character Guard {
    uses behaviors: [
        {
            tree: guards::patrol_route
            priority: normal
        },
        {
            tree: guards::engage_hostile
            when: threat_detected
            priority: high
        },
        {
            tree: guards::sound_alarm
            when: emergency
            priority: critical
        }
    ]
}

Behavior selection:

  • Multiple behaviors evaluated by priority (critical > high > normal > low)
  • Conditions (when clause) gate behavior activation
  • Higher-priority behaviors preempt lower-priority ones

See Characters for complete behavior linking syntax.

Execution Semantics

Tick-Based Evaluation

Behavior trees execute every “tick” (typically once per frame):

  1. Start at root node
  2. Traverse down to leaves based on composite logic
  3. Execute leaf nodes (conditions, actions)
  4. Return success/failure up the tree
  5. Repeat next tick

Node Return Values

Every node returns one of:

  • Success: Node completed successfully
  • Failure: Node failed
  • Running: Node still executing (async action)

Stateful vs. Stateless

  • Stateless nodes: Evaluate fresh every tick (conditions, simple actions)
  • Stateful nodes: Maintain state across ticks (decorators, long actions)

Example of stateful behavior:

behavior LongAction {
    timeout(30s) {           // Stateful: tracks elapsed time
        ComplexCalculation   // May take multiple ticks
    }
}

Validation Rules

  1. At least one node: Behavior body must contain at least one node
  2. Composite children: choose and then require at least one child
  3. Decorator child: Decorators require exactly one child
  4. Action exists: Action names must reference registered actions in runtime
  5. Subtree exists: include must reference a defined behavior declaration
  6. Expression validity: Condition expressions must be well-formed
  7. Duration format: Decorator durations must be valid (e.g., 5s, 10m)
  8. Unique labels: Node labels (if used) should be unique within their parent
  9. Parameter types: Action parameters must match expected types

Best Practices

1. Prefer Shallow Trees

Avoid:

choose {
    then { then { then { then { Action } } } }  // Too deep!
}

Prefer:

choose root {
    include combat_tree
    include exploration_tree
    include social_tree
}

2. Use Named Nodes for Clarity

Avoid:

choose {
    then {
        IsHungry
        FindFood
        Eat
    }
    Wander
}

Prefer:

choose survival {
    then eat_if_hungry {
        IsHungry
        FindFood
        Eat
    }
    Wander
}

3. Subtrees for Reusability

Avoid: Duplicating logic across behaviors

Prefer:

behavior Combat_Common {
    then attack_sequence {
        DrawWeapon
        Approach
        Strike
    }
}

behavior Warrior {
    include Combat_Common
}

behavior Guard {
    include Combat_Common
}

4. Decorators for Timing

Avoid: Manual timing in actions

Prefer:

timeout(10s) {
    ComplexTask
}

cooldown(30s) {
    SpecialAbility
}

5. Guard for Preconditions

Avoid:

then problematic {
    ExpensiveAction  // Always runs even if inappropriate
}

Prefer:

if(can_afford_action) {
    ExpensiveAction  // Only runs when condition passes
}

Cross-References

  • Reactive AI: Behavior trees continuously react to changing conditions
  • Hierarchical decision-making: Composite nodes create decision hierarchies
  • Modularity: Subtrees enable behavior reuse and composition
  • Narrative-driven design: Named nodes make behavior trees readable as stories