Custom AI Systems
Build sophisticated AI with behavior trees, utility AI, and GOAP.
Introduction
Professional games like Halo, FEAR, and The Sims use advanced AI architectures beyond simple state machines. This guide explains behavior trees, utility AI, and Goal-Oriented Action Planning (GOAP) — then builds a custom AI system in Appley.
Advanced Guide 9 of 25
Screenshot
How Professional Games Do It
C
h
S
t
I
m
A
A
d
S
c
U
s
D
e
A
d
A
I
I
m
E
n
FluxScript Implementation
Here is how to build this system in Appley using FluxScript (Appley Scripting):
// Behavior Tree implementation for advanced enemy AI
// BT node types: Selector, Sequence, Action, Condition
func createSelector(children) {
return {
type: "selector",
run: (ctx) => {
for i = 0 to children.length {
result = children[i].run(ctx)
if result != "failure" return result // success or running
}
return "failure"
}
}
}
func createSequence(children) {
return {
type: "sequence",
run: (ctx) => {
for i = 0 to children.length {
result = children[i].run(ctx)
if result != "success" return result // failure or running
}
return "success"
}
}
}
func createCondition(check) {
return { type: "condition", run: (ctx) => check(ctx) ? "success" : "failure" }
}
func createAction(execute) {
return { type: "action", run: (ctx) => execute(ctx) }
}
// Build the enemy behavior tree
on start {
enemyBT = createSelector([
// Priority 1: Survive
createSequence([
createCondition((ctx) => ctx.health < 30),
createSelector([
createSequence([
createCondition((ctx) => ctx.hasMedkit),
createAction((ctx) => useMedkit(ctx))
]),
createSequence([
createCondition((ctx) => ctx.coverAvailable),
createAction((ctx) => moveToCover(ctx)),
createAction((ctx) => waitInCover(ctx, 3))
]),
createAction((ctx) => flee(ctx))
])
]),
// Priority 2: Attack
createSequence([
createCondition((ctx) => ctx.canSeePlayer && ctx.hasAmmo),
createSelector([
createSequence([
createCondition((ctx) => ctx.inAttackRange),
createAction((ctx) => attackPlayer(ctx))
]),
createSequence([
createAction((ctx) => moveToAttackPosition(ctx)),
createAction((ctx) => attackPlayer(ctx))
])
])
]),
// Priority 3: Investigate
createSequence([
createCondition((ctx) => ctx.lastKnownPlayerPos != null),
createAction((ctx) => investigate(ctx))
]),
// Priority 4: Patrol
createAction((ctx) => patrol(ctx))
])
}
on update(dt) {
// Build AI context (shared blackboard)
ctx = {
health: self.health,
hasMedkit: self.inventory.has("medkit"),
hasAmmo: self.currentWeapon.ammo > 0,
canSeePlayer: canSeePlayer(),
inAttackRange: math.distance(self.position, player.position) < 15,
coverAvailable: findCover() != null,
lastKnownPlayerPos: lastSeenPlayer,
coverPos: findCover(),
patrolTarget: getCurrentPatrolPoint()
}
// Tick the behavior tree
result = enemyBT.run(ctx)
// result is "success", "failure", or "running"
}
// Utility AI example (alternative to BT)
func utilityAI.decide(ctx) {
actions = [
{
name: "attack",
score: 0,
execute: (ctx) => attackPlayer(ctx)
},
{
name: "takeCover",
score: 0,
execute: (ctx) => moveToCover(ctx)
},
{
name: "reload",
score: 0,
execute: (ctx) => reload(ctx)
},
{
name: "flank",
score: 0,
execute: (ctx) => flankPlayer(ctx)
}
]
// Score each action based on context
for action in actions {
if action.name == "attack" {
action.score = 50
if ctx.canSeePlayer action.score += 30
if ctx.hasAmmo action.score += 20
if ctx.inAttackRange action.score += 30
}
if action.name == "takeCover" {
action.score = 10
if ctx.health < 50 action.score += 40
if ctx.coverAvailable action.score += 20
}
if action.name == "reload" {
action.score = 5
if !ctx.hasAmmo action.score += 60
if ctx.coverAvailable action.score += 20
}
if action.name == "flank" {
action.score = 15
if ctx.canSeePlayer action.score += 10
if ctx.squadMembers > 1 action.score += 25
}
}
// Execute highest-scored action
actions.sort((a, b) => b.score - a.score)
if actions[0].score > 0 {
actions[0].execute(ctx)
}
}Node Graph Equivalent
The same system implemented as a Node Graph in Appley Node Scripting:
[Behavior Tree Structure]
Selector (try until success)
+-- Sequence: Survive
| +-- Condition: health < 30
| +-- Selector:
| +-- Sequence: hasMedkit -> useMedkit
| +-- Sequence: coverAvailable -> moveToCover -> wait
| +-- Action: flee
+-- Sequence: Attack
| +-- Condition: canSeePlayer && hasAmmo
| +-- Selector:
| +-- Sequence: inRange -> attack
| +-- Sequence: moveToPosition -> attack
+-- Sequence: Investigate
| +-- Condition: lastKnownPos != null
| +-- Action: investigate
+-- Action: Patrol (fallback)
[Utility AI Scoring]
For Each Action:
--> [Calculate: base score]
--> [Add: context bonuses (health, ammo, distance, squad)]
--> [Sort: by score descending]
--> [Execute: highest scored action]Professional Optimization Tips
How AAA studios optimize these systems — applicable to your Appley project:
- Use behavior trees for complex, hierarchical AI. BTs are modular — you can swap subtrees between enemy types. A Grunt and an Elite share the same attack subtree but have different survival logic.
- Use utility AI for autonomous NPCs. The Sims scores hundreds of actions (eat, sleep, socialize) by current needs. Utility AI produces emergent, life-like behavior without scripted sequences.
- Use GOAP for tactical AI. FEAR enemies plan action sequences: reload -> find cover -> peek -> shoot. GOAP finds the optimal plan given preconditions and effects. It is expensive but produces intelligent behavior.
- Add a reaction delay to AI perception. Professional AI does not react instantly. Add 200-500ms from detection to action. This feels realistic and gives players a chance to react.
Common Mistakes
Single monolithic state machine
Complex AI in one FSM becomes unmaintainable. Break into behavior trees or utility systems. FSMs are fine for simple enemies but do not scale to bosses or squad tactics.
Instant reaction to player detection
AI that instantly turns and shoots feels unfair. Add a perception delay and a turn animation. Players need time to react to being detected.
