๐ŸŒ AIๆœ็ดข & ไปฃ็† ไธป้กต
Skip to content

[v 0.2.0, still experimental] A lightweight behavior trees C++ library with its real-time visualizer application

License

Notifications You must be signed in to change notification settings

Lecrapouille/BehaviorTree

Folders and files

NameName
Last commit message
Last commit date

Latest commit

ย 

History

40 Commits
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 
ย 

Repository files navigation

๐ŸŒณ C++ Behavior Tree Library

โš ๏ธ The API is still evolving. Expect breaking changes while the library matures.

A Behavior Tree (BT) is a mathematical model of plan execution used in computer science, robotics, control systems and video games. Behavior Trees are used as an alternative to state machines. A Behavior Tree describes switching between a finite set of tasks in a modular fashion. Its strength lies in its ability to create very complex tasks composed of simple tasks, without worrying how the simple tasks are implemented.

This project is a modernized fork of BrainTree and BehaviorTree.CPP focused on debuggable behavior trees for robotics, simulation, and games. The codebase targets C++17, ships with a YAML builder, subtree support, scoped blackboards, and an open source visualizer.

  • Use YAML files to describe trees, subtrees, and blackboard values, then instantiate them with a builder class.
  • Scoped blackboards: every subtree can inherit or override keys via the ${key} syntax, making parameter passing explicit. You can store your own structure.
  • Available builtin nodes: reactive/with-memory composites, classic decorators, and sugar actions/conditions for quick experiments.
  • Visualizer: inspect running trees without proprietary tools (like Groot).

๐Ÿ’ป Download, compilation, installation and usage

  • Prerequisites:
sudo apt-get install libyaml-cpp-dev
  • Download and compilation:
git clone https://github.com/Lecrapouille/BehaviorTree --recurse
cd BehaviorTree
make -j8          # builds library + examples
make test -j8     # optional unit tests
  • This library also comes with a standalone application that listens on port 9090 (by default) and visualizes the running Behavior Tree connected to it.
./build/BehaviorTree-Visualizer --port 9090

A blank windows shall appear waiting a connection with a client application using this library. Launch it before a demo to see live node states.

  • Run the bundled examples
make examples -j8
./build/Example-GameState   # doc/examples/GameState
./build/Example-Patrol      # doc/examples/Patrol

Each example folder contains its own Makefile, YAML description, and C++ entry point. Use them as templates for your own projects.


๐ŸŒฒ Behavior Tree Primer

A Behavior Tree (BT) is a directed acyclic graph used to express decision logic.

  • Tree: A hierarchical parent-children structure of nodes and a blackboard. The root is the initial node. A sub-tree is just an hierarchy view to make the tree smaller and more human readable.
  • Nodes: The basic building blocks of a behavior tree. Nodes are of three types: composite, decorator, and leaf. Each nodes return SUCCESS, FAILURE, or RUNNING. Parents nodes, depending on their type, decide which children to invoke and how to interpret their status.
    • Composite: A node that has several children (at least one child).
    • Decorator: A node that has exactly one child and modifies its status (decoration).
    • Leaf: Nodes that don't have any children; these are the actual actions that interact with the system (i.e., robot commands).
  • Blackboard: Since node instances do not know each other and may want to exchange data, they use a shared key/value structure (dictionary) for exchanging data. In the same way than C++ these values are the arguments of functions (here node): they can be either an input or and output or input-output.
---
config:
  theme: forest
  class:
    hideEmptyMembersBox: true
---
classDiagram
    class Tree
    class Node
    class Composite
    class Decorator
    class Leaf
    class Blackboard

    Node <|-- Composite
    Node <|-- Decorator
    Node <|-- Leaf
    Tree --> Node : root
    Tree --> Blackboard
Loading

๐Ÿชพ Hierarchy of Nodes

The following diagram shows available nodes by this library. Each node will be fully described in this document

---
config:
  theme: forest
  classWidth: 200
  classFontSize: 14
  classFontFamily: "Trebuchet MS"
  curve: basis
  class:
    hideEmptyMembersBox: true
