Action Strategies¶
Strategies are the execution logic for GOAP actions. Each strategy is a ScriptableObject that defines how an agent performs a specific behavior — move, attack, animate, interact. Post-processes run after a strategy succeeds to handle cleanup, key resets, and follow-up work.
Strategy Base Class¶
public abstract class BaseGoapActionStrategy : ScriptableObject, IActionStrategy
{
public abstract Type GetSettingsType();
public virtual void OnStart(IGoapAgentContext context, GoapBlackboard blackboard,
BaseStrategySettings settings) { }
public virtual GoapActionStatus OnUpdate(IGoapAgentContext context, GoapBlackboard blackboard,
float deltaTime, BaseStrategySettings settings)
=> GoapActionStatus.Success;
public virtual void OnStop(IGoapAgentContext context, GoapBlackboard blackboard,
BaseStrategySettings settings) { }
}
GoapActionStatus¶
| Status | Meaning |
|---|---|
Running |
Still in progress — OnUpdate called again next frame |
Success |
Complete — proceed to post-processes, then next action |
Failure |
Failed — skip post-processes, fail the entire plan |
Strategy Settings¶
Each strategy has a companion settings class that holds its configuration:
[Serializable]
public class MyStrategySettings : BaseStrategySettings
{
[Tooltip("Blackboard key for the target position.")]
public SerializableGuid TargetKeyId;
[Tooltip("Movement speed.")]
public float Speed = 3.5f;
}
Settings are configured per-action in the GOAP Hub inspector. The same strategy SO can be used by multiple actions with different settings.
Access Patterns¶
Capabilities (O(1) cached)¶
var navAgent = context.GetCapability<NavMeshAgent>();
var animator = context.GetCapability<Animator>();
var custom = context.GetCapability<MyCustomComponent>();
Blackboard Read/Write¶
// Read (with fallback)
Vector3 pos = blackboard.GetVector3(keyId, Vector3.negativeInfinity);
bool flag = blackboard.GetBool(keyId, false);
float value = blackboard.GetFloat(keyId, 0f);
// Write (with source tag for debugging)
blackboard.SetBool(keyId, true, "MyStrategy");
blackboard.SetFloat(keyId, elapsed, "MyStrategy");
Agent Transform¶
Critical Rules for Strategies¶
Strategies Are Shared Singletons¶
Strategy SOs are shared across all agents using the same brain. Any field on the SO — including [NonSerialized] fields — is shared across every agent.
// BAD — shared across all agents
[NonSerialized] private bool _destinationSet;
// GOOD — per-agent via component
var manager = context.GetCapability<MyManager>();
manager.BeginSearch(center);
// GOOD — per-agent via blackboard
blackboard.SetFloat(s.TimerKeyId, Time.time, "MyStrategy");
Danger
Never store per-agent runtime state on a strategy SO. Use GetCapability<T>() for complex state, existing per-agent components (NavMeshAgent, Animator), or the blackboard for simple values.
OnStart Must Handle Re-Entry¶
Actions can be interrupted and re-entered. Guard against unconditional resets:
// BAD — unconditionally resets
manager.BeginSearch(center);
// GOOD — preserves progress
bool contextChanged = Vector3.Distance(center, manager.LastCenter) > manager.Radius;
if (manager.IsComplete || contextChanged)
manager.BeginSearch(center);
OnStop Is Your "Finally" Block¶
OnStop() is called in all cases: success, failure, external interruption, and goal preemption. Use it for cleanup:
- Release held resources
- Reset animation triggers
- Stop NavMeshAgent
- Clear temporary blackboard keys
Null-Check Capabilities¶
Every GetCapability<T>() call must handle null with an actionable warning:
var agent = context.GetCapability<NavMeshAgent>();
if (agent == null)
{
Debug.LogWarning(
$"[{GetType().Name}] No NavMeshAgent on '{context.gameObject.name}'. "
+ "Add a NavMeshAgent component to the agent's GameObject.");
return GoapActionStatus.Failure;
}
Built-In Strategies¶
NavMeshMoveStrategy¶
Moves the agent via NavMesh to a position stored on the blackboard.
| Setting | Type | Description |
|---|---|---|
TargetKeyId |
SerializableGuid | Blackboard key containing the target Vector3 position |
MoveSpeed |
float | Agent movement speed (overrides NavMeshAgent.speed) |
StoppingDistance |
float | Distance at which the agent considers itself "arrived" |
Returns Running while navigating, Success when within stopping distance, Failure if the path is invalid.
Using NavMeshMoveStrategy in a custom action
// Configure in the GOAP Hub Inspector:
// Strategy: NavMeshMoveStrategy
// TargetKeyId: → WaypointPosition slot
// MoveSpeed: 3.5
// StoppingDistance: 0.5
//
// The strategy handles everything:
// 1. Reads the target position from the blackboard
// 2. Sets NavMeshAgent.destination on OnStart
// 3. Checks remainingDistance each OnUpdate
// 4. Stops the NavMeshAgent on OnStop (success, failure, or interruption)
Tip
Set StoppingDistance on the strategy to match or slightly exceed the NavMeshAgent.stoppingDistance on the GameObject. A mismatch causes the agent to spin at the destination.
SendSignalStrategy\<T>¶
Sends a typed signal to the agent itself or to a target stored on the blackboard. Used for damage, healing, and inter-agent messaging.
| Setting | Type | Description |
|---|---|---|
MessageTarget |
enum | Self or BlackboardTarget |
TargetKeyId |
SerializableGuid | Blackboard key containing the target GameObject (when BlackboardTarget) |
OnUnhandled |
enum | Ignore, Warn, or Fail if no listener handles the signal |
See Inter-Agent Communication for a complete walkthrough.
Post-Process Base Class¶
Post-processes run after a strategy returns Success. They handle state transitions, key resets, memory clearing, and follow-up behaviors.
public abstract class BaseGoapPostProcess : ScriptableObject
{
public abstract Type GetSettingsType();
public virtual void OnStart(IGoapAgentContext context, GoapBlackboard blackboard,
BasePostProcessSettings settings) { }
// NOTE: OnUpdate is ABSTRACT — you must implement it
public abstract GoapActionStatus OnUpdate(IGoapAgentContext context, GoapBlackboard blackboard,
float deltaTime, BasePostProcessSettings settings);
public virtual void OnStop(IGoapAgentContext context, GoapBlackboard blackboard,
BasePostProcessSettings settings) { }
}
Execution Rules¶
- Post-processes only run after strategy Success — skipped entirely on failure.
- They run sequentially — PP[0] must return Success before PP[1] starts.
- Failure in any post-process fails the entire plan.
- All post-processes run before
HandlePlanCompletedfires the stored transition. OnStop()is called on success, failure, AND external interruption.
Built-In Post-Processes¶
| Post-Process | Purpose | Key Settings |
|---|---|---|
| WaitPostProcess | Idle for a configurable duration after strategy success | Duration (0 = pass-through) |
The Self-Reset Pattern¶
States should own and reset their own keys. Use a PostProcess chain:
PostProcess 1: SetBool(InvestigationComplete = true) ← goal satisfied
PostProcess 2: ClearRecollection(TargetPosition) ← cleanup
PostProcess 3: SetBool(InvestigationComplete = false) ← reset for next entry
HandlePlanCompleted fires stored transition → Patrol
Step 3 does NOT cancel the transition because HandlePlanCompleted uses the transition stored at plan creation time — it does not re-evaluate the goal.
Tip
Without self-reset, every new path INTO a state would need the same setup step added to the transition source. The self-reset pattern makes states robust to any entry path.
See Also
- GOAPAgent — The component that coordinates the executor running your strategies
- Blackboard — The key-value store strategies read from and write to
- The Planning Loop — How the executor calls OnStart/OnUpdate/OnStop
- Guard Post Demo — Complete example of strategies, post-processes, and the self-reset pattern in action
- Glossary — Quick definitions for Strategy, Post-Process, and related terms
What's Next¶
- Custom Sensors — Build sensors that feed data to your strategies.
- Override System — Tune strategy settings per-agent without changing the brain.
- Inter-Agent Communication — Use the signal system for typed messaging.