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 Type | Role | Return Value |
|---|---|---|
| Sequence | Executes children in order; returns SUCCESS if all succeed | FAILURE if any child fails |
| Selector | Tries children in order; returns SUCCESS if any succeed | FAILURE if all children fail |
| Decorator | Modifies a child's result (invert, repeat, etc.) | Modified result |
| Leaf | Terminal 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 nodestick(): 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