---
classDiagram
    Node <|-- Decorator
    Node <|-- Composite
    Node <|-- Leaf

    Composite <|-- Sequence
    Sequence <|-- Parallel Sequence
    Sequence <|-- Sequence With Memory
    Sequence <|-- Reactive Sequence
    Composite <|-- Selector
    Selector <|-- Parallel Selector
    Selector <|-- Selector With Memory
    Selector <|-- Reactive Selector

    Decorator <|-- Inverter
    Decorator <|-- Force Success
    Decorator <|-- Force Failure
    Decorator <|-- Repeater
    Decorator <|-- Timeout

    Leaf <|-- Action
    Leaf <|-- Condition
    Leaf <|-- Always Success
    Leaf <|-- Always Failure
Loading
  • Leaves:
    • Condition
    • Action
    • Always Success/failure
  • Decorator:
    • Inverter
    • Force Success/failure
    • Repeater
    • Timeout
  • Composite:
    • Sequence (with or without memory or reactive)
    • Selector (with or without memory or reactive)
    • Parallel Sequence

๐Ÿšฅโ€‹ Status

  • ๐ŸŸขโ€‹ SUCCESS: The task was completed successfully. In behavior tree visualizer, success nodes are painted in green.
  • ๐Ÿ”ด FAILURE: The task could not be completed or a condition was not met. In behavior tree visualizer, failed nodes are painted in red.
  • ๐ŸŸกโ€‹ RUNNING: The task is still in progress and needs more time to complete. In behavior tree visualizer, running nodes are painted in yellow.

โŒ› Execution Cycle

First, let remember the two important definitions:

  • Concurrency: When two or more tasks can start, run, and complete in overlapping time periods. It doesn't necessarily mean they will ever both be running at the same instant.
  • Parallelism is when tasks run at the same time in different threads, e.g., on a multi-core processor.

The execution way for running a tree is single-threaded with concurrency: nodes are executed in discrete time concurrently by their tick() callback. Tick is the fundamental execution mechanism in behavior trees:

  • Each tick represents one update cycle of the behavior tree. Ticks occur at a frequency determined by the application (e.g., 10Hz for a robot, 60Hz for a game).
  • A tick is a propagation of execution signals from parent to children. It flows down the tree from the root to the leaves following specific rules for each node type. When a parent node is ticked, it decides which children to tick based on its type.
  • When a node is ticked, it executes its logic and returns a status: SUCCESS, FAILURE, or RUNNING. Since the logic is not executed inside a thread, the execution should return as soon as possible to avoid blocking the entire flow of execution of the tree.

๐Ÿ’ฉ Workflow

The tick() function implements a template method pattern with hooks for onSetUp(), onRunning() and onTearDown().

  • A node calls onSetUp() on the first execution. This allows for initialization that can either succeed (SUCCESS) or fail (FAILURE).
  • A node calls onRunning() on subsequent ticks, returning SUCCESS, FAILURE, or RUNNING.
  • When a node transitions from RUNNING to SUCCESS/FAILURE, onTearDown() is called. This allows for cleanup operations.
  • Different node types (Composite, Decorator, Leaf) implement different strategies for propagating ticks, as explained in details in next sections of this document.

Here is the sequence diagram showing the execution flow:

sequenceDiagram
    participant App as Application
    participant Tree as Tree
    participant Node as Node
    participant Child as Child Node

    App->>Tree: tick()
    activate Tree

    alt Root is null or invalid
        Tree-->>App: FAILURE
    else Root exists and valid
        Tree->>Node: tick()
        activate Node

        alt Status != RUNNING (First tick)
            Node->>Node: onSetUp()
            Node-->>Node: status
        end

        alt Status != FAILURE
            Node->>Node: onRunning()
            activate Node

            alt Composite/Decorator
                Node->>Child: tick() children
                activate Child
                Child-->>Node: child status
                deactivate Child
            end

            Node-->>Node: status
            deactivate Node

            alt Status != RUNNING
                Node->>Node: onTearDown(status)
            end
        end

        Node-->>Tree: status
        deactivate Node
        Tree-->>App: status
    end
    deactivate Tree
