Behaviour Trees
Build modular AI behavior trees with selectors, sequences, and decorators.
Introduction
Halo, Assassins Creed, and Splinter Cell use behavior trees for complex AI. Unlike state machines, BTs are modular, reusable, and handle concurrent behaviors gracefully. This guide builds a full BT framework in Appley.
Advanced Guide 10 of 25
Screenshot
How Professional Games Do It
U
n
S
e
D
e
O
r
I
m
E
a
U
s
A
A
d
D
e
V
i
P
r
FluxScript Implementation
Here is how to build this system in Appley using FluxScript (Appley Scripting):
// Full behavior tree framework with decorators
// Node states: "success", "failure", "running"
func selector(children) {
return {
run: (ctx) => {
for i = 0 to children.length {
result = children[i].run(ctx)
if result != "failure" return result
}
return "failure"
}
}
}
func sequence(children) {
return {
run: (ctx) => {
for i = 0 to children.length {
result = children[i].run(ctx)
if result != "success" return result
}
return "success"
}
}
}
// Decorators
func repeat(child, count) {
return {
run: (ctx) => {
for i = 0 to count {
result = child.run(ctx)
if result == "failure" return "failure"
}
return "success"
}
}
}
func invert(child) {
return {
run: (ctx) => {
result = child.run(ctx)
if result == "success" return "failure"
if result == "failure" return "success"
return result
}
}
}
func cooldown(child, seconds) {
return {
lastRun: 0,
run: (ctx) => {
if time.now() - self.lastRun < seconds return "failure"
result = child.run(ctx)
if result == "success" self.lastRun = time.now()
return result
}
}
}
func condition(check) {
return { run: (ctx) => check(ctx) ? "success" : "failure" }
}
func action(execute) {
return { run: (ctx) => execute(ctx) }
}
// Guard AI behavior tree
on start {
guardBT = selector([
// Alarm response (highest priority)
sequence([
condition((ctx) => ctx.alarmActive),
action((ctx) => moveToAlarmPoint(ctx)),
action((ctx) => searchArea(ctx))
]),
// Combat
sequence([
condition((ctx) => ctx.canSeeEnemy),
selector([
sequence([
condition((ctx) => ctx.inCoverRange),
cooldown(action((ctx) => shootAtEnemy(ctx)), 0.5)
]),
sequence([
action((ctx) => moveToCover(ctx)),
cooldown(action((ctx) => shootAtEnemy(ctx)), 0.5)
]),
action((ctx) => suppressEnemy(ctx))
])
]),
// Investigation
sequence([
condition((ctx) => ctx.heardNoise),
action((ctx) => investigateNoise(ctx)),
action((ctx) => searchArea(ctx))
]),
// Patrol (lowest priority)
repeat(sequence([
action((ctx) => moveToPatrolPoint(ctx)),
action((ctx) -> waitAtPoint(ctx, 5))
]), 999)
])
}
on update(dt) {
ctx = buildContext()
guardBT.run(ctx)
}Node Graph Equivalent
The same system implemented as a Node Graph in Appley Node Scripting:
[Guard Behavior Tree]
Selector (priority order)
+-- Sequence: Alarm Response
| +-- Condition: alarmActive
| +-- Action: moveToAlarmPoint
| +-- Action: searchArea
+-- Sequence: Combat
| +-- Condition: canSeeEnemy
| +-- Selector:
| +-- Sequence: inCover -> shoot (cooldown 0.5s)
| +-- Sequence: moveToCover -> shoot
| +-- Action: suppress
+-- Sequence: Investigation
| +-- Condition: heardNoise
| +-- Action: investigate -> search
+-- Repeat 999x: Patrol
+-- Sequence: moveToPoint -> wait 5sProfessional Optimization Tips
How AAA studios optimize these systems — applicable to your Appley project:
- Order BT children by priority. Selectors try children in order. Put the most important behavior first (survive) and the least important last (patrol). This creates natural priority-based AI.
- Use decorators for reuse. Instead of writing a "shoot with cooldown" action, wrap a basic "shoot" action in a Cooldown decorator. This keeps leaf nodes simple and composable.
- Use Running state for multi-tick actions. moveToCover takes multiple frames. Return Running until arrived, then Success. The BT resumes the running node next tick without re-evaluating parents.
- Share a blackboard (context) across the tree. Pass a context object to every node. Nodes read/write context data (health, enemy position). This decouples nodes and makes the tree reusable.
Common Mistakes
Re-evaluating the entire tree every tick
If a node returns Running, resume it next tick. Do not restart from the root every frame. This causes AI to restart actions constantly and never complete them.
No decorators — everything in leaf nodes
Decorators (cooldown, repeat, invert) keep leaf nodes clean. Without them, every action embeds cooldown/repeat logic, creating code duplication.
