Godot Behavior Trees

Check out my Godot Project Ant Battle using the Behavoir Trees in this article.
Check out the project on GitHub or play it on itch.io!
Behavior trees have become an essential tool in game development for creating sophisticated AI behaviors. Unlike simple state machines, behavior trees offer a hierarchical, modular approach to AI decision-making that scales well with complexity. In games where NPCs need to make contextual decisions, respond to changing environments, or execute multi-step tasks, behavior trees provide a structured framework that's both powerful and maintainable. They excel at breaking down complex behaviors into smaller, reusable components that can be composed in different ways. This modularity makes behavior trees particularly valuable as your game grows - you can add new behaviors without rewriting existing ones, test individual components in isolation, and create varied NPC personalities by reconfiguring the same set of basic actions. For Godot developers looking to move beyond basic AI implementations, behavior trees offer a robust solution that balances sophistication with developer-friendly design.
Behavior Tree
A behavior tree is a hierarchical structure used to control AI decision-making and actions. At its core, it's a tree of nodes where:
- Leaf nodes represent concrete actions or condition checks
- Internal nodes control the flow and execution of their child nodes
- Each node returns one of three states: SUCCESS, FAILURE, or RUNNING
The tree is processed from top to bottom, with each node determining how its children are evaluated. Common types of nodes include:
- Sequence Nodes: Execute children in order, stopping if any fail
- Selector Nodes: Try children in order until one succeeds
- Parallel Nodes: Run multiple children simultaneously
- Decorator Nodes: Modify the behavior of a single child node
Behavior trees combine the simplicity of finite state machines with the flexibility of hierarchical structures, allowing AI to make complex decisions through composable, reusable behaviors.
Godot Behavior Tree Example
In this example we see a basic AI for an enemy that has has two actions we want to perform. Wandering and Following. If the enemny doesn't have a target we run the Wandering behavior. If the enemy does have a target we follow the target.
BTNode
The BTNode class serves as the base class for all behavior tree nodes. It defines the core functionality that all behavior tree nodes share:
- A Status enum defining the possible return states (RUNNING, SUCCESS, FAILURE)
- Reference to a shared blackboard for storing data
- Reference to the agent (e.g. NPC) being controlled
- Core lifecycle methods (enter, process, exit)
Each node in the behavior tree inherits from BTNode and implements its own logic by overriding these methods:
enter()
: Called when the node starts executing, used for initializationprocess(delta)
: Called each frame to execute the node's behavior, returns a Statusexit()
: Called when the node finishes executing, used for cleanup
The Status return values determine how the tree processes:
- SUCCESS: The node completed successfully
- FAILURE: The node failed to complete
- RUNNING: The node is still executing
This base class provides the foundation for building more specialized node types like sequences, selectors, and actions.
class_name BTNode extends Node
enum Status { RUNNING, SUCCESS, FAILURE }
var blackboard: BTBlackboard
var agent
func enter():
pass # Called when the node starts execution
func process(delta: float) -> Status:
return Status.FAILURE # Each node overrides this
func exit():
pass # Called when the node stops execution
Blackboard
The blackboard acts as a shared memory space that all nodes in the tree can access. It's essentially a key-value store that allows nodes to share data and communicate with each other.
Some common uses for the blackboard include:
- Storing the position of a target the AI is pursuing
- Tracking resources like health or ammunition
- Remembering the last known location of enemies
- Sharing state between different branches of the behavior tree
The blackboard implementation is intentionally simple - just a dictionary with getter and setter methods. This allows nodes to:
- Store data using
set_var(key, value)
- Retrieve data using
get_var(key, default_value)
class_name BTBlackboard extends Node
var data: Dictionary = {}
func set_var(key: String, value: Variant):
data[key] = value
#print("[Blackboard] Set ", key, " to ", value)
func get_var(key: String, default_value: Variant = null) -> Variant:
return data.get(key, default_value)
Behavior Tree Player
The BTPlayer is the main component that runs the behavior tree. It has three key responsibilities:
Tree Setup: During
_ready()
, it ensures there's a blackboard (creating one if needed) and recursively assigns the blackboard and agent references to all nodes in the tree.Tree Execution: In
_process()
, it runs the behavior tree by:- Calling
process()
on the root node - Checking if execution finished (SUCCESS/FAILURE)
- Restarting the tree if needed by calling
exit()
andenter()
- Calling
Resource Management: It manages the blackboard and agent references that behavior nodes need to function.
The BTPlayer is typically attached to the AI agent (like an enemy) in the scene tree, with the behavior tree structure set up as its children.
class_name BTPlayer extends Node
@export var root_node: Node
@export var blackboard: BTBlackboard
@export var agent: Node
func _ready():
if blackboard == null:
blackboard = BTBlackboard.new()
add_child(blackboard)
if root_node:
_assign_to_tree(root_node) # Recursively assign blackboard & agent
root_node.enter()
func _process(delta: float) -> void:
if root_node:
var result = root_node.process(delta)
if result != BTNode.Status.RUNNING:
root_node.exit()
root_node.enter()
func _assign_to_tree(node: Node):
if node is BTNode:
node.blackboard = blackboard
node.agent = agent # Assign the agent to each behavior node
for child in node.get_children():
_assign_to_tree(child) # Recurse into the behavior tree
Composite Nodes
Composite nodes are behavior tree nodes that control the flow of execution through multiple child nodes. They act as containers and define how their child nodes are processed. The two most common composite nodes are:
Selector Node (OR Logic): Processes children in order until one succeeds. If any child succeeds, the selector succeeds. Only if all children fail does the selector fail. This creates "fallback" behavior - try one approach, if it fails try another, etc.
Sequence Node (AND Logic): Processes children in order until one fails. All children must succeed for the sequence to succeed. If any child fails, the sequence fails. This creates a chain of required steps that must all complete successfully.
Here's a visualization of how they work:
Selector Node
class_name BTSelectorNode extends BTNode
var current_child_index: int = 0
var running_child: BTNode = null # Track which child is currently running
func enter():
current_child_index = 0 # Reset selector when it starts
running_child = null
func process(delta: float) -> Status:
while current_child_index < get_child_count():
var child = get_child(current_child_index)
if child is BTNode: # Ensure it's a behavior node
if running_child != child: # Call enter() only if it's a new child
child.enter()
running_child = child
var result = child.process(delta)
if result == Status.SUCCESS:
child.exit()
running_child = null
return Status.SUCCESS # Selector succeeds if any child succeeds
elif result == Status.RUNNING:
return Status.RUNNING # Keep running the current child
else: # FAILURE
child.exit()
running_child = null
current_child_index += 1 # Try the next child
else:
# Skip non-BTNode children
current_child_index += 1
# Reset for next time this selector is run
current_child_index = 0
return Status.FAILURE # If all children fail, return FAILURE
func exit():
if running_child != null:
running_child.exit()
running_child = null
Sequence Node
class_name BTSequenceNode extends BTNode
var current_child_index: int = 0
var running_child: BTNode = null # Track which child is currently running
func enter():
current_child_index = 0 # Reset sequence when it starts
running_child = null
func process(delta: float) -> Status:
while current_child_index < get_child_count():
var child = get_child(current_child_index)
if child is BTNode: # Ensure it's a behavior node
if running_child != child: # Call enter() only if it's a new child
child.enter()
running_child = child
var result = child.process(delta)
if result == Status.FAILURE:
child.exit()
running_child = null
return Status.FAILURE # Sequence fails if any child fails
elif result == Status.RUNNING:
return Status.RUNNING # Keep running the current child
else: # SUCCESS
child.exit()
running_child = null
current_child_index += 1 # Move to the next child
else:
# Skip non-BTNode children
current_child_index += 1
# Reset for next time this sequence is run
current_child_index = 0
return Status.SUCCESS # If all children succeed, return SUCCESS
func exit():
if running_child != null:
running_child.exit()
running_child = null
Decorator Nodes
Decorator nodes are nodes that modify the behavior of a single child node. They act as wrappers that can change how their child node executes or what result it returns. Common decorator patterns include:
- Inverter: Flips SUCCESS to FAILURE and vice versa
- Repeater: Runs its child multiple times
- UntilSuccess: Keeps running its child until it succeeds
- UntilFailure: Keeps running its child until it fails
- TimeLimit: Fails if the child takes too long to complete
Decorators are powerful because they let you modify existing behaviors without changing their implementation. For example, you can take any action node and make it repeat by wrapping it in a Repeater decorator.
The key characteristics of decorator nodes are:
- They have exactly one child node
- They intercept and potentially modify the result from their child
- They can add pre/post processing around their child's execution
- They help keep the behavior tree DRY by making behaviors reusable in different contexts
Inverter Node
The Inverter node is one of the simplest but most useful decorator nodes. It inverts the result of its child node:
- If the child returns SUCCESS, the Inverter returns FAILURE
- If the child returns FAILURE, the Inverter returns SUCCESS
- If the child returns RUNNING, it passes through unchanged
This allows you to negate any condition or action. For example, if you have an "IsPlayerVisible" condition, wrapping it in an Inverter gives you "IsPlayerNotVisible" without writing any new code.
Some common use cases for Inverters include:
- Creating negative conditions (e.g. "not in range", "door is not open")
- Flipping the logic of action nodes (e.g. "move away from" instead of "move toward")
- Building more complex composite behaviors by combining with other nodes
class_name BTInverter extends BTNode
var child_node: BTNode = null
func _ready():
# Find the first child that is a BTNode
for child in get_children():
if child is BTNode:
child_node = child
break
func enter():
if child_node:
child_node.enter()
func process(delta: float) -> Status:
if not child_node:
return Status.FAILURE
var result = child_node.process(delta)
match result:
Status.SUCCESS:
return Status.FAILURE
Status.FAILURE:
return Status.SUCCESS
Status.RUNNING:
return Status.RUNNING
# Default fallback (should never reach here)
return Status.FAILURE
func exit():
if child_node:
child_node.exit()
Condition Nodes
Condition nodes are specialized behavior tree nodes that check if certain conditions are met. They act as the decision-making components of the behavior tree, returning either SUCCESS or FAILURE based on their evaluation. Unlike action nodes that perform behaviors, condition nodes only test states or predicates.
Condition nodes typically:
- Return SUCCESS if the condition is true
- Return FAILURE if the condition is false
- Never return RUNNING (they evaluate immediately)
- Don't modify any game state
- Are used to make decisions about which branches to execute
Common examples of condition nodes include:
- Checking if a target is in range
- Testing if health is below a threshold
- Verifying if a path exists to a destination
- Detecting if an item is available
Condition nodes are often used with composite nodes to create branching behavior.
Has Target
The HasTarget condition node checks if there is a valid target stored in the blackboard. It:
- Takes a target_var export parameter specifying which blackboard variable to check
- Returns SUCCESS if a target exists in the blackboard
- Returns FAILURE if no target is found
This node is commonly used to gate behaviors that require a target, like chasing or attacking.
class_name BTHasTarget extends BTNode
@export var target_var := &"target"
func process(_delta: float) -> Status:
var target: CharacterBody2D = blackboard.get_var(target_var, null)
if target == null:
return Status.FAILURE
return Status.SUCCESS
Action Nodes
Action nodes are the "leaf" nodes of a behavior tree that actually perform behaviors and modify game state. Unlike condition nodes that only check conditions, action nodes carry out the concrete actions that make up an AI's behavior. They can:
- Move characters
- Play animations
- Attack targets
- Modify game variables
- Interact with the environment
Action nodes typically return:
- SUCCESS when they complete their action
- FAILURE if they cannot perform their action
- RUNNING while they are still executing
Some common examples of action nodes include:
- Moving to a position
- Attacking a target
- Playing an animation
- Picking up an item
- Patrolling between waypoints
Action nodes form the building blocks of behavior trees - they are combined with composite and decorator nodes to create complex AI behaviors. Below are some example action nodes that demonstrate common AI behaviors.
Choose Random Screen Position
class_name BTChooseRandomScreenPos extends BTNode
@export var position_var: StringName = &"pos"
func process(_delta: float) -> Status:
var viewport_size = agent.get_viewport().get_visible_rect().size
var pos = Vector2(
randf_range(0, viewport_size.x),
randf_range(0, viewport_size.y)
)
blackboard.set_var(position_var, pos)
return Status.SUCCESS
Wait For Seconds
class_name BTWaitForSeconds extends BTNode
@export var wait_time: float = 1.0 # Time to wait before succeeding
var elapsed_time: float = 0.0
func enter():
elapsed_time = 0.0 # Reset the timer
#print(agent.name, "started waiting for", wait_time, "seconds")
func process(delta: float) -> Status:
elapsed_time += delta
if elapsed_time >= wait_time:
return Status.SUCCESS
return Status.RUNNING # Keep running until time is up
func exit():
#print(agent.name, "finished waiting")
pass
Move To Position
class_name BTMoveToPosition extends BTNode
@export var pos_var := &"pos"
@export var target_var := &"target"
@export var tolerance = 10
func process(delta: float) -> Status:
var target: CharacterBody2D = blackboard.get_var(target_var, null)
if target != null:
return Status.FAILURE
var pos: Vector2 = blackboard.get_var(pos_var, Vector2.ZERO)
var direction = (pos - agent.global_position).normalized()
if agent.move_to(direction, delta):
return Status.SUCCESS
if agent.global_position.distance_to(pos) < tolerance:
return Status.SUCCESS
return Status.RUNNING
Play Animation
class_name BTPlayAnimation extends BTNode
@export var animation_name: String = ""
func enter():
if agent.animation_player and animation_name != "":
agent.animation_player.play(animation_name)
func process(_delta: float) -> Status:
if not agent.animation_player or animation_name == "":
return Status.FAILURE
return Status.SUCCESS
func exit():
# Nothing to clean up
pass
Follow Target
class_name BTFollowTarget extends BTNode
@export var target_var := &"target"
func process(delta: float) -> Status:
var target: CharacterBody2D = blackboard.get_var(target_var, null)
if target == null:
return Status.FAILURE
var direction = (target.global_position - agent.global_position).normalized()
if agent.move_to(direction, delta):
return Status.SUCCESS
return Status.RUNNING
Conclusion
Behavior trees provide a powerful and flexible way to create sophisticated AI behaviors in Godot. By breaking down complex behaviors into smaller, reusable components and organizing them hierarchically, behavior trees make it easier to:
- Create modular and maintainable AI logic
- Test and debug individual behaviors in isolation
- Compose complex behaviors from simple building blocks
- Share data between nodes through the blackboard
- Handle state transitions and sequential actions gracefully
The implementation we've covered demonstrates the core concepts:
- A base BTNode class that all behavior nodes inherit from
- A blackboard for sharing data between nodes
- Composite nodes (Sequence and Selector) for flow control
- Action nodes for concrete behaviors like movement and animation
- A BTPlayer class to manage and execute the behavior tree
While this is a basic implementation, it provides a solid foundation that you can build upon. Some potential extensions include:
- Additional composite node types like Parallel nodes
- Decorator nodes to modify child node behavior
- Visual behavior tree editor tools
- More sophisticated action and condition nodes
- Performance optimizations for large behavior trees
Whether you're creating NPCs for an RPG, enemies for an action game, or AI companions, behavior trees offer a robust framework for implementing game AI that scales with your project's complexity.