Examples

Replicated Patrol

Server-authoritative GOAP in a replicated actor. The server plans; clients observe the agent's current action via standard UE replication.

[ Example ]

Replicated Patrol — multiplayer

The canonical pattern for putting an Intent Forge agent in a replicated actor — the kind you'd ship in a co-op shooter, hosted PvE encounter, or any multiplayer scenario where AI behaviour must be consistent across all clients. The server runs the planner; clients receive the agent's current action via UE's standard replication and use it to drive cosmetic state (animation, VFX, audio) without ever planning themselves.

DIFFICULTY ▮▮▮ ADVANCEDSHIPS AS WALKTHROUGH (C++ + BLUEPRINT)BUILD ~60 MIN

The actor shape

// In your project — not in IntentForgePlugin
UCLASS()
class AReplicatedPatrolGuard : public APawn
{
    GENERATED_BODY()

public:
    AReplicatedPatrolGuard();

protected:
    virtual void BeginPlay() override;

    UPROPERTY(VisibleAnywhere)
    TObjectPtr<UIntentForgeComponent> IntentForge;

    UFUNCTION()
    void HandleReplicatedActionChanged(
        const FIntentForgeReplicatedAction& NewAction,
        const FIntentForgeReplicatedAction& PreviousAction);
};
AReplicatedPatrolGuard::AReplicatedPatrolGuard()
{
    bReplicates = true;
    SetReplicatingMovement(true);

    IntentForge = CreateDefaultSubobject<UIntentForgeComponent>(TEXT("IntentForge"));
    // The component opts into replication automatically.
}

void AReplicatedPatrolGuard::BeginPlay()
{
    Super::BeginPlay();
    // Delegate fires only on non-authoritative clients.
    IntentForge->OnReplicatedActionChanged.AddDynamic(
        this, &AReplicatedPatrolGuard::HandleReplicatedActionChanged);
}

void AReplicatedPatrolGuard::HandleReplicatedActionChanged(
    const FIntentForgeReplicatedAction& NewAction,
    const FIntentForgeReplicatedAction& PreviousAction)
{
    // Resolve ActionId via GetActionByName — GetActionByHandle is NOT
    // available here (the wire snapshot omits the handle index;
    // cosmetics key on action identity, not plan position).
    if (const UIntentAction* Action = IntentForge->GetActionByName(NewAction.ActionId))
    {
        // Action->ExecutorClass, Action->DefaultParams, or your
        // designer-authored cosmetic metadata is now available
        // for the anim BP / VFX pick.
    }
    // NewAction.ActionId == NAME_None is the idle sentinel.
}

Authoring the archetype

The archetype, schema, actions, and goals are identical to the single-player Patrol / Chase example. Networking doesn't change the planning surface — only where the planner runs.

Build the same DA_Schema_Patrol, DA_Action_Patrol, DA_Action_Chase, DA_Goal_Patrol, DA_Goal_Chase from the single-player example.
Assemble them into DA_Archetype_PatrolGuard.
Assign the archetype on the UIntentForgeComponent in your replicated pawn Blueprint or C++ class.

What replicates (and what doesn't)

ReplicatedRead on client via
ReplicatedAction.ActionId (FName)OnReplicatedActionChanged delegate
Archetype (UIntentArchetypeAsset*)GetActionByName(ActionId) once Archetype arrives
PlanGeneration (int32)GetPlanGeneration()
Not replicatedLives on
Full FPlanServer only — clients see one action at a time
Plan step indexServer only — cosmetics key on action identity, not position
FWorldStateServer only — sensor data never crosses the wire
Active executor instanceServer only — clients drive cosmetics from delegate
Recent plan / activity historyServer only — diagnostic data
Goal scores, considerationsServer only — internal to the planner

Bandwidth per replicated agent is roughly 20–80 bytes/second, peaking briefly when a multi-step plan transitions. The anti-flap stack keeps ReplicatedAction stable, so most of the time nothing is going over the wire.

Where to write facts

The recommended pattern is server-side only. Your gameplay code on the server detects events ("player entered patrol zone"), calls SetFactBool(NearPlayer, true) on the component, and the component replans + replicates the new ReplicatedAction automatically.

A client that calls SetFactBool is no-oped by default. The console variable IntentForge.bAllowClientFactWrites (default false) is the gate for a future client→server fact-write RPC. In v1.0-alpha, flipping it to true does not yet route through an RPC — it logs and drops with an explanatory message. Use it for development / debugging only. Don't ship production multiplayer with it enabled; write facts from server-side gameplay systems where the authority and replication contracts are clean.

Driving a replan from a client gameplay event

When a client-side event (UI interaction, predicted hit confirmation, ability cast) needs the server's agent to re-evaluate immediately rather than wait for the safety-net timer, call RequestServerReplan from the client:

// On the owning client (or any client with relevance to the actor)
void AYourPlayerController::OnQuestItemPickedUp()
{
    if (AReplicatedPatrolGuard* Nearby = GetNearbyGuard())
    {
        Nearby->IntentForge->RequestServerReplan();
    }
}

RequestServerReplan is the Blueprint-friendly wrapper. On the authority it forwards to RequestReplan() directly. On a client it verifies the owning actor has a network connection (i.e. a pawn possessed by the local PlayerController) before invoking the underlying ServerRequestReplan RPC. If no owning connection exists, it logs a warning and returns — UE silently drops Server RPCs from connectionless actors, so the helper surfaces the failure mode.

The RPC itself is Server, Unreliable, WithValidation. Server-side, the handler applies a cooldown via LastReplanTime to drop redundant requests, so spamming the call is safe.

Authority gating model

Every mutating entry point on UIntentForgeComponent checks HasAuthority() (via an internal ComponentIsAuthoritative helper) and no-ops on clients. This includes:

  • SetArchetype
  • SetFactBool / SetFactScalar / SetFactObject / ClearFactObject
  • BatchMutate
  • RequestReplan (and the internal EvaluateReplan it schedules)
  • NotifyActionCompleted
  • Sensor sampling and the safety-net replan timer

Clients are passive observers. They receive ReplicatedAction, Archetype, and PlanGeneration via standard UE replication; they do not run sensors, the planner, or the dispatcher.

Authority-dependent initialization is deferred to BeginPlay rather than InitializeComponent. HasAuthority() is unreliable during InitializeComponent on replicated actors — the net role hasn't been assigned yet on placed actors, so initializing there could either double-init or skip-init depending on the load path. The component also guards both initialization helpers with idempotency checks so a pre-BeginPlay SetArchetype call doesn't double-up when BeginPlay runs.

Anti-flap is server-only

The five-family Anti-Flap Toolkit runs on the server. Clients see the result — a stable CurrentAction that only changes when the server's stack has decided the change is real. You don't need to implement any flap correction on the client; the animation state will be pre-stabilised by the time you see it.

For animation Blueprints: bind state transitions to OnReplicatedActionChanged and trust that the input is clean.

Dispatcher mode for replicated agents

For replicated agents, use DispatchMode = Auto (the default) on the server. The Auto dispatcher only registers components when Owner->HasAuthority(), so clients automatically skip registration and never spawn executors.

Dispatch modeServer-side behaviourClient-side behaviour
Auto (default)Plans + executes via the dispatcher subsystemNo registration; no executor
StateTreePlans inside the StateTree; the IntentForge task drives executorsWrap the task in a Server Authority decorator
BehaviorTreePlans inside the BT; standard AIController already runs server-onlyNo special handling needed
ExternalYour code owns the lifecycleYour code owns the lifecycle — gate on HasAuthority

Testing locally

Editor Preferences → Play → Multiplayer Options: set Number of Players to 2 and Net Mode to Play as Listen Server.
Drop one or more AReplicatedPatrolGuard actors in the level.
Hit Play. Two PIE windows open — server (window 1) and client (window 2).
In the server window, open Window → Developer Tools → IntentForge Live Inspector. The agent appears with its full plan, sensors, and history.
In the client window, open the Live Inspector. The agent appears with CurrentAction populated but nothing else — the client never planned and has no local state to inspect.
Verify animation transitions in the client window are driven by OnReplicatedActionChanged — the server's plan adoption shows up after the network's RTT.

What this example does not cover (yet)

  • Client-side prediction with rollback — the v2.0 candidate for high-latency / fast-paced shooter shapes. The current model trades ~100ms perceived latency for simplicity and cheat-resistance.
  • Per-fact replication — projects that want clients to observe specific facts (e.g. a guard's alert level for HUD display) will need to mirror those facts onto the actor as separate replicated UPROPERTYs in v1.0-alpha. A FactSchemaAsset-level replication policy is a v1.x candidate.
  • Headless dedicated server tuning — works by default; profiling notes for high-agent-count dedicated servers are a roadmap item.
◢ Next

Try this next

On this page