Skip to content

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.
  • CheckTransitions runs before Executor.Update in 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:
    1. The executor's ClearPlan() calls OnStop on the active strategy/post-process
    2. The planner's ClearCache() discards any pending results
    3. A new plan is requested for the target state

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() calls OnStop on 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

final = base + ResponseCurve( normalize(input) ) × Weight

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() via GetModifiedPriority()

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
  • AsyncCalculatePlan() returns PlanStatus.Running until the job completes (may span multiple frames)
  • ContentRevision gate — if the blackboard hasn't changed since the last plan, returns Unchanged without 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 iterationsBrain.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. Only OnStop() guarantees cleanup.
  • HandlePlanCompleted uses 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