Skip to content

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

Vector3 agentPos = context.transform.position;
string agentName = context.gameObject.name;

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

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

  1. Post-processes only run after strategy Success — skipped entirely on failure.
  2. They run sequentially — PP[0] must return Success before PP[1] starts.
  3. Failure in any post-process fails the entire plan.
  4. All post-processes run before HandlePlanCompleted fires the stored transition.
  5. 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