State Machines
Learn how to use state machines to decide WHAT your AI should do.
Introduction
State machines and behavior trees serve different purposes in AI design. Understanding this distinction is crucial for building clean, maintainable AI systems.
The WHAT vs HOW Principle
| Layer | Pattern | Responsibility | Example |
|---|---|---|---|
| WHAT | State Machine | Decides which action to perform | "Should I attack, flee, or patrol?" |
| HOW | Behavior Tree | Implements the details of each action | "Move to target, wait for cooldown, deal damage" |
Key Insight
State machines excel at high-level decision making and mode switching. Behavior trees excel at executing sequences and handling reactive conditions. The best AI systems use both together.
Basic State Machine
In ClosureBT, state machines are created using YieldSimple. This node returns a different child node each tick based on a state variable.
private void Awake() => AI = YieldSimple("Simple AI", () =>
{
// State variable: 0 = Idle, 1 = Patrol, 2 = Attack
var state = Variable(() => 0);
// Cache nodes to avoid recreating them
Node idleNode = null;
Node patrolNode = null;
Node attackNode = null;
// Return different node based on state
return () => state.Value switch
{
0 => idleNode ??= JustRunning("Idle", () =>
{
OnTick(() =>
{
// Check for enemies every tick
if (CanSeeEnemy())
state.Value = 2; // Switch to attack state
// Check if time to patrol
if (ShouldPatrol())
state.Value = 1; // Switch to patrol state
});
}),
1 => patrolNode ??= Sequence("Patrol", () =>
{
// Patrol behavior
MoveTo(patrolPoints[currentPatrolPoint]);
Wait(2f);
// Return to idle when done
OnExit(() =>
{
currentPatrolPoint = (currentPatrolPoint + 1) % patrolPoints.Length;
state.Value = 0;
});
}),
2 => attackNode ??= Reactive * Sequence("Attack", () =>
{
// Attack behavior
Condition(() => EnemyVisible());
ChaseEnemy();
Attack();
// Return to idle when done
OnExit(() => state.Value = 0);
}),
_ => null
});
}); private void Awake() => AI = YieldSimple("Simple AI", () =>
{
// State variable: 0 = Idle, 1 = Patrol, 2 = Attack
var state = Variable(() => 0);
// Cache nodes to avoid recreating them
Node idleNode = null;
Node patrolNode = null;
Node attackNode = null;
// Return different node based on state
return () => state.Value switch
{
0 => idleNode ??= JustRunning("Idle", () =>
{
OnTick(() =>
{
// Check for enemies every tick
if (CanSeeEnemy())
state.Value = 2; // Switch to attack state
// Check if time to patrol
if (ShouldPatrol())
state.Value = 1; // Switch to patrol state
});
}),
1 => patrolNode ??= Sequence("Patrol", () =>
{
// Patrol behavior
MoveTo(patrolPoints[currentPatrolPoint]);
Wait(2f);
// Return to idle when done
OnExit(() =>
{
currentPatrolPoint = (currentPatrolPoint + 1) % patrolPoints.Length;
state.Value = 0;
});
}),
2 => attackNode ??= Reactive * Sequence("Attack", () =>
{
// Attack behavior
Condition(() => EnemyVisible());
ChaseEnemy();
Attack();
// Return to idle when done
OnExit(() => state.Value = 0);
}),
_ => null
});
}); Breaking it down:
var state = Variable(() => 0)- Creates a state variable initialized to 0 (Idle)return () => state.Value switch- Returns a function that switches based on current state??=- Null-coalescing assignment ensures each node is created only onceOnTick(() => state.Value = X)- Transition based on continuous condition checksOnExit(() => state.Value = X)- Transition when behavior completes
State Transitions
State transitions happen when you change the state variable's value. The most common way to do this is through lifecycle callbacks.
Transition Patterns
1. Condition-Based Transitions (OnTick)
Use OnTick when you need to check conditions every frame:
0 => idleNode ??= JustRunning("Idle", () =>
{
OnTick(() =>
{
// Check continuously for transition conditions
if (EnemyVisible()) state.Value = 2; // Attack
if (ShouldPatrol()) state.Value = 1; // Patrol
if (Health < 20) state.Value = 3; // Flee
});
}); 0 => idleNode ??= JustRunning("Idle", () =>
{
OnTick(() =>
{
// Check continuously for transition conditions
if (EnemyVisible()) state.Value = 2; // Attack
if (ShouldPatrol()) state.Value = 1; // Patrol
if (Health < 20) state.Value = 3; // Flee
});
}); 2. Completion-Based Transitions (OnExit)
Use OnExit when you want to transition after a behavior finishes:
1 => patrolNode ??= Sequence("Patrol", () =>
{
MoveTo(patrolPoint);
Wait(2f);
// Automatically return to idle when patrol completes
OnExit(() => state.Value = 0);
}); 1 => patrolNode ??= Sequence("Patrol", () =>
{
MoveTo(patrolPoint);
Wait(2f);
// Automatically return to idle when patrol completes
OnExit(() => state.Value = 0);
}); 3. Outcome-Based Transitions (OnSuccess/OnFailure)
Use OnSuccess or OnFailure for outcome-specific transitions:
2 => attackNode ??= Sequence("Attack", () =>
{
ChaseEnemy();
Attack();
// Different transitions based on outcome
OnSuccess(() => state.Value = 0); // Victory -> Idle
OnFailure(() => state.Value = 3); // Failed -> Flee
}); 2 => attackNode ??= Sequence("Attack", () =>
{
ChaseEnemy();
Attack();
// Different transitions based on outcome
OnSuccess(() => state.Value = 0); // Victory -> Idle
OnFailure(() => state.Value = 3); // Failed -> Flee
}); Important
State variables are local to the YieldSimple node. If you have nested state machines (hierarchical), each level has its own state variables that don't interfere with each other.
Summary
State machines provide the high-level decision making for your AI. They answer "What should I be doing right now?" by switching between mutually exclusive modes.
State Machines (WHAT):
- Decide between modes (Idle, Combat, Patrol, Flee)
- Handle state transitions via lifecycle callbacks
- Implemented with
YieldSimple - Can be hierarchical (nested state machines)
Behavior Trees (HOW):
- Execute steps within a state
- Handle reactive conditions
- Manage sequences and parallel tasks
- Implemented with Sequence, Selector, etc.
The best practice is to use YieldSimple at the top level with behavior trees as states. This gives you clear separation of concerns and easy-to-understand AI logic.
Next: Reactive Pattern
Build reactive behavior trees that respond to changes.
Read about Reactive Pattern →