Loading

๐Ÿ“ Simple Example

Let's consider a robot with the following minimal tree that allows it to cross a room.

Sequence of the behavior tree

The tree is composed of the following nodes:

  • Root node depicted by the black dot.
  • A single composite node named Sequence which is a composite node executing children in order until one fails. This node is given in all behavior tree libraries.
  • For our example, three project-specific leaves depicting three actions the robot can perform:
    • opening a door,
    • walking through the doorway,
    • and closing the door.

Thanks to the Sequence node, the three actions open the door, walk and close the door are executed in this order and, in our case, immediately return the status SUCCESS. While three actions have not all ended with SUCCESS the status of the Sequence node stays to RUNNING. If one of the actions fails (return FAILURE), the status of the sequence node becomes FAILURE and stop executing children nodes. If all actions end with SUCCESS, the status of sequence node becomes SUCCESS.

In a real situation, each ticked actions of the robot will not immediately return SUCCESS or FAILURE but will last several seconds or minutes to be achieved. Each action shall not take too much time to not block the tree tick() to make reactive the robot by allowing the execution of other nodes. For this reason it will return RUNNING.

๐Ÿ“š More Documentation on Behavior Trees

You can read the following documents:


๐Ÿ“˜ YAML Files, SubTrees & Blackboards

Before describing each behavior nodes, let first explain the structure used in YAML files to store a behavior tree. Every YAML file shall contain the BehaviorTree: section and can contain tow optional sections Blackboard: and SubTrees:. For example:

Blackboard:   # Initial scoped keys available to all nodes
  counter: 42
  position:
    x: 1.0
    y: 2.0
    z: 0.5

BehaviorTree: # Root tree definition
  Sequence:
    children: [...]

SubTrees:     # Reusable subtrees with isolated scopes
  PatrolLoop:
    Sequence:
      children: [...]

Blackboard: Section Keys defined under Blackboard populate the root blackboard. Complex structures are fully supported: nested maps, arrays, booleans, strings, and numbers. All nodes in the tree can access these values via blackboard->get<T>("key").

Variable References with ${key} The ${key} syntax allows you to reference any blackboard value by name, copying it into another field. This works with primitives, maps, and entire nested structures:

Blackboard:
  primary_enemy:
    name: "Drone-A"
    health: 35
  selected_target: ${primary_enemy}  # Copies the entire enemy map

SubTree Scope Isolation When a SubTree node is instantiated, the builder creates a child blackboard (parent->createChild()) for that subtree. Parameters passed to the subtree are stored in this child scope, preventing data leakage into the parent. The child blackboard inherits all parent keys but can override them locally.

BehaviorTree:
  SubTree:
    name: ProcessTarget
    reference: EngageEnemy
    parameters:
      target: ${primary_enemy}  # Passed to child blackboard

SubTrees:
  EngageEnemy:
    Sequence:
      children:
        - Action:
            name: AimWeapon
            parameters:
              enemy: ${target}  # Reads from child scope

See doc/examples/GameState/GameState.yaml and doc/examples/Patrol/Patrol.yaml for realistic examples of global data, subtree composition, and parameter passing.


๐Ÿโ€‹ The Different Type of Leaves

๐ŸŸข Always Success

Simple leaf that always returns SUCCESS. Do not confuse with the decorator Succeeder.

Behavior:

  • Returns SUCCESS.

YAML Syntax:

- Success

๐Ÿ”ด Always Failure

Simple leaf that always returns FAILURE. Do not confuse this leaf with the decorator Failer.

Behavior:

  • Returns FAILURE.

YAML Syntax:

- Failure

โ“ Condition

Evaluates a condition and returns SUCCESS or FAILURE. Useful for:

  • Boolean checks.
  • Sensor readings.
  • State validation.

Behavior:

  • Returns SUCCESS if the condition is true.
  • Returns FAILURE if the condition is false.
  • Can never return RUNNING.

Example of YAML Syntax:

- Condition:
    name: "IsEnemyVisible"
    parameters:
      range: 100.0

The field parameters: is optional.

๐Ÿ’ฌ Action

Performs a short action or a chunk of a long action and returns a status.

Behavior:

  • Return SUCCESS if action completes successfully.
  • Return FAILURE if action fails.
  • Return RUNNING if action is still executing (need more time before succeeding ot failing).

Example of YAML Syntax:

- Action:
    name: "MoveToTarget"
    parameters:
      speed: 1.0
      target: [10, 20]

The field parameters: is optional.


๐ŸŽจ Decorator Nodes

YAML Syntax: Since these nodes have a single child: field which has a single child node. The syntax follows the pattern:

- <parent node>:
    child:
      - <child node>:

๐ŸŒ– Inverter

Inverts the result of its child node. Useful for:

  • Checking if something is NOT true.
  • Creating "unless" conditions.

Mnemonic: Negate, Not.

Behavior:

  • Return SUCCESS if child returns FAILURE.
  • Return FAILURE if child returns SUCCESS.
  • Return RUNNING if child returns RUNNING.

Example YAML Syntax:

- Inverter:
    child:
      - Condition:
          name: "IsEnemyVisible"

โœ… ForceSuccess

Similar to the leaf Success but only transforms child failures: It always returns SUCCESS regardless of child's status. It is useful for:

  • Implementing "try but don't fail" semantics.
  • Optional tasks that shouldn't cause the whole sequence to fail.
  • Debugging behavior trees.
  • Temporarily disabling failure conditions.
  • Testing tree structure.

Behavior:

  • If child returns SUCCESS or RUNNING, returns the same status.
  • If child returns FAILURE, returns SUCCESS instead.

Example YAML Syntax:

- Succeeder:
    child:
      - Action:
          name: "OptionalTask"

โŒ ForceFailure

The opposite of ForceSuccess. Similar to the leaf Failure but only transforms child success: It always returns SUCCESS regardless of child's status. It is useful for:

  • Testing negative conditions.
  • Preventing specific success cases.
  • Implementing "succeed only if not" semantics.

Behavior:

  • If child returns FAILURE or RUNNING, returns the same status.
  • If child returns SUCCESS, returns FAILURE instead.

Example YAML Syntax:

- ForceFailure:
    child:
      - Action:
          name: "UndesiredResult"

โ™ป๏ธ Repeater

Repeats its child node a specified number of times. It is useful for:

  • Retrying failed actions.
  • Implementing persistence behaviors.
  • Creating loops in behavior trees.

Behavior:

  • Repeats child until it succeeds or reaches the limit.
  • Can be configured for infinite repetition.
  • Returns SUCCESS when child succeeds or limit is reached.
  • Returns RUNNING while repeating.

Example YAML Syntax:

- Repeater:
    times: 3
    child:
      - Action:
          name: "TryOpenDoor"

โณ Timeout

Limits the execution time of its child. Useful for:

  • Preventing actions from running too long.
  • Time-critical operations.
  • Implementing fallback triggers.

Behavior:

  • Starts a timer when the child begins execution.
  • Returns FAILURE if the timeout is reached.
  • Otherwise, returns the child's status.

Example YAML Syntax:

- Timeout:
    milliseconds: 5000
    child:
      - Action:
          name: "LongRunningTask"

๐Ÿชต Composites Nodes

๐Ÿ”ข Sequence

A Sequence node executes children in order until one fails. It logics is the and operator: if any child returns FAILURE, the sequence stops and returns FAILURE. If all children return SUCCESS, the sequence returns SUCCESS.

Mnemonic: "And", "Do In Order".

Behavior:

  • Tick each child node in order.
  • Return SUCCESS if all children succeed.
  • Return FAILURE if any child fails.
  • Return RUNNING if any child is running.

Use Case YAML Syntax:

- Sequence:
    children:
      - Action:
          name: "OpenDoor"
      - Action:
          name: "Walk"
      - Action:
          name: "CloseDoor"

Used for tasks that must be completed in sequence, like:

  1. Open the door.
  2. Walk into the room.
  3. Close the door.

Beware:

If the action Walk fails, the door will remain open since the last action CloseDoor is skipped.

๐Ÿชœ Parallel Sequence

Executes all children simultaneously. It comes in two forms: Parallel and ParallelAll.

Behavior:

  • Parallel:

    • Ticks all children simultaneously.
    • Returns SUCCESS if at least M children succeed (M is configurable, default is 1).
    • Returns FAILURE if more than N-M children fail (N is the total number of children).
    • Returns RUNNING otherwise.
  • ParallelAll:

    • Ticks all children simultaneously.
    • Returns SUCCESS only if ALL children succeed.
    • Returns FAILURE if ANY child fails.
    • Returns RUNNING otherwise.

Use Case - Parallel:

- Parallel:
    threshold: 2  # Succeeds when at least 2 children succeed
    children:
      - Action:
          name: "MonitorBattery"
      - Action:
          name: "MonitorObstacles"
      - Action:
          name: "MonitorEnemies"

Perfect for scenarios where:

  1. You need any one of several conditions to trigger a behavior.
  2. You want redundant systems where only some need to succeed.
  3. You need to monitor multiple conditions with different priorities.

Use Case - ParallelAll:

- ParallelAll:
    children:
      - Action:
          name: "MoveForward"
      - Action:
          name: "ScanEnvironment"
      - Action:
          name: "MaintainBalance"

Perfect for multitasking where:

  1. All tasks must succeed simultaneously.
  2. Background processes need to run alongside main tasks.
  3. Multiple conditions must all be satisfied.

Key differences:

  • Parallel offers more flexibility with a configurable success threshold.
  • ParallelAll is more strict, requiring all children to succeed.
  • Both execute all children at each tick regardless of individual results.

โฏ๏ธ Sequence with Memory

Same than Sequence but memorizes the point where it stopped. Used in:

  • Tasks that continue over multiple ticks.
  • Preserving progress in a sequence.
  • Avoiding redundant execution of already completed steps.

Mnemonic: Continue In Order.

Behavior:

  • Similar to Sequence, but it remembers which child it was executing if a child returns RUNNING.
  • On the next tick, it will continue from the last running child instead of starting from the beginning.
  • Returns SUCCESS if all children succeed.
  • Returns FAILURE if any child fails.
  • Returns RUNNING if the current child is running.

Use Case:

- SequenceWithMemory:
    children:
      - Action:
          name: "PrepareTask"
      - Action:
          name: "LongRunningTask"
      - Action:
          name: "CompleteTask"

โฎ๏ธ ReactiveSequence

Same than Sequence but checks all previous children at each tick. Needed when:

  • Conditions that must remain true throughout an action.
  • Safety checks that need constant verification.
  • Tasks that should stop immediately if prerequisites are no longer met.

Mnemonic: Check All

Behavior:

  • Always starts from the first child.
  • Re-evaluates all previously successful children at each tick.
  • If a previously successful child now fails, the sequence fails.
  • Returns SUCCESS if all children succeed.
  • Returns FAILURE if any child fails.
  • Returns RUNNING if the current child is running.

Use Case:

- ReactiveSequence:
    children:
      - Condition:
          name: "IsPathClear"
      - Action:
          name: "MoveForward"

โ“ Selector (aka Fallback)

Also known under the name of Selector in other documentation or project. A Selector node tries its children in order until one succeeds. it acts like the or operator: if a child returns SUCCESS, the selector stops and returns SUCCESS. If all children return FAILURE, the selector returns FAILURE. This is useful for fallback strategies.

Mnemonic: "Or", "Try Until Success", "Try alternatives".

Behavior:

  • Ticks each child node in order.
  • Returns SUCCESS if any child succeeds.
  • Returns FAILURE if all children fail.
  • Returns RUNNING if any child is running.

Use Case:

- Fallback:
    children:
      - Action:
          name: "FindEnemy"
      - Action:
          name: "PatrolArea"
      - Action:
          name: "ReturnToBase"

Explanation:

  1. Try to find an enemy.
  2. If no enemy found, patrol the area.
  3. If patrol fails, return to base.

๐Ÿ”„ Fallback with Memory

Similar to Fallback because it "tries until success" but memorizes the point where it stopped. Perfect for:

  • Complex fallback scenarios that span multiple ticks.
  • Recovery strategies that need to continue from where they left off.
  • Avoiding restarting already attempted recovery steps.

Mnemonic: Remember And Try

Behavior:

  • Similar to Selector, but it remembers which child it was executing if a child returns RUNNING.
  • On the next tick, it will continue from the last running child instead of starting from the beginning.
  • Returns SUCCESS if any child succeeds.
  • Returns FAILURE if all children fail.
  • Returns RUNNING if the current child is running.

Use Case:

- FallbackWithMemory:
    children:
      - Action:
          name: "TryPrimaryMethod"
      - Action:
          name: "TryBackupMethod"
      - Action:
          name: "EmergencyProtocol"

๐Ÿ”‚ Reactive Fallback

Similar to Fallback because it checks for success and tries again at each tick.

Mnemonic: Check Until Success

Behavior:

  • Always starts from the first child.
  • Re-evaluates previously failed children at each tick.
  • If a previously failed child now succeeds, the selector succeeds.
  • Returns SUCCESS if any child succeeds.
  • Returns FAILURE if all children fail.
  • Returns RUNNING if the current child is running.

Use Case:

- ReactiveFallback:
    children:
      - Condition:
          name: "IsPathA"
      - Condition:
          name: "IsPathB"
      - Action:
          name: "FindAlternativePath"

Perfect for:

  1. Continuously checking if preferred options become available.
  2. Reactive planning that adapts to changing conditions.
  3. Implementing priority-based decisions that need constant reassessment.

๐Ÿงฉ Develop your C++ code

Each code snippet is intentionally short; follow the "Full example" links for complete programs under doc/examples. We show two exclusive construction ways:

  • the programmatic construction in the case you do not want to use YAML fil (not recommended).
  • and the YAML file (recommended way).

๐Ÿ’ฌ Action & Condition

Full example: doc/examples/Nodes/LeafExample.cpp

Leaf nodes are the endpoints of a behavior tree where actual work happens. The two main types are:

  • Action โ€“ performs work and returns a status (SUCCESS, FAILURE, RUNNING)
  • Condition โ€“ evaluates a predicate and returns SUCCESS or FAILURE
  • Success/Failure โ€“ helper nodes that always return a fixed status

Programmatic construction:

Here the syntax to create a condition, using a lambda.

auto battery_ok = bt::Node::create<bt::Condition>(
    bt::Condition::Function([bb = blackboard]()
    {
        return bb->get<int>("battery").value_or(0) > 20;
    }),
    blackboard);

Here the syntax to create an action using a class with an optional blackboard (mandatory for action using parameters).

auto report_enemy = bt::Node::create<ReportEnemy>(blackboard);

YAML construction:

# Action node
Action:
  name: ReportEnemy
  parameters:
    target: "Drone-A"

# Condition node
Condition:
  name: BatteryOK
  parameters:
    battery: ${battery}

# Always-success helper
Success:
  name: MissionComplete

Action with Blackboard Access:

class ReportEnemy final: public bt::Action
{
public:
    explicit ReportEnemy(bt::Blackboard::Ptr blackboard)
    {
        m_blackboard = std::move(blackboard);
    }

    bt::Status onRunning() override
    {
        auto target = m_blackboard->get<std::string>("target");
        if (!target)
        {
            std::cout << "No target assigned\n";
            return bt::Status::FAILURE;
        }
        std::cout << "Engaging " << *target << '\n';
        return bt::Status::SUCCESS;
    }
};

๐Ÿ”ข Sequence

A Sequence node executes its children left to right. If any child returns FAILURE, the sequence stops and returns FAILURE. If all children return SUCCESS, the sequence returns SUCCESS.

Full example: doc/examples/Nodes/SequenceExample.cpp

Programmatic construction:

auto sequence = bt::Node::create<bt::Sequence>();
sequence->addChild(bt::Node::create<OpenDoor>());
sequence->addChild(bt::Node::create<WalkThrough>());
sequence->addChild(bt::Node::create<CloseDoor>());

YAML construction:

Sequence:
  name: EnterRoom
  children:
    - Action: { name: "OpenDoor" }
    - Action: { name: "WalkThrough" }
    - Action: { name: "CloseDoor" }

In bot case, you have to create the C++ classes OpenDoor, WalkThrough, CloseDoor.

class OpenDoor final: public bt::Action
{
public:
    bt::Status onRunning() override
    {
        std::cout << "Opening door\n";
        return bt::Status::SUCCESS;
    }
};

class WalkThrough final: public bt::Action
{
public:
    bt::Status onRunning() override
    {
        std::cout << "Walking through doorway\n";
        return bt::Status::SUCCESS;
    }
};

โ“ Selector / Fallback

A Selector node tries its children left to right until one succeeds. If a child returns SUCCESS, the selector stops and returns SUCCESS. If all children return FAILURE, the selector returns FAILURE. This is useful for fallback strategies.

Full example: doc/examples/Nodes/SelectorExample.cpp

Programmatic construction:

auto selector = bt::Node::create<bt::Selector>();
selector->addChild(bt::Node::create<ScanPrimary>());
selector->addChild(bt::Node::create<ScanFallback>());

YAML construction:

Selector:
  name: FindTarget
  children:
    - Action: { name: "ScanPrimary" }
    - Action: { name: "ScanFallback" }

In bot case, you have to create the C++ classes ScanPrimary, ScanFallback.

class ScanPrimary final: public bt::Action
{
public:
    bt::Status onRunning() override
    {
        std::cout << "Scanning primary sector ... not found\n";
        return bt::Status::FAILURE;  // Try next option
    }
};

class ScanFallback final: public bt::Action
{
public:
    bt::Status onRunning() override
    {
        std::cout << "Fallback scan succeeded\n";
        return bt::Status::SUCCESS;  // Selector succeeds
    }
};

๐Ÿชœ Parallel & ParallelAll

A Parallel node ticks all its children simultaneously and aggregates their results. You specify success and failure thresholds: if enough children succeed or fail to meet the threshold, the parallel node returns accordingly.

Full example: doc/examples/Nodes/ParallelExample.cpp

Programmatic construction:

auto parallel = bt::Node::create<bt::Parallel>(2, 2);  // successThreshold, failureThreshold
parallel->addChild(bt::Node::create<MonitorBattery>());
parallel->addChild(bt::Node::create<MonitorObstacles>());
parallel->addChild(bt::Node::create<MonitorComms>());

YAML construction:

Parallel:
  success_threshold: 2
  failure_threshold: 2
  children:
    - Action: { name: "MonitorBattery" }
    - Action: { name: "MonitorObstacles" }
    - Action: { name: "MonitorComms" }

In bot cases, you have to create classes MonitorBattery, MonitorObstacles and MonitorComms.

class MonitorBattery final: public bt::Action
{
public:
    bt::Status onRunning() override
    {
        std::cout << "Battery OK\n";
        return bt::Status::SUCCESS;
    }
};

class MonitorObstacles final: public bt::Action
{
public:
    bt::Status onRunning() override
    {
        std::cout << "Obstacles detected -> FAILURE\n";
        return bt::Status::FAILURE;
    }
};

๐ŸŽจ Decorators

Decorators wrap a single child node and modify its behavior. Common decorators include Inverter (flip success/failure), Retry (retry on failure), and ForceSuccess (always return success).

Full example: doc/examples/Nodes/DecoratorExample.cpp

Programmatic construction:

auto retry = bt::Node::create<bt::Retry>(3);  // Max 3 attempts
retry->setChild(bt::Node::create<FlakyConnect>());

YAML construction:

# Retry decorator
Retry:
  attempts: 3
  child:
    - Action: { name: "Connect" }

# Inverter decorator
Inverter:
  child:
    - Condition: { name: "IsEnemyNear" }

# ForceSuccess decorator
ForceSuccess:
  child:
    - Action: { name: "OptionalCleanup" }

Here the action class to create:

class FlakyConnect final: public bt::Action
{
public:
    bt::Status onRunning() override
    {
        ++m_attempts;
        if (m_attempts < 3)
        {
            std::cout << "Connect attempt " << m_attempts << " failed\n";
            return bt::Status::FAILURE;
        }
        std::cout << "Connect attempt " << m_attempts << " succeeded\n";
        return bt::Status::SUCCESS;
    }

private:
    int m_attempts = 0;
};

๐ŸŒณ SubTree Node

A SubTree node allows you to define reusable tree fragments in the SubTrees section of your YAML file. When instantiated, the builder creates a child blackboard for parameter isolation. This enables modular, composable behavior trees.

Full examples:

Simple SubTree Example:

Blackboard:
  greetings:
    text: "Hello from parent scope"

BehaviorTree:
  Sequence:
    name: Root
    children:
      - SubTree:
          name: GreetAndReport
          reference: GreetingSubTree

SubTrees:
  GreetingSubTree:
    Sequence:
      children:
        - Action:
            name: PrintGreeting
            parameters:
              message: ${greetings}  # Reads from parent blackboard
        - Action:
            name: ReportCompletion

Complex SubTree with Structured Data:

Blackboard:
  enemy_contact:
    name: "Intruder-17"
    health: 50
    position: { x: 2.0, y: 1.0, z: 0.5 }

BehaviorTree:
  Sequence:
    children:
      - SubTree:
          name: EngageEnemy
          reference: EngageEnemy

SubTrees:
  EngageEnemy:
    Selector:
      children:
        - Action:
            name: AttemptNonLethal
            parameters:
              contact: ${enemy_contact}  # Entire struct passed
        - Action:
            name: NeutralizeThreat
            parameters:
              target: ${enemy_contact}

Each SubTree is instantiated with an isolated child blackboard. Parameters from the parent scope are passed explicitly, preventing unintended data leakage between subtrees.


๐Ÿง  Blackboard Tips

The blackboard is a shared data store accessible by all nodes in the tree. It supports primitives (int, bool, string, double) and complex nested structures (maps, arrays).

Best Practices

Use descriptive keys:

blackboard->set<int>("battery", 75);
blackboard->set<std::string>("target", "Drone-A");

Access values with type safety:

auto battery = blackboard->get<int>("battery");
if (battery && *battery > 20) {
    // Proceed
}

Store complex structures as maps (YAML):

Blackboard:
  game_state:
    score: 1280
    time: 42.5
    enemies:
      - name: "Drone-A"
        health: 35
        position: { x: 0.0, y: 5.0, z: 1.5 }
      - name: "Drone-B"
        health: 20
        position: { x: -3.5, y: 2.25, z: 0.0 }

Access nested maps in C++:

auto snapshot = blackboard->get<bt::AnyMap>("game_state");
if (snapshot) {
    // Process nested structure
    auto enemies = (*snapshot)["enemies"];
}

Use ${key} to reference existing data:

Blackboard:
  primary_enemy:
    name: "Drone-A"
    health: 35
  selected_target: ${primary_enemy}  # Copy entire structure

Pass parameters to subtrees explicitly:

BehaviorTree:
  SubTree:
    name: ProcessEnemy
    reference: EngageEnemy
    parameters:
      target: ${primary_enemy}  # Passed to child blackboard

Remember: The builder automatically calls setBlackboard() for nodes created from YAML, so your custom constructors can accept the blackboard as a parameter and store it directly.

See doc/examples/GameState/GameState.yaml for a complete example of structured blackboard data with nested maps, arrays, and parameter passing.


๐Ÿ–ฅ๏ธ Visualizer & Debugger

./build/BehaviorTreeVisualizer --port 9090

Inside your application:

#include "BehaviorTree/Visualizer.hpp"
bt::Visualizer viz(*tree, "127.0.0.1", 9090);
while (!viz.isConnected()) { std::this_thread::sleep_for(1s); }
while (running) {
    viz.updateDebugInfo();
    tree->tick();
}

The UI colors nodes (green = success, red = failure, orange = running) and shows ports/blackboard values.

About

[v 0.2.0, still experimental] A lightweight behavior trees C++ library with its real-time visualizer application

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published