[Godot] Designing AI with Behavior Trees - GDScript Implementation and Enemy AI

Created: 2026-02-08Last updated: 2026-07-09

Flexible AI design with behavior trees (BT) in Godot. Covers how BTs differ from state machines, the four node types, implementing Sequence/Selector/Leaf, assembling an enemy AI tree, and the Blackboard pattern, with code and diagrams.

Write enemy AI as a pile of if statements and it gets unmanageable as conditions multiply. But a state machine alone tends to hit "state explosion," where transitions tangle as states grow. "Chase and attack when you spot the player, patrol when you don't, flee when health drops." A behavior tree (BT) is what lets you organize and build that kind of prioritized, complex behavior.

A BT expresses behavior as a hierarchical tree of tasks. In this article, we implement a small BT from scratch in GDScript and take it all the way to a working enemy AI.

Godot has no built-in BT feature. This article is structured around understanding the mechanism by building it yourself in GDScript. For production work, an addon like beehave gives you a visual editor and debug display, which pays off at scale. Grasp the mechanism first, then move to an addon when you need it.

Behavior tree hierarchy branching from top to bottom, from a root node through branches to leaf action nodes, representing an AI decision tree

What You'll Learn

  • How BTs differ from state machines (a tree of tasks vs. a web of states)
  • The four node types that make up a BT (Sequence / Selector / Decorator / Leaf)
  • How Sequence (AND) and Selector (OR) behave, and how to implement them
  • Assembling an enemy AI tree (spot the player and fight, otherwise patrol)
  • Sharing data between nodes loosely with a Blackboard

Sponsored

What a Behavior Tree Is (vs. a State Machine)

State machines and BTs both manage AI behavior, but they think about structure differently.

Comparison of the two approaches: a state machine as a web of states tangled by transition arrows, and a behavior tree as an organized tree from root to leaves
  • State machine (a web of states): connects states like "patrol," "chase," and "attack" with transitions. As states grow, the number of transitions explodes and gets tangled
  • Behavior tree (a tree of tasks): expresses behavior as a tree structure. Higher means higher priority, you extend it by adding branches, and subtrees are reusable

Prioritized decisions like "try attacking first, flee if that fails, otherwise patrol" map directly onto the order of branches in a BT. A state machine is plenty for simple AI, but once behaviors multiply and get complex, that's the BT's moment.

The Four Node Types

A BT is built by combining just four kinds of nodes.

Diagram of the four node types: Sequence (run all in order, AND), Selector (try until one succeeds, OR), Decorator (transform the result), and Leaf (the actual work)
NodeRoleResult
SequenceRuns children in order (AND)SUCCESS if all succeed / FAILURE if one fails
SelectorTries children in order (OR)SUCCESS if one succeeds / FAILURE if all fail
DecoratorTransforms a child's result (invert, repeat, etc.)The transformed result
LeafThe actual work (moving, attacking, etc.)SUCCESS / FAILURE / RUNNING

Every node is evaluated via tick() and returns one of SUCCESS, FAILURE, or RUNNING. RUNNING represents still in progress, like "still moving, not done yet," and it's a state unique to BTs.

Let's start with the base class everything builds on (using type registration with class_name).

# bt_node.gd
class_name BTNode
extends Node

enum Status { SUCCESS, FAILURE, RUNNING }

# Evaluation method that subclasses override
func tick(actor: Node, blackboard: Dictionary) -> Status:
    return Status.FAILURE
  • actor: the entity running this tree (the enemy character, for example)
  • blackboard: the data dictionary shared between nodes (covered below)
Sponsored

Implementing Sequence and Selector

Let's implement the two nodes at the heart of control flow. The important part is grasping how their behavior differs.

Diagram of the execution difference: Sequence is AND and aborts with failure as soon as one child fails, while Selector is OR and succeeds as soon as one child succeeds

Sequence (AND) runs children in order and aborts the moment one fails. Use it for a chain of actions you need to carry out all the way through, like "find the enemy, close in, attack."

# bt_sequence.gd
class_name BTSequence
extends BTNode

func tick(actor: Node, blackboard: Dictionary) -> Status:
    for child in get_children():
        var status: Status = child.tick(actor, blackboard)
        if status == Status.FAILURE:
            return Status.FAILURE   # Abort immediately if any child fails
        if status == Status.RUNNING:
            return Status.RUNNING   # Still running, so we stop here for this frame
    return Status.SUCCESS           # All succeeded

Selector (OR) tries children in order and commits to the first one that succeeds. Use it for prioritized choices like "heal, or flee, or fight."

# bt_selector.gd
class_name BTSelector
extends BTNode

func tick(actor: Node, blackboard: Dictionary) -> Status:
    for child in get_children():
        var status: Status = child.tick(actor, blackboard)
        if status == Status.SUCCESS:
            return Status.SUCCESS   # Commit to the first success and finish
        if status == Status.RUNNING:
            return Status.RUNNING
    return Status.FAILURE           # All failed

The two are exact mirror images. Sequence stops on failure, Selector stops on success. Once that clicks, the rest is just combining them.

Writing the Real Work in Leaf Nodes

If Sequence and Selector decide how to choose, a Leaf is what actually gets done. This is where concrete game logic goes: moving, attacking, waiting. Here's an example 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: Node2D = blackboard.get("target")
    if target == null:
        return Status.FAILURE              # No target means failure
    var dist := actor.global_position.distance_to(target.global_position)
    if dist <= arrival_distance:
        return Status.SUCCESS              # Arrived means success
    # Still far away, so keep moving
    var dir := actor.global_position.direction_to(target.global_position)
    actor.velocity = dir * move_speed
    actor.move_and_slide()
    return Status.RUNNING                  # Moving means still in progress

Return SUCCESS on arrival and RUNNING while still far, so the node continues next frame. Thanks to RUNNING, "move to the next task once movement finishes" falls out naturally. Hand the actual path movement to NavigationAgent2D and you get obstacle avoidance too. Keep leaves small and single-purpose so you can reuse them in other trees.

Sponsored

Hands-On: Building an Enemy AI Tree

Now that the parts are ready, let's combine them into an enemy AI: fight if the player is around, patrol if not. It's a standard shape that works for action games, RPGs, and stealth alike.

Example enemy AI behavior tree with a Selector root, a combat Sequence (HasTarget, MoveToTarget, Attack), and Patrol alongside it

Build the tree like this. Making the root a Selector naturally expresses the priority "try combat first, fall back to patrol."

Enemy (CharacterBody2D)
└─ BehaviorTree (BTSelector)          … try the top branch (combat) first
   ├─ CombatSequence (BTSequence)     … combat only holds if everything lines up
   │  ├─ HasTarget   (condition: is there a target?)
   │  ├─ MoveToTarget(close in)
   │  └─ Attack      (attack)
   └─ Patrol         (patrol)          … the fallback when combat isn't available

The character itself just writes "is the player nearby?" to the Blackboard every frame and ticks the root.

extends CharacterBody2D

@onready var bt_root: BTNode = $BehaviorTree
var blackboard := {}

func _ready() -> void:
    blackboard["patrol_points"] = [Vector2(100, 100), Vector2(300, 100)]

func _physics_process(_delta: float) -> void:
    # Reflect the player detection result into the Blackboard (detection can live in a separate system)
    var player := get_tree().get_first_node_in_group("player")
    var near := player and global_position.distance_to(player.global_position) < 200
    blackboard["target"] = player if near else null
    # Evaluate the tree (this one line drives every behavior)
    bt_root.tick(self, blackboard)

There are two key points.

  • Priority is decided by the order of branches: a Selector's children are tried top-down, so placing "combat" above "patrol" means combat wins as long as the player is around. To change behavior priority, you just reorder branches.
  • Keep decisions (the BT) separate from detection and movement: leave "can I see the player?" to a stealth vision system and "how do I close in?" to NavigationAgent2D, and let the BT focus on deciding how to behave. That keeps every piece small.

Sharing Data with a Blackboard

The blackboard in the example above is a standard BT pattern. It collects data shared between nodes into a single dictionary that each node reads from and writes to.

Loose coupling diagram showing multiple nodes exchanging read and write arrows with a central Blackboard holding target, health, and is_alerted
# Writing (updated by the detection system or the character itself)
blackboard["target"] = player
blackboard["health"] = 78
blackboard["is_alerted"] = true

# Reading (referenced inside each node's tick)
if blackboard.get("is_alerted"):
    # Go to alerted behavior

The essential part is that nodes never connect directly, they talk through the blackboard. Because of that, MoveToTarget works without knowing who wrote target. Centralizing data also makes debugging easier, and it stands alongside signals as a go-to loose-coupling technique.

Sponsored

Bonus: Limits and Next Steps

Everything so far is the basic form of a BT. Before shipping one, it helps to know the following.

  • Where RUNNING resumes: this implementation re-evaluates everything from the root every frame. That's fine for a small tree, but as it grows, add an optimization that remembers where RUNNING was returned and resumes from there.
  • Decorators add expressiveness: adding decorators like Inverter (flip the result), Repeater (repeat), and Timeout (time limit) gives you the flexibility to "try for N seconds" or "read a failure as a success."
  • Addons are an option: for larger projects, beehave is a strong choice. You can build trees visually, which keeps big trees readable. Understanding the mechanism by hand first makes the addon's behavior much easier to follow.
  • Use it alongside state management: BTs and state machines aren't rivals. A common setup combines them: big states in a state machine and the fine-grained behavior inside each state in a BT.

Summary

  • A BT is a design approach that expresses behavior as a tree of tasks, strong at prioritized, complex AI (Godot has no built-in feature, so build it or use an addon)
  • Sequence (AND) aborts on the first failure and Selector (OR) commits on the first success, a mirror-image pair
  • Write the real work in leaves and use RUNNING to mean "still in progress." Keeping them small and single-purpose makes them reusable
  • An enemy AI tree expresses "combat, then patrol" priority via a Selector root
  • A Blackboard shares data between nodes with loose coupling

Start with a small tree that puts a combat Sequence and a patrol Leaf under a Selector. Once you feel how behaviors accumulate just by adding branches, the strength of BTs becomes obvious.

Further Reading