Skip to main content

Continuous State

Continuous state is how a moving entity moves on every other client. Set the entity component to Dynamic mode and, every replication interval, the owner sends a snapshot of the actor's location and rotation; each other client in range applies it to a proxy through the rendering backend. There is nothing to write for that: the SDK's default executor takes the snapshot for you.

What is coming

A later SDK release simplifies this further: continuous state will happen by itself, with location and rotation, and need no executor at all, and it adds an extension point for sending your own data alongside them. The aim is to take this setup out of your hands. Everything this page teaches keeps working.

When to use it​

Almost always the default is enough: a pawn, a player, an AI that moves. Write a custom snapshot only when more fields must travel with the transform at the movement cadence, a pose, say. For many small independent values that change at different times, prefer Crowdy State properties: they diff per property and ship only what changed. The two are independent channels, not alternatives; an entity can use both.

Dynamic mode is unrelated to ownership and to Crowdy State property replication.

Mode on the entity component chooses whether the continuous channel runs. It does not decide where your replicated properties go, and Static mode does not disable them; both work in either mode. Set Dynamic for a moving entity and nothing else changes.

The default: no executor at all​

Set Mode to ECrowdyEntityMode::Dynamic and leave StateExecutor unset. The SDK's UCrowdyDefaultActorUpdateExecutor snapshots the actor's transform into FCrowdyActorState, a Location and a Rotation and deliberately nothing else. With bAutoRegister on (the default) the component joins the channel at BeginPlay; off, you call StartReplication yourself, and StopReplication leaves the channel.

The lantern world's mover is ALanternPlayer, the player character from the Quickstart: a UCrowdyEntityComponent in Dynamic mode with PlayerDerived identity and LocalClient ownership, and a Torch point light, so a moving light is what every other client sees.

// LanternPlayer.h
#include "Components/PointLightComponent.h"
#include "Replication/Components/CrowdyEntityComponent.h"
#include "LanternPlayer.generated.h"

UCLASS()
class ALanternPlayer : public ACharacter
{
GENERATED_BODY()

public:
ALanternPlayer();

virtual void BeginPlay() override;

UPROPERTY(VisibleAnywhere, Category = "Lantern")
TObjectPtr<UPointLightComponent> Torch;

UPROPERTY(VisibleAnywhere, Category = "Crowdy")
TObjectPtr<UCrowdyEntityComponent> CrowdyEntity;
};

// LanternPlayer.cpp
#include "LanternPlayer.h"

ALanternPlayer::ALanternPlayer()
{
Torch = CreateDefaultSubobject<UPointLightComponent>(TEXT("Torch"));
Torch->SetupAttachment(RootComponent);

// Dynamic sends the transform every interval, Player Derived names the entity after the signed-in user, Local Client makes this client its sender.
CrowdyEntity = CreateDefaultSubobject<UCrowdyEntityComponent>(TEXT("CrowdyEntity"));
CrowdyEntity->Mode = ECrowdyEntityMode::Dynamic;
CrowdyEntity->IdentityPolicy = ECrowdyIdentityPolicy::PlayerDerived;
CrowdyEntity->Ownership = ECrowdyOwnership::LocalClient;
}

Start the pawn only once the SDK reports the connection up (OnUDPConnectionSuccess, or GetUDPConnectionState() at Connected); the Quickstart spawns it from that event, and the Gotchas say why.

Success signal. On a second client the player's torch moves with the owner. crowdy.entity.trace 1 logs the registration; if nothing moves, check the backend config before the executor.

A host-owned Dynamic entity leaves BeginPlay sending nothing.

bAutoRegister starts the channel only for an entity this client owns outright. A level-placed Dynamic entity with Ownership = Host has no owner at BeginPlay, so nothing is sent until the host is known; the component logs this once. A moving world entity is driven by the host through StartReplication once it knows it is the host, or by an owner after an ownership transfer.

A custom executor​

Subclass UActorUpdateExecutor when the snapshot needs more than a transform. Two functions: GetActorState(UpdateComponent) returns the FInstancedStruct snapshot (UpdateComponent is the entity component; GetOwner() on it is the actor), and GetStateStruct() returns the UScriptStruct it is made of, which registers the type for the wire; the struct needs no annotation.

Override the _Implementation functions, with const, never the bare event names.

Both are BlueprintNativeEvents, so the bare names are not virtual in the generated C++. A native subclass declares GetActorState_Implementation(const UActorComponent*) const override and GetStateStruct_Implementation() const override; UCrowdyDefaultActorUpdateExecutor is the shipped example of the correct shape. Marking GetActorState itself override does not compile, because there is nothing virtual to override.

ULanternPlayerExecutor snapshots FLanternPlayerState: the transform the default carries plus one field of the player's own, bTorchLit, so a proxy's torch can follow its owner's. The struct is declared next to the executor, and the executor is assigned to StateExecutor in the character's constructor. On the other clients the snapshot is applied by the backend's replication application policy, which is what moves a remote player's proxy: the shipped one applies Location and Rotation, and a policy of your own replaces it, so write one only when your executor sends more than the transform, and then apply the transform yourself first.

// LanternPlayer.h
#include "Replication/Executor/ActorUpdateExecutor.h"

// The custom snapshot: the transform the default carries, plus one field of your own.
USTRUCT()
struct FLanternPlayerState
{
GENERATED_BODY()
UPROPERTY() FVector Location = FVector::ZeroVector;
UPROPERTY() FRotator Rotation = FRotator::ZeroRotator;
UPROPERTY() bool bTorchLit = true;
};

UCLASS()
class ULanternPlayerExecutor : public UActorUpdateExecutor
{
GENERATED_BODY()

public:
virtual FInstancedStruct GetActorState_Implementation(const UActorComponent* UpdateComponent) const override;
virtual UScriptStruct* GetStateStruct_Implementation() const override;
};

// LanternPlayer.cpp
CrowdyEntity->StateExecutor = CreateDefaultSubobject<ULanternPlayerExecutor>(TEXT("Executor"));

FInstancedStruct ULanternPlayerExecutor::GetActorState_Implementation(const UActorComponent* UpdateComponent) const
{
FLanternPlayerState State;
if (const ALanternPlayer* Player = UpdateComponent ? Cast<ALanternPlayer>(UpdateComponent->GetOwner()) : nullptr)
{
State.Location = Player->GetActorLocation();
State.Rotation = Player->GetActorRotation();
State.bTorchLit = Player->Torch->IsVisible();
}
return FInstancedStruct::Make(State);
}

UScriptStruct* ULanternPlayerExecutor::GetStateStruct_Implementation() const
{
return FLanternPlayerState::StaticStruct();
}
Only the owner runs GetActorState. Keep it a read-only snapshot.

A remote proxy is fed from the network and never calls it, so a side effect inside the override happens on one client only; a value the proxy needs comes from the backend's policy. Read the actor, fill the struct, return it.

The owner evaluates GetActorState every interval (ReplicationIntervalHz, 10 by default, 1 to 10 Hz, one clock shared with Crowdy State; bUseAutoReplicator off means no polling at all). With bSendActorStateOnlyOnChange on, the default, an unchanged snapshot does not repeat: a full keyframe goes out every ActorKeyframeIntervalSeconds (3 s) so a late or lossy observer converges, and a heartbeat every ActorHeartbeatIntervalSeconds (1 s) so an idle entity is not reaped; off, the snapshot goes out every interval whether it changed or not. The map profile page lists each field. A field lives on exactly one channel: a CrowdyState property whose name and canonical type also match a field of the executor struct is dropped from the Crowdy State layout at discovery, with an error mentioning also lives in its executor state struct.

Gotchas​

  • Mode is orthogonal to Ownership and to Crowdy State. Do not set Dynamic hoping to make properties replicate; they already do.
  • _Implementation, const, override. The shipped default is the template.
  • Nothing is held until the connection is up. OnUDPConnectionSuccess already includes the server's readiness wait after assignment, so you never time anything; but an actor update sent before it is neither held nor replayed, so it is lost, mostly silently, until the next interval's update or keyframe repairs it. A Dynamic entity you own that begins play early heals that way; spawn a player pawn from the event instead, as the Quickstart does.
  • An unchanged actor is not silent. Set both intervals to 0 and an idle entity sends nothing after its first snapshot, and its observers time it out after the tracker's ActorTimeoutThreshold.
  • The policy is what moves the proxy. With no Backend Config the shipped UCrowdyTransformRepPolicy applies FCrowdyActorState, interpolating across a ring of recent samples with bounded extrapolation; a custom struct like FLanternPlayerState is skipped until the profile names a policy that reads it, and that policy must apply the transform itself, or the proxy stands still while the owner moves.
  • Class identity travels on every update, so several entity classes may share one state struct; the receiver never derives the class from the struct. This is what lets the default executor serve every entity in a project.
  • RegisterStateClass on UCrowdyActorManager is filled for you at BeginPlay; you never call it.