The Planning Loop¶
This page explains what happens every frame inside a GOAP agent — the execution order, how the planner works, and how the executor runs actions.
Frame Execution Order¶
Every frame follows this sequence. Understanding it is essential for timing-dependent code.
┌───────────────────────────────────────────────────────────────┐
│ FRAME START │
│ │
│ 1. GoapRecollectionSystem.Update() [ExecutionOrder -10] │
│ └─ SyncToBlackboard(): writes ValidityKeys from memory │
│ │
│ 2. SensorController.Update() [ExecutionOrder 0] │
│ └─ Polls each sensor at its UpdateInterval (default 0.5s) │
│ Writes to blackboard via WriteBool/WriteVector3/etc. │
│ │
│ 3. GoapAgent.Update() [ExecutionOrder 0] │
│ ├─ CheckTransitions() — FSM belief-driven transitions │
│ ├─ CheckForBetterGoal() — goal preemption (every 0.5s) │
│ └─ Executor.Update(deltaTime) — runs active strategy │
│ │
│ 4. GoapRecollectionSystem.LateUpdate() │
│ └─ ScheduleJobs: processes stimulus queue via Burst jobs │
│ │
│ FRAME END │
└───────────────────────────────────────────────────────────────┘
Timing Implications¶
Warning
A sensor stimulus reported in step 2 is not reflected in blackboard ValidityKeys until step 1 of the next frame. Design your code to tolerate one-frame delays on temporal keys.
- A PostProcess fired in step 3 cannot rely on sensor state being current — sensors may have last fired 0–0.5 seconds ago.
CheckTransitionsruns beforeExecutor.Updatein the same tick. A PostProcess that modifies blackboard state is evaluated by CheckTransitions only on the next tick.
Two Independent Transition Systems¶
The framework has two separate systems that can change what an agent is doing. Understanding the difference is critical.
FSM Layer (GoapStateTransition)¶
This is the outer layer — it switches between behavioral states.
- Evaluated on a timer (
_transitionCheckInterval, default ~0.1s) - Each transition checks a belief condition against the blackboard
- If the condition matches
TargetValue, the agent transitions immediately - Can interrupt a running plan mid-action
- When a transition fires:
- The executor's
ClearPlan()callsOnStopon the active strategy/post-process - The planner's
ClearCache()discards any pending results - A new plan is requested for the target state
- The executor's
GOAP Layer (Goal Preemption)¶
This is the inner layer — it operates within a single behavioral state.
CheckForBetterGoal()runs on a timer (default 0.5s) during plan execution- Calls
EvaluateBestGoal()— lightweight, no Burst job, just iterates goals - If a different goal exceeds the current goal's priority by a configurable margin, calls
ForceReplan() ForceReplan()callsOnStopon the active strategy, clears the plan, and requests a new one
Goal Preemption¶
Within a behavioral state, higher-priority goals can interrupt the current plan mid-execution.
Configuration¶
| Setting | Default | Description |
|---|---|---|
Enable Goal Preemption |
true |
Toggle preemption on/off |
Goal Reevaluation Interval |
0.5s |
How often to check for a better goal |
Goal Preemption Margin |
0.1 |
Hysteresis to prevent thrashing between similar-priority goals |
Tip
The hysteresis margin prevents two goals with nearly equal priority from constantly interrupting each other. If Goal A has priority 5.0 and Goal B has priority 5.05, no preemption occurs (the difference is below the 0.1 margin).
Key Design Decisions¶
- The agent owns the replanning decision, not the planner (the planner is stateless/reactive).
- The check only runs when the executor has an active plan (not during async planning or idle).
EvaluateBestGoal()is O(goals) with a few float ops each — negligible cost at 0.5-second intervals.
Score Modifiers (Utility Curves)¶
Score modifiers dynamically adjust action costs and goal priorities based on blackboard values. This is how you create responsive, utility-driven AI without writing code.
Formula¶
Where normalize(input) maps the blackboard value into the 0–1 range using the configured InputRange.
Fields¶
| Field | Type | Description |
|---|---|---|
InputKeyId |
SerializableGuid | Blackboard key to read (Float or Int) |
InputRange |
Vector2 | Expected min/max of the input value |
ResponseCurve |
AnimationCurve | Maps normalized input (0–1) to a multiplier |
Weight |
float | Scalar applied to the curve output |
Enabled |
bool | Toggle the modifier on/off |
Example¶
A "Heal" goal reads the agent's health (0–100). The response curve rises steeply below 30%, making the goal's priority spike when health is low. At full health, the curve output is near zero and the goal is effectively ignored.
- Action costs are updated before each A* job via
UpdateActionCosts() - Goal priorities are evaluated in
SelectBestGoal()viaGetModifiedPriority()
The Planner¶
The planner finds action sequences using A* search over a 128-bit bitmask state space.
Key Properties¶
- Burst-compiled IJob — runs on a worker thread with zero managed allocations
- Async —
CalculatePlan()returnsPlanStatus.Runninguntil the job completes (may span multiple frames) - ContentRevision gate — if the blackboard hasn't changed since the last plan, returns
Unchangedwithout scheduling a job - Plan caching — if a new plan matches the current plan from the current step onward, returns
Unchanged(executor is not interrupted) - Max iterations —
Brain.MaxPlannerIterations(default 2000) caps A* search
PlanStatus¶
| Status | Meaning |
|---|---|
Success |
A valid plan was found |
Failed |
No plan could be found |
Running |
The Burst job is still executing |
Unchanged |
The plan hasn't changed (blackboard unchanged or same plan produced) |
Plan Failure Reasons¶
| Reason | Meaning |
|---|---|
NoValidGoal |
No goal has priority > 0 and passes IsValid() |
PathNotFound |
A* exhausted the search space without finding a path |
MaxIterationsReached |
Exceeded MaxPlannerIterations |
The Action Executor¶
Once the planner produces a plan, the executor runs each action in sequence.
STRATEGY PHASE
├─ Strategy.OnStart(context, blackboard, settings)
├─ Strategy.OnUpdate(context, blackboard, deltaTime, settings) [loops]
│ ├─ Returns Running → call again next frame
│ ├─ Returns Success → Strategy.OnStop() → enter POST-PROCESS PHASE
│ └─ Returns Failure → Strategy.OnStop() → fail plan
│
POST-PROCESS PHASE (only on strategy Success)
├─ For each post-process in chain (sequential):
│ ├─ PP.OnStart → PP.OnUpdate [loops] → PP.OnStop
│ ├─ Success → next post-process (or complete action)
│ └─ Failure → fail plan
│
ACTION COMPLETE
├─ Pop next action from stack, or...
└─ HandlePlanCompleted → fire stored transition (if set)
Key Rules¶
OnStop()is your "finally" block — called on success, failure, AND external interruption (ForceReplan, state transition, goal preemption). Cleanup that must always run goes here.- Post-processes are skipped on failure — if the strategy returns
Failure, no post-processes run. OnlyOnStop()guarantees cleanup. HandlePlanCompleteduses a stored transition — it does NOT re-evaluate the goal's DesiredState from the blackboard. The transition target was determined at plan creation time.
What's Next¶
- ScriptableObject Workflow — How the framework's SO-based authoring system works.
- GOAPAgent Component — Detailed reference for the agent component and its inspector settings.
- Action Strategies — How strategies and post-processes execute behavior.