【Godot】Implementing Behavior Trees for AI Design in GDScript

Created: 2026-02-08

How to implement flexible AI design using behavior trees (BT) in Godot. From node type explanations to a practical enemy AI example, learn how to build reusable AI structures.

Overview

Tested with: Godot 4.3+

When designing complex enemy AI or NPC behavior, chains of if statements or state machines alone can become hard to manage. Behavior Trees (BT) structure task priorities and conditional logic hierarchically, enabling reusable and extensible AI designs.

tips: Godot does not include a built-in behavior tree system. This article demonstrates a from-scratch GDScript implementation. For production use, consider the beehave addon, which provides a visual editor, debug overlay, and a rich set of node types.

This article explains how to implement a basic behavior tree in GDScript and apply it to enemy AI.

Basic Structure of Behavior Trees

Let's start by understanding the four node types that make up a behavior tree. With just these four building blocks, you can hierarchically express complex behaviors like "when you spot an enemy, move closer and attack; otherwise patrol."

Node TypeRoleReturn Value
SequenceExecutes children in order; returns SUCCESS if all succeedFAILURE if any child fails
SelectorTries children in order; returns SUCCESS if any succeedFAILURE if all children fail
DecoratorModifies a child's result (invert, repeat, etc.)Modified result
LeafTerminal node that performs actual logic (move, attack, etc.)SUCCESS/FAILURE/RUNNING

Each node is evaluated via a tick() method and returns one of SUCCESS, FAILURE, or RUNNING.

Implementing the Base Node Class

Now that you have an overview of the node types, let's start coding. First, define a base class that all nodes inherit from. We'll use class_name to register it as a global type so other node classes can extend it.

# bt_node.gd
class_name BTNode
extends Node

enum Status { SUCCESS, FAILURE, RUNNING }

# Override in subclasses
func tick(actor: Node, blackboard: Dictionary) -> Status:
    return Status.FAILURE

Key points:

  • actor: The entity running this tree (e.g., an enemy character)
  • blackboard: A shared data dictionary between nodes
  • tick(): The evaluation method called every frame

Implementing the Sequence Node

With the base class in place, let's move on to the control nodes. The Sequence node executes child nodes in order until all succeed. This corresponds to AND logic and expresses "complete all actions in sequence."

# bt_sequence.gd
class_name BTSequence
extends BTNode

func tick(actor: Node, blackboard: Dictionary) -> Status:
    for child in get_children():
        var status = child.tick(actor, blackboard)

        if status == Status.FAILURE:
            return Status.FAILURE  # Abort if any child fails
        elif status == Status.RUNNING:
            return Status.RUNNING  # Wait while running

    return Status.SUCCESS  # All children succeeded

Use case: A sequence of actions like "find enemy -> move closer -> attack."

Implementing the Selector Node

Tries child nodes in order and returns as soon as one succeeds (priority control). This corresponds to OR logic and expresses "choose the first successful action from multiple options."

# bt_selector.gd
class_name BTSelector
extends BTNode

func tick(actor: Node, blackboard: Dictionary) -> Status:
    for child in get_children():
        var status = child.tick(actor, blackboard)

        if status == Status.SUCCESS:
            return Status.SUCCESS  # Stop on first success
        elif status == Status.RUNNING:
            return Status.RUNNING  # Wait while running

    return Status.FAILURE  # All children failed

Use case: Prioritized action selection like "attack OR flee OR patrol." For example, in an action RPG you might express "use a healing item if health is low -> flee if no items -> fight as a last resort" using a Selector.

Implementing a Leaf Node

While Sequence and Selector handle how to decide, Leaf nodes handle what to actually do. They implement concrete game logic -- movement, attacks, waiting -- and sit at the terminal ends of the tree.

Here's an example Leaf node that moves toward a target.

# bt_move_to_target.gd
class_name BTMoveToTarget
extends BTNode

@export var move_speed: float = 100.0
@export var arrival_distance: float = 10.0

func tick(actor: Node, blackboard: Dictionary) -> Status:
    var target = blackboard.get("target")
    if not target:
        return Status.FAILURE  # No target found

    var distance = actor.global_position.distance_to(target.global_position)

    if distance <= arrival_distance:
        return Status.SUCCESS  # Arrived

    # Movement logic
    var direction = (target.global_position - actor.global_position).normalized()
    actor.velocity = direction * move_speed
    actor.move_and_slide()

    return Status.RUNNING  # Still moving

Practical Example: Enemy AI

At this point, all the building blocks are in place. Let's put them together to build an actual enemy AI. We'll implement a classic action-game enemy that enters combat when it spots the player and patrols otherwise.

# enemy.gd
extends CharacterBody2D

@onready var bt_root = $BehaviorTree

var blackboard = {}

func _ready():
    # Initialize the Blackboard
    blackboard["target"] = null
    blackboard["patrol_points"] = [Vector2(100, 100), Vector2(300, 100)]
    blackboard["current_patrol_index"] = 0

func _process(delta):
    # Simple player detection
    var player = get_tree().get_first_node_in_group("player")
    if player and global_position.distance_to(player.global_position) < 200:
        blackboard["target"] = player
    else:
        blackboard["target"] = null

    # Run the tree
    bt_root.tick(self, blackboard)

Scene tree structure example:

Enemy (CharacterBody2D)
+-- BehaviorTree (BTSelector)
   +-- CombatSequence (BTSequence)
   |  +-- HasTarget (BTCondition)
   |  +-- MoveToTarget (BTMoveToTarget)
   |  +-- Attack (BTAttack)
   +-- Patrol (BTPatrol)

Behavior: Engages in combat when a player is nearby; otherwise patrols.

The Blackboard Pattern

You may have noticed the blackboard dictionary in the practical example -- this is a staple pattern in behavior tree design. The Blackboard allows each node to read and write necessary data without directly depending on other nodes.

Here's how you store and access AI-relevant information through the Blackboard.

# Usage example
blackboard["target"] = player
blackboard["health"] = 100
blackboard["is_alerted"] = true

# Reading from a node
if blackboard.get("is_alerted"):
    # Alert behavior

Benefits:

  • Loose coupling between nodes
  • Centralized data management
  • Easy to debug

Limitations and Extensions

The implementation so far covers the essentials of a basic BT framework. Before integrating it into a real game, be aware of these important considerations.

  • RUNNING state resume: This implementation doesn't remember where RUNNING was returned, so the entire tree is re-evaluated from root each frame. For large trees, you'll need optimization that saves the RUNNING node's index and manages resumption position
  • Decorator nodes: Adding Inverter (result inversion), Repeater, Timer constraints, and other Decorators increases expressiveness
  • Addon usage: beehave provides visual editors, debug overlays, and rich node types, making it recommended for large-scale projects

Summary

  • Behavior trees design AI through hierarchical task control
  • Godot does not include a built-in behavior tree system -- you need to implement one from scratch or use an addon like beehave
  • Sequence executes all children in order (AND logic)
  • Selector finds the first successful child (OR logic)
  • Leaf nodes implement actual behavior (movement, attacks, etc.)
  • Blackboard streamlines data sharing between nodes
  • Keeping Leaf nodes small and single-purpose improves reusability
  • RUNNING state resume is an important optimization point -- storing the last RUNNING node index avoids re-evaluating the entire tree from root each frame

Further Reading