Skip to main content

Behaviour Tree System

The Behaviour Tree system in Don't Starve Together provides a hierarchical framework for implementing AI decision-making logic. This system enables complex AI behaviors through modular, reusable node structures that can be composed to create sophisticated state machines for entities like mobs, NPCs, and other game objects.

Version History

Build VersionChange DateChange TypeDescription
6760422025-06-21stableUpdated documentation to match current implementation and added missing node types

Overview

The behaviour tree system serves multiple purposes:

  • AI Decision Making: Provides structured approach to entity behavior logic
  • Modular Design: Enables reusable behavior components across different entities
  • Performance Optimization: Includes sleep/wake mechanisms for efficient execution
  • Debug Support: Offers comprehensive debugging and visualization tools

The system is built around a tree structure where each node represents a specific behavior or decision point, allowing for complex AI patterns through simple composition.

Core Architecture

Status Constants

The system defines four primary execution states:

SUCCESS = "SUCCESS"   -- Node completed successfully
FAILED = "FAILED" -- Node failed to complete
READY = "READY" -- Node is ready to execute
RUNNING = "RUNNING" -- Node is currently executing

Main BT Class

The root behavior tree class that manages execution:

BT = Class(function(self, inst, root)
self.inst = inst -- The entity this tree belongs to
self.root = root -- The root node of the tree
end)

Key Methods

MethodDescription
Update()Executes one tick of the behavior tree
ForceUpdate()Forces immediate update regardless of sleep state
Reset()Resets the entire tree to initial state
Stop()Stops execution and cleans up resources
GetSleepTime()Returns optimal sleep duration for performance
__tostring()Returns string representation of tree for debugging

Node Types

Base BehaviourNode

All behavior tree nodes inherit from the base BehaviourNode class:

BehaviourNode = Class(function(self, name, children)
self.name = name or ""
self.children = children
self.status = READY
self.lastresult = READY
self.nextupdatetick = 0
self.id = NODE_COUNT -- Unique identifier for debugging

-- Set parent references for all children
if children then
for i, k in pairs(children) do
k.parent = self
end
end
end)

Core Node Methods

MethodDescription
Visit()Execute the node's primary logic
Reset()Reset node and children to READY state
Step()Update child nodes after execution
Stop()Clean up resources and stop execution
Sleep(t)Put node to sleep for specified time
SaveStatus()Save current status as last result
GetSleepTime()Calculate how long this node should sleep
GetTreeSleepTime()Calculate sleep time for entire subtree
GetString()Get string representation for debugging
GetTreeString(indent)Get formatted tree string with indentation
DBString()Get debug string for node-specific information
DoToParents(fn)Execute function on all parent nodes

Composite Nodes

Composite nodes manage multiple child nodes with different execution strategies.

SequenceNode

Executes children in order, failing if any child fails:

SequenceNode = Class(BehaviourNode, function(self, children)
BehaviourNode._ctor(self, "Sequence", children)
self.idx = 1 -- Current child index
end)

Behavior: Continues to next child only if current child succeeds. Returns FAILED immediately if any child fails.

SelectorNode

Executes children in order, succeeding if any child succeeds:

SelectorNode = Class(BehaviourNode, function(self, children)
BehaviourNode._ctor(self, "Selector", children)
self.idx = 1 -- Current child index
end)

Behavior: Continues to next child only if current child fails. Returns SUCCESS immediately if any child succeeds.

PriorityNode

Re-evaluates child priority periodically, switching execution as needed:

PriorityNode = Class(BehaviourNode, function(self, children, period, noscatter)
BehaviourNode._ctor(self, "Priority", children)
self.period = period or 1 -- Re-evaluation interval
end)

Behavior: Higher-priority children can interrupt lower-priority running children.

ParallelNode

Executes all children simultaneously:

ParallelNode = Class(BehaviourNode, function(self, children, name)
BehaviourNode._ctor(self, name or "Parallel", children)
end)

Behavior: Succeeds when all children succeed, fails if any child fails.

ParallelNodeAny

Parallel node that succeeds when any child completes:

ParallelNodeAny = Class(ParallelNode, function(self, children)
ParallelNode._ctor(self, children, "Parallel(Any)")
self.stoponanycomplete = true
end)

Behavior: Executes all children simultaneously, succeeds when the first child succeeds or fails.

RandomNode

Randomly selects a child to execute:

RandomNode = Class(BehaviourNode, function(self, children)
BehaviourNode._ctor(self, "Random", children)
end)

Behavior: Picks random child on first execution, retries failed children randomly.

LoopNode

Repeats execution of children for a specified number of iterations:

LoopNode = Class(BehaviourNode, function(self, children, maxreps)
BehaviourNode._ctor(self, "Sequence", children)
self.idx = 1
self.maxreps = maxreps
self.rep = 0
end)

Behavior: Executes children sequentially, resetting and repeating until maxreps is reached or a child fails.

Leaf Nodes

Leaf nodes perform actual actions or evaluate conditions.

ActionNode

Executes a single action function:

ActionNode = Class(BehaviourNode, function(self, action, name)
BehaviourNode._ctor(self, name or "ActionNode")
self.action = action -- Function to execute
end)

ConditionNode

Evaluates a boolean condition:

ConditionNode = Class(BehaviourNode, function(self, fn, name)
BehaviourNode._ctor(self, name or "Condition")
self.fn = fn -- Function that returns boolean
end)

MultiConditionNode

Evaluates different conditions for start and continue logic:

MultiConditionNode = Class(BehaviourNode, function(self, start, continue, name)
BehaviourNode._ctor(self, name or "Condition")
self.start = start -- Initial condition function
self.continue = continue -- Continuing condition function
end)

Behavior: Uses start condition on first execution, then switches to continue condition for subsequent evaluations.

ConditionWaitNode

Waits until a condition becomes true:

ConditionWaitNode = Class(BehaviourNode, function(self, fn, name)
BehaviourNode._ctor(self, name or "Wait")
self.fn = fn -- Function that returns boolean
end)

Behavior: Returns RUNNING until condition becomes true, then returns SUCCESS.

WaitNode

Waits for a specified duration:

WaitNode = Class(BehaviourNode, function(self, time)
BehaviourNode._ctor(self, "Wait")
self.wait_time = time -- Duration to wait
end)

Decorator Nodes

Decorator nodes modify the behavior of a single child node.

NotDecorator

Inverts the result of its child:

NotDecorator = Class(DecoratorNode, function(self, child)
DecoratorNode._ctor(self, "Not", child)
end)

Behavior: SUCCESS becomes FAILED, FAILED becomes SUCCESS, RUNNING remains RUNNING.

FailIfRunningDecorator

Converts RUNNING status to FAILED:

FailIfRunningDecorator = Class(DecoratorNode, function(self, child)
DecoratorNode._ctor(self, "FailIfRunning", child)
end)

FailIfSuccessDecorator

Converts SUCCESS status to FAILED:

FailIfSuccessDecorator = Class(DecoratorNode, function(self, child)
DecoratorNode._ctor(self, "FailIfSuccess", child)
end)

Special Nodes

EventNode

Responds to specific entity events:

EventNode = Class(BehaviourNode, function(self, inst, event, child, priority)
BehaviourNode._ctor(self, "Event("..event..")", {child})
self.inst = inst
self.event = event
self.priority = priority or 0
end)

Behavior: Triggers when specified event occurs, executes child node in response.

LatchNode

Prevents re-execution for a specified duration:

LatchNode = Class(BehaviourNode, function(self, inst, latchduration, child)
BehaviourNode._ctor(self, "Latch ("..tostring(latchduration)..")", {child})
self.latchduration = latchduration
end)

Utility Functions

WhileNode

Creates a parallel node that continuously checks a condition:

function WhileNode(cond, name, node)
return ParallelNode{
ConditionNode(cond, name),
node
}
end

Usage: Executes node while cond remains true. Interrupts immediately if condition fails.

IfNode

Creates a sequence that executes only if a condition passes:

function IfNode(cond, name, node)
return SequenceNode{
ConditionNode(cond, name),
node
}
end

Usage: Executes node only after cond succeeds once. Condition not re-checked during execution.

IfThenDoWhileNode

Advanced conditional execution with different start and continue conditions:

function IfThenDoWhileNode(ifcond, whilecond, name, node)
return ParallelNode{
MultiConditionNode(ifcond, whilecond, name),
node
}
end

Implementation Examples

Basic Mob Behavior

-- Simple mob that wanders and flees from players
local brain = BT(inst,
PriorityNode({
-- High priority: flee from nearby players
IfNode(function() return FindClosestPlayer(5) ~= nil end,
"See Player",
ActionNode(function() RunAway() end, "Flee")),

-- Low priority: wander randomly
SequenceNode({
WaitNode(math.random(2, 5)),
ActionNode(function() WalkRandomly() end, "Wander")
})
})
)

Event-Driven Behavior

-- Mob that responds to being attacked
local brain = BT(inst,
PriorityNode({
-- React to being attacked
EventNode(inst, "attacked",
SequenceNode({
ActionNode(function() TurnToAttacker() end, "Face Attacker"),
ActionNode(function() CounterAttack() end, "Fight Back")
}),
10), -- High priority

-- Default behavior
ActionNode(function() Idle() end, "Idle")
})
)

Complex State Machine

-- Advanced mob with multiple states
local brain = BT(inst,
PriorityNode({
-- Emergency: low health
IfNode(function() return inst.components.health:GetPercent() < 0.3 end,
"Low Health",
ActionNode(function() FindHealing() end, "Seek Healing")),

-- Combat: Start fighting if enemy seen, continue while able to fight
IfThenDoWhileNode(
function() return FindNearbyEnemy() ~= nil end, -- Start condition
function() return CanContinueFighting() end, -- Continue condition
"Combat State",
SequenceNode({
ActionNode(function() MoveToEnemy() end, "Approach"),
ActionNode(function() Attack() end, "Attack"),
WaitNode(1.0) -- Attack cooldown
})
),

-- Maintenance: repair if damaged
IfNode(function() return NeedsRepair() end,
"Needs Repair",
ActionNode(function() DoRepair() end, "Repair")),

-- Default: patrol area
LoopNode({
ActionNode(function() MoveToNextPoint() end, "Move"),
WaitNode(2.0)
}, 5) -- Patrol 5 points then stop
})
)

Performance Optimization

Sleep System

The behavior tree system includes sophisticated sleep mechanisms for performance:

-- Nodes can sleep to reduce CPU usage
function SomeExpensiveCheck()
if expensive_condition() then
return SUCCESS
else
-- Sleep for 1 second before checking again
self:Sleep(1.0)
return RUNNING
end
end

Priority-Based Updates

Priority nodes intelligently schedule re-evaluation:

-- Only re-evaluate priorities every 2 seconds
PriorityNode(children, 2.0) -- Period = 2 seconds

Debugging Support

Tree Visualization

The system provides string representation for debugging:

-- Print current tree state
print(tostring(brain))

-- Output format shows node hierarchy with sleep times:
-- Priority>0.00
-- >Sequence - RUNNING <READY> ()
-- >Condition - SUCCESS <SUCCESS> ()
-- >Action - RUNNING <READY> (doing something)
-- >Wait - READY <READY> ()

Node Identification

Each node has a unique ID for debugging tools:

-- Access node information
print("Node ID:", node.id)
print("Node Status:", node.status)
print("Last Result:", node.lastresult)

Error Handling

Graceful Failure

Nodes should handle errors gracefully:

ActionNode(function()
if not inst:IsValid() then
return FAILED
end

local success = DoAction()
return success and SUCCESS or FAILED
end, "Safe Action")

Resource Cleanup

Always implement proper cleanup in Stop methods:

function CustomNode:Stop()
-- Clean up resources
if self.timer then
self.timer:Cancel()
self.timer = nil
end

-- Call parent cleanup
self._base.Stop(self)
end

Integration with Game Systems

Brain Component

Behavior trees integrate with the brain component:

-- Set up brain with behavior tree
local brain = require("brains/mybrain")
inst:AddComponent("locomotor")
inst:AddComponent("brain")
inst.components.brain:SetBrain(brain)

Event System

EventNodes automatically integrate with the game's event system:

-- This will listen for "gohome" events
EventNode(inst, "gohome",
ActionNode(function() inst:GoHome() end, "Return Home"))

Component Integration

Behavior trees commonly interact with entity components:

-- Example using various components
IfNode(function()
return inst.components.hunger:GetPercent() < 0.5
end, "Hungry",
ActionNode(function()
local food = inst.components.inventory:FindItem(function(item)
return item:HasTag("edible")
end)
if food then
inst.components.eater:Eat(food)
end
end, "Eat Food"))

Best Practices

Node Composition

  • Keep individual nodes simple and focused
  • Use composition over complex single nodes
  • Prefer shallow trees over deep nesting
  • Name nodes descriptively for debugging

Performance Considerations

  • Use appropriate sleep durations for expensive operations
  • Implement proper sleep scheduling in custom nodes
  • Avoid deep recursion in tree structures
  • Cache expensive calculations when possible

Maintainability

  • Document complex behavior trees thoroughly
  • Use meaningful names for conditions and actions
  • Group related behaviors into reusable subtrees
  • Implement proper error handling and cleanup
  • Brains - Pre-built behavior trees for specific entity types
  • Components - System components that behavior trees interact with
  • Actions - Action system that behavior trees can trigger
  • State Graphs - Animation state machines that work alongside behavior trees

Status: 🟢 Stable

The Behaviour Tree system is stable and actively used throughout the DST codebase for AI implementation. The API is considered mature and changes are rare.