# Player Sign-in
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/player-sign-in
# Player Sign-in
Nothing replicates until a player is signed in. Once sign-in succeeds the SDK opens the realtime connection itself, so this is the one step every page in this section assumes has already happened. This page shows the most basic way to do it and nothing more.
## When you touch this
Once per project, at startup, before any entity, event, or property.
## Sign in with a password
The Game Instance signs in from `Init`, after `Super::Init()` has created its subsystems: bind **On Login** and **On UDP Connection Success** on the Crowdy SDK Subsystem, then call `Login` with an email and password read from `Config/DefaultGame.ini`. The first handler reports the sign-in; the second fires when the connection is up.
```cpp
// LanternGameInstance.h
#include "Subsystem/CrowdySDKSubsystem.h"
#include "LanternGameInstance.generated.h"
UCLASS(Config = Game)
class ULanternGameInstance : public UGameInstance
{
GENERATED_BODY()
public:
virtual void Init() override;
// A development account read from Config/DefaultGame.ini. Never ship credentials in a game.
UPROPERTY(Config)
FString Email;
UPROPERTY(Config)
FString Password;
private:
UFUNCTION()
void HandleLogin(bool bSuccess, FString Message);
UFUNCTION()
void HandleConnected();
};
// LanternGameInstance.cpp
#include "LanternGameInstance.h"
void ULanternGameInstance::Init()
{
Super::Init();
UCrowdySDKSubsystem* Sdk = GetSubsystem();
Sdk->OnLogin.AddDynamic(this, &ULanternGameInstance::HandleLogin);
Sdk->OnUDPConnectionSuccess.AddDynamic(this, &ULanternGameInstance::HandleConnected);
Sdk->Login(Email, Password);
}
void ULanternGameInstance::HandleLogin(bool bSuccess, FString Message)
{
UE_LOG(LogTemp, Log, TEXT("Login %s %s"), bSuccess ? TEXT("ok") : TEXT("failed:"), *Message);
}
void ULanternGameInstance::HandleConnected()
{
// From here on entities register on the wire and a host is elected.
UE_LOG(LogTemp, Log, TEXT("Connected to the app"));
}
```
Make it the project's Game Instance under **Project Settings, Maps & Modes, Game Instance Class**, and give it a test account in `Config/DefaultGame.ini` (`YourModule` is your game module's name):
```ini
[/Script/YourModule.LanternGameInstance]
Email=you@example.com
Password=your-test-password
```
:::caution[A test account belongs in a config file or a pin default, never in shipped code.]
A package stages `Config/DefaultGame.ini`, so an account written there ships with the game. Remove the `Email` and `Password` lines before you package, and sign players in from your own sign-in screen instead.
:::
Create a Widget Blueprint with two **Editable Text Box** widgets and a **Button**, and two String variables, `Email` and `Password`, each set from its box's **On Text Committed**. The button's **On Clicked** calls a custom event `SignIn`, whose graph reads the two variables into the latent **Login** node and removes the widget from its parent on `On Success`. Add the widget to the viewport from your Game Mode's **Event BeginPlay** or a level Blueprint at startup, not from a Game Instance's **Event Init**, which runs before the SDK's subsystems exist ([why](../services/authentication.md#signing-in)).
[Blueprint snippet: signin-widget, see /bp/unreal-sdk/signin-widget.txt]
:::warning[Never print, log, or store the token in the result.]
The `Result` a success carries holds the player's `UserID` and a token the SDK already keeps for you. The SDK's own **On Login** event on the Crowdy SDK Subsystem carries a message and no credential, on purpose; keep anything you put on screen the same way.
:::
A player who has not yet agreed to the current terms sees a prompt over the game before the sign-in completes, and it finishes on its own once they accept; you add nothing for it. [Authentication](../services/authentication.md#let-the-player-agree-to-the-terms) covers what it shows and how to draw your own.
If this machine signed in before, restore instead of asking: call `RestoreSession` first and show the widget only on its error path, as [Authentication](../services/authentication.md#restore-a-saved-session) shows; the connection then comes up exactly as after a `Login`.
**Success signal.** The log reads `Login ok`, then `Connected to the app`. From then on `Get UDP Connection State` on the subsystem reads connected, and the rest of the Runtime section applies. Start replicating your pawn only from that moment: **On UDP Connection Success** fires once the server has assigned this client and its readiness wait has elapsed, an update sent earlier is not held for it, and the [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity) spawns the pawn from the connected handler for that reason.
## Related
- [Authentication](../services/authentication.md): register, magic link, social sign-in, restoring a saved session, sign-out, and the events.
- [Quickstart](../quickstart.md): this step in the context of one complete lantern, and the pawn that makes the signed-in player present.
- [Connection and reconnect](./connection-and-reconnect.md): what the SDK does once the sign-in succeeds.
# Map Profiles
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/map-profile
# Map Profiles
A map profile is a `UCrowdyMapProfile` data asset: one asset configures one map end to end. A map you configure nothing for runs on the profile the plugin ships; a profile of your own is how you change a map's settings, or switch the SDK off there. A configured row whose asset fails to load resolves to no profile, and that map has no networking however correct the code is.
## When you need one
When a map needs settings the shipped default does not have: a Backend Config of your own, a different send cadence, a tighter relevance distance. The [Quickstart](../quickstart.md) authors none and runs on the shipped default. A map that should run without the SDK gets a profile with **Enable Networking** unticked; that records the intent, and it is the only way to switch the SDK off on one map.
## Create the asset
In the Content Browser, **Add**, **Miscellaneous**, **Data Asset**, pick **Crowdy Map Profile**. The Details panel shows every field of the asset:

The screenshot is the SDK's own shipped default profile. **Backend Config** is empty on it, which is fine: the actor pool runs on a built-in config when none is set.
| Field | Default | What it controls |
|---|---|---|
| `bEnableNetworking` | on | Gates the host subsystem, the event router, and the entity subsystem on this map. Off means the map runs with the SDK loaded but idle. |
| `ActorManagement` | see [Rendering backends](./rendering-backends.md) | Which backend draws remote entities (`BackendClass`, `BackendConfig`) and the actor tracker settings. Pick **Crowdy Mass Rendering Backend** as the **Backend Class** to draw a crowd; see [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md). |
| `bUseAutoReplicator` | on | The continuous state channel for Dynamic entities. See [Continuous state](./continuous-state.md). |
| `ReplicationIntervalHz` | 10 (1 to 10) | Shown as **Replication Interval (Hertz)**. The send cadence shared by the continuous channel and the Crowdy State replicator. Greyed out in the Details panel when Use Auto Replicator is off, but still read by the Crowdy State replicator. |
| `bSendActorStateOnlyOnChange` | on | Shown as **Send Actor State Only On Change**. An unchanged Dynamic entity sends a keyframe or a heartbeat instead of restating its state every interval. |
| `ActorKeyframeIntervalSeconds` | 3.0 | Full re-send period for an unchanged Dynamic entity, so a late observer converges. 0 turns keyframes off. |
| `ActorHeartbeatIntervalSeconds` | 1.0 | Heartbeat period for an unchanged Dynamic entity: the spatial header and no state, which is what keeps an idle entity from being reaped. 0 turns heartbeats off. |
| `bUseStateReplicator` | on | Master switch for Crowdy State property replication. See [Crowdy State](./crowdy-state.md). |
| `StateRelevanceDistance` | Four Chunks | Shown as **State Relevance Distance**. How far a Crowdy State delta travels, as a chunk count (`ECrowdyReplicationDistance`), tighter than the continuous channel. |
| `StateKeyframeIntervalSeconds` | 2.0 | Period of the Crowdy State keyframe, the full re-send of every property marked `CrowdyHeartbeat`. 0 turns the keyframe off map-wide; on-change replication is unaffected. |
:::note[An empty Backend Config means the built-in default, not a disabled backend.]
`ActorManagement.BackendClass` defaults to `UCrowdyActorPoolBackend`. With `ActorManagement.BackendConfig` left empty, the backend creates a transient **Actor Pool Backend Config** and applies remote movement with `UCrowdyTransformRepPolicy`, the shipped policy that reads the default executor's `FCrowdyActorState` (location and rotation) and interpolates across a ring of recent samples, extrapolating briefly when an update is late. Set a config of your own when your executor sends a state struct of its own, when you want pool sizes per class, or when you want a different pool policy. The actor pool refuses a config of another backend's class with a warning naming the field. The Crowdy Mass Rendering Backend takes its own Backend Config, and ignores a config of another class without a word. See [Rendering Backends](./rendering-backends.md) and [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md).
:::
## Assign the profile
Open **Project Settings, Plugins, Crowdy SDK** and find the **Map Profiles** category of `UCrowdySDKDeveloperSettings`:

- `MapProfiles` maps a level asset to a profile. The lookup is by the map's short name, with the Play in Editor prefix stripped, so a PIE session resolves the same entry as a packaged build.
- `DefaultProfile` covers every map without an entry.
The SDK resolves the profile for a world through `UCrowdySDKDeveloperSettings::ResolveProfileForWorld`, in this order:
1. A `MapProfiles` entry whose map matches the current one.
2. `DefaultProfile`, if set.
3. The profile a plugin ships. The Crowdy SDK plugin registers its own `DA_CrowdySDKDefaultProfile` at module startup through `RegisterShippedDefaultProfile`, so a project that sets nothing at all runs on that asset rather than going inactive. The log says so once per world.
4. Nothing: the SDK stays inactive on the map and logs a warning, once per world, naming the map and the fix.
:::caution[An entry that names an asset that fails to load is a broken setting, not an absent one. Resolution stops there.]
A `MapProfiles` row or a `DefaultProfile` that points at an asset the build cannot load returns no profile and does not fall through to the next step. The warning names the asset path; check that it still exists and is packaged.
:::
Every SDK subsystem resolves through the same function, and both warnings are reported once per world rather than once per subsystem, so you will see one line, not a dozen.
## Check it from code
A lantern that never lights for other players is nearly always a map whose profile did not resolve. The example, `CheckMapProfile`, called from the lantern's `BeginPlay`, asks the same question the SDK asks, once, so the answer is in the log before anything else is suspected.
```cpp
// Lantern.cpp
#include "Data/CrowdyMapProfile.h"
#include "Utils/CrowdySDKDeveloperSettings.h"
void ALantern::CheckMapProfile()
{
// A lantern that never lights for other players usually has no active profile; rule that out first.
const UCrowdyMapProfile* Profile = UCrowdySDKDeveloperSettings::ResolveProfileForWorld(GetWorld());
if (!Profile || !Profile->bEnableNetworking)
{
UE_LOG(LogTemp, Warning, TEXT("ALantern: no active map profile; this lantern will not replicate."));
}
}
```
`ResolveProfileForWorld` is a plain C++ static. There is no Blueprint node for it.
There is no Blueprint node for profile resolution: assigning a profile is Project Settings and Data Asset editing only, which the two screenshots above cover. What a Blueprint can read is the connection, `Get UDP Connection State` on the Crowdy SDK Subsystem; a connected state with dead entities is the profile symptom.
**Success signal.** With a resolved profile the entity subsystem logs registrations once `crowdy.entity.trace 1` is on. Without one, the log carries a single `resolved no map profile` warning for the map and no entity ever registers.
## The shipped default, in one paragraph
The plugin registers a shipped profile under the provider name `CrowdySDK`, and only one registration is in effect at a time: a second plugin offering a different profile is refused and logged, so which one a map runs on never depends on module load order. `UnregisterShippedDefaultProfile` withdraws an offer at module shutdown; `GetShippedDefaultProfilePath` and `GetShippedDefaultProfileProvider` report what is currently offered; `ResolveShippedDefaultProfile` returns the asset, or null with a reason when an offer exists but its asset failed to load. None of these is something a game calls; they exist so a plugin can carry a profile.
## Gotchas
- A `MapProfiles` row or `DefaultProfile` naming an asset that did not load means no networking on that map. Check this first when a map looks dead; the warning names the asset.
- The shipped default profile has no `BackendConfig`, which means the built-in transform policy draws remote entities. On 2.14.0 and earlier nothing was drawn from the continuous channel in that state; see [What's Changed](../guides/whats-changed.md#2026-09-22-sdk-v2150).
- A non-networked map wants a profile with **Enable Networking** off, not no profile. The warning text asks for exactly that.
- `StateKeyframeIntervalSeconds` is a safety net, not the mechanism. Changed properties ship on change whatever the interval; 0 only stops the periodic baseline for `CrowdyHeartbeat` properties.
- `ReplicationIntervalHz` is one clock for two channels: continuous state and Crowdy State share it but send independently.
## Related
- [Rendering backends](./rendering-backends.md): `BackendClass`, `BackendConfig`, and the policy classes.
- [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md): choose the Crowdy Mass Rendering Backend and give it a Backend Config.
- [Continuous state](./continuous-state.md): what `bUseAutoReplicator` and the keyframe and heartbeat intervals drive.
- [Crowdy State](./crowdy-state.md): what `bUseStateReplicator`, `StateRelevanceDistance`, and `StateKeyframeIntervalSeconds` drive.
- [Config Sync](../studio/config-sync.md): the network half of the same settings class, written by Crowdy Studio.
# Entities and Spawning
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/entities-and-spawning
# Entities and Spawning
An entity is an actor with a `UCrowdyEntityComponent`. A level-placed one needs no spawn call: every client loads it and derives the same NetID. This page is about the other kind, an actor created during play, which one client spawns and every other client rebuilds from a spawn event. For identity policies, NetIDs, and the owner and proxy roles, read [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md) first.
## When to spawn through the SDK
Whenever an actor created at runtime must exist on other clients: a dropped item, a thrown object, a summoned creature. The plain engine `SpawnActor` creates a local actor and tells nobody; the SDK's spawn call creates the same actor and announces it.
## Spawning
`UCrowdyEntitySubsystem::SpawnEntity(EntityClass, SpawnTransform, InitialState)` spawns the class locally with a deferred spawn, injects the entity's identity into its component before `BeginPlay`, and broadcasts a spawn event so every client in range spawns the same class as a proxy. The client that called it is the owner; `Role` is `Owner` there and `RemoteProxy` everywhere else. `UCrowdyUtilities::SpawnCrowdyEntity(WorldContextObject, EntityActorClass, SpawnTransform, InitialState)` is the same operation as a static call with a world context pin, which is the form a Blueprint uses.
`InitialState` is an `FInstancedStruct` of your own type. It travels with the spawn and lands in the component's `OnCrowdySpawned` event on every client; pass an empty struct when there is nothing to send.
The lantern world's runtime spawn is the player arriving with a lantern. `ALanternPlayer` is the player character: a pawn carrying a `UCrowdyEntityComponent` in Dynamic mode with PlayerDerived identity and LocalClient ownership, declared in full on [Continuous state](./continuous-state.md). Once the character knows it is locally owned, `ALanternPlayer::DropLantern` spawns an `ALantern` a short way in front of it, and the new lantern appears there on every client in range. The moment is `OnCrowdyOwnershipAssigned`, not `BeginPlay`: a player pawn spawned during play (`RestartPlayer`) is possessed after its `BeginPlay`, and its Player Derived identity resolves inside that first possession, so `IsLocallyOwned()` is still false at `BeginPlay`. The `AddDynamic` in the block sits in `ALanternPlayer::BeginPlay`, after `Super::BeginPlay()`; the handler fires on the tick after the entity registers, on the first-frame pawn and the runtime-spawned one alike.
```cpp
// LanternPlayer.h
virtual void BeginPlay() override;
UFUNCTION()
void HandleOwnershipAssigned(const FGuid& NewOwnerID, ECrowdyRole NewRole, bool bIsLocallyOwned);
void DropLantern();
// LanternPlayer.cpp
#include "Lantern.h"
#include "Utils/CrowdyUtilities.h"
void ALanternPlayer::BeginPlay()
{
Super::BeginPlay();
// A pawn spawned during play registers inside its first possession, after BeginPlay, so the owner answer arrives here.
CrowdyEntity->OnCrowdyOwnershipAssigned.AddDynamic(this, &ALanternPlayer::HandleOwnershipAssigned);
}
void ALanternPlayer::HandleOwnershipAssigned(const FGuid& NewOwnerID, ECrowdyRole NewRole, bool bIsLocallyOwned)
{
// Each player arrives carrying a lantern; only the owner spawns it, every client then sees it.
if (bIsLocallyOwned)
{
DropLantern();
}
}
void ALanternPlayer::DropLantern()
{
const FTransform SpawnAt(GetActorRotation(), GetActorLocation() + GetActorForwardVector() * 150.f);
UCrowdyUtilities::SpawnCrowdyEntity(this, ALantern::StaticClass(), SpawnAt, FInstancedStruct());
}
```
Any Actor Blueprint can spawn; nothing on the spawner needs a component. In the player pawn's Blueprint, at **Event BeginPlay**, **Get Crowdy Entity Component** feeds **Bind Event to On Crowdy Ownership Assigned**, whose `Event` pin is wired to a custom event, `OnOwnershipAssigned`, that receives the owner id, the role, and the `Is Locally Owned` flag; the pawn registers inside its first possession, after `BeginPlay`, so this is where the answer arrives. The event's `Is Locally Owned` pin feeds a **Branch** (the owner gate: only the owner spawns), and on the true side **Get Actor Transform** feeds the `Spawn Transform` pin of **Spawn Crowdy Entity**, which takes the entity class and an optional Initial State struct and returns the spawned actor. The class pin in the figure names the sample lantern; pick your own entity class there. A spawner that is not a Player Derived pawn can run the same Branch straight from **Event BeginPlay** with the pure **Is Crowdy Entity Locally Controlled** as the condition; only a Player Derived pawn learns its identity after `BeginPlay`.
[Blueprint snippet: spawn-entity, see /bp/unreal-sdk/spawn-entity.txt]
:::warning[A runtime-spawned entity never consults its Identity Policy or its Ownership.]
`SpawnEntity` injects a fresh NetID, `Role = Owner`, and the spawning client's player id before `BeginPlay`, so the component's `IdentityPolicy` and `Ownership` settings are not read. Setting Stable on a class you spawn does not give it a stable id, and setting Host does not make it host-owned; a level-placed `ALanternPost` is how you get both, and [Ownership transfer](./ownership-transfer.md) is how a spawned entity changes hands afterwards.
:::
:::danger[Do not spawn an entity with the engine's SpawnActor.]
A plain `SpawnActor` never broadcasts the spawn event. The component still registers locally, so the actor looks like an entity on the spawning client and exists nowhere else.
:::
:::caution[A Dynamic entity's spawn event is a one-shot spatial send, so a client that joins later never receives its `InitialState`.]
It reaches the clients within eight chunks of the spawn point at that moment and is never replayed. A client that joins afterwards, or comes into range later, still draws a Dynamic entity from its position updates, but with no spawn event `OnCrowdySpawned` does not fire there. A Static entity is different: its owner repeats the spawn event every 4 seconds (below), so a late joiner gets the actor, its owner, and `OnCrowdySpawned` with the `InitialState`. Anything a late joiner must see on a Dynamic entity belongs in class defaults or in replicated state, and anything that must exist for everyone from the start is level-placed.
:::
**Success signal.** On a second client the proxy appears and its `OnCrowdySpawned` fires with `bIsLocallyOwned` false. A single client can only confirm the send: `crowdy.entity.trace 1` logs the registration and the spawn event; `crowdy.rpc.loopback` replays RPC calls, not spawn events, so no proxy appears.
## Destroying
Three calls converge on the same path: broadcast a destroy event, then destroy the actor after the component's `DestroyDelay` (0 by default, so immediately).
| Call | Where |
|---|---|
| `UCrowdyEntitySubsystem::DestroyEntity(TargetEntity)` | The subsystem, given the actor. |
| `UCrowdyUtilities::DestroyCrowdyEntity(WorldContextObject, TargetEntity)` | The static form with a world context pin; the Blueprint node. `TargetEntity` defaults to self. |
| `UCrowdyEntityComponent::DestroyEntity()` | The component, on itself. See [Entity component](./entity-component.md). |
On every client `OnCrowdyDestroyed` fires first, with `bIsLocallyOwned` true on the client that called the destroy and false everywhere else; nothing stops a non-owner from destroying an entity. The actor goes after the delay. A `DestroyDelay` above zero is for a dissolve or a fade the actor plays out before it disappears. The entity stops sending updates when the destroy is called, not when the actor goes, and other clients do not draw updates that arrive in the half second after the destroy event, so an update already on its way does not bring the entity back.
```cpp
// LanternPlayer.h
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
void PickUpLantern(AActor* Target);
// LanternPlayer.cpp
#include "Lantern.h"
#include "Utils/CrowdyUtilities.h"
void ALanternPlayer::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
// Walking over one of your own lanterns that has gone out picks it up; every client sees it go.
const ALantern* Lantern = Cast(OtherActor);
if (Lantern && !Lantern->bLit && Lantern->CrowdyEntity->IsLocallyOwned())
{
PickUpLantern(OtherActor);
}
}
void ALanternPlayer::PickUpLantern(AActor* Target)
{
UCrowdyUtilities::DestroyCrowdyEntity(this, Target);
}
```
The player picks up a lantern of their own that has gone out: in C++ the character's overlap checks the lantern is unlit and locally owned, then `PickUpLantern` destroys it. The graph is the call alone: from **Event ActorBeginOverlap**, **Destroy Crowdy Entity** takes `Other Actor` on its `Target Entity` pin; the "unlit and yours" check is yours to add in front of it. The removal is visible on every client without any further node.
[Blueprint snippet: despawn, see /bp/unreal-sdk/despawn.txt]
## Receiving a spawn on a remote client
A remote client that receives the spawn event spawns the named class, injects the identity as `RemoteProxy`, finishes spawning, and broadcasts `OnCrowdySpawned` with the `InitialState` from the event. The event carries the class path, so a class that is not loaded yet is streamed in and spawned when it arrives; a destroy event that lands during the load cancels the spawn. If the class ships without a `UCrowdyEntityComponent`, the SDK adds one so the lifecycle callbacks have a home.
That actor is the one players see: the actor pool draws it rather than a pooled copy, so whatever your `OnCrowdySpawned` handler set on it stays. When the entity's first update arrives before its spawn event, the pool draws it at once and the spawn event then hands its `InitialState` to that actor instead of spawning a second one. Either way `OnCrowdySpawned` fires once per entity on each client.
:::note[The spawn and destroy payloads are wire format, not something you build.]
`FCrowdyEntitySpawnEvent`, `FCrowdyEntityDestroyEvent`, and the registry's `FCrowdyEntityRecord` are Blueprint-visible types because they cross a delegate, but you never construct one. `InitialState` is the only payload you author.
:::
### A Dynamic entity can appear with no spawn event at all
A Dynamic-mode entity (see [Continuous state](./continuous-state.md)) streams position updates, and each update carries the class id and the state struct type. The `UCrowdyActorManager` resolves the class from that (`RegisterStateClass` is filled in for you by each entity component's `BeginPlay`) and asks the rendering backend to draw a proxy, so a moving entity shows up on a client that joined late without any spawn round trip.
A Static entity spawned with `SpawnEntity` sends no updates, so its owner repeats the spawn event instead: every 4 seconds while a client owns it, that client sends the spawn event again, from where the entity is now, naming its owner and carrying its `InitialState`. A client that joins later builds the entity from a repeat exactly as if it had seen the spawn, and its `OnCrowdySpawned` fires there. A client that already has the entity ignores repeats, and `OnCrowdySpawned` never fires twice. A repeat costs about 280 bytes plus your `InitialState`: about 70 bytes per second of upload per entity on its owner, and the same in download for each client within eight chunks. A spawn event for an entity destroyed less than 10 seconds earlier is ignored, so a repeat already on its way cannot bring it back; an owner that never received the destroy keeps repeating, and the entity reappears after those 10 seconds. A Static entity placed in the level, or registered with `RegisterStaticEntity`, sends nothing.
:::warning[Preloaded Entity Classes is empty by default, and an observer on this path needs it.]
This path resolves the class by id against loaded classes; a spawn event carries the class path and loads it for you, this does not. The startup scan walks loaded classes, and a Blueprint entity class that nobody on this client has spawned yet is not loaded, so an observer that meets it first over the network cannot name it. The actor manager warns `cannot resolve to a loaded class ... Preload the entity class or give it a ClassIDOverride` and the entity stays undrawn. List every Blueprint entity class a client can be shown before it spawns one itself under **Project Settings, Plugins, Crowdy SDK, Replication, Preloaded Entity Classes** (`PreloadedEntityClasses` on `UCrowdySDKDeveloperSettings`). Classes already loaded for another reason need no entry, and a duplicate costs nothing.
:::
## The actor tracker
Inbound actor updates arrive at `UCrowdyActorTracker`, a world subsystem the actor manager listens to. Its settings live on the `ActorManagement` struct of the [map profile](./map-profile.md) (`FCrowdyActorManagementConfigStruct`, the same struct a `UCrowdyActorManagementConfig` asset holds):

The shipped default profile leaves **Backend Config** at None and runs on the built-in one; the [map profile page](./map-profile.md#create-the-asset) shows that state and when to assign your own.
| Field | Default | Effect |
|---|---|---|
| `bUseCrowdyActorTracker` | on | Off means no tracker, no actor manager, and no backend on this map. |
| `bDispatchUpdatesOnGameThread` | off | Also broadcast each update batch to `OnUpdatesGameThread` for a Blueprint listener. |
| `bEnableOwnerTracking` | on | Track the local player's own actor echo as well: the reflection the [Quickstart](../quickstart.md#2-see-yourself) uses as proof that the server is receiving you. |
| `ActorTimeoutThreshold` | 12 s | Seconds of silence before an entity is dropped on a guess. The server announces a real departure about five seconds after the last update, so this is the fallback. |
| `MaxTrackedActors` | 4096 | How many network-received actors may be tracked at once. A bound on forged ids, not a performance setting; set it above the largest crowd you intend to show. |
| `MaxUpdatesPerBatch`, `MaxBatchWaitTime` | 100, 5 ms | How updates for not-yet-seen entities are batched off the game thread. |
The tracker's events, all on the game thread: `OnRemoteEntityAppeared` (an id seen for the first time, with its `InitialState`), `OnRemoteEntityTimedOut` (this client heard nothing for `ActorTimeoutThreshold`), `OnRemoteEntityLeft` (the server said it is gone, with an `FCrowdyActorLeft` carrying the `ECrowdyActorLeftReason`: `Stale`, or `SessionReleased` when the server ended that actor's session), and `OnRemoteEntityLeftAnnounced` (every departure, including one for an actor this client never held, for code keeping its own per-sender state). Whichever of timed-out and left reaches an actor first is the only one that reports it. The delegate types are `FOnActorSpawnRequested`, `FOnActorTimeoutRequested`, `FOnActorLeftReported` (used by both departure events), and `FOnActorUpdateGameThreadBatch`. `ToggleOwnerTracking` and `ToggleBroadcastUpdatesToGameThread` change those two settings at runtime. `Configure` does not: it applies only before the tracker starts, which is before any Blueprint can call it, so on a running tracker it logs a Warning and changes nothing, and its Blueprint node is deprecated. Set the limits in the [map profile](./map-profile.md) or in the Crowdy SDK developer settings instead. `FCrowdyActorUpdate` is the batch element, carrying `PayloadTypeID`, the payload type id the sender's state blob led with, kept even when no struct is registered for it on this client. `crowdy.replication.tracker.maxgatheredupdates` caps the backlog of gathered updates for actors already on screen. You do not normally touch any of this: the actor manager and the [rendering backend](./rendering-backends.md) consume it for you.
## Looking entities up
`UCrowdyEntitySubsystem` is the registry. `FindEntity(NetID)` returns the actor or null; `FindEntityID(Actor)` is the reverse lookup; `IsLocallyOwned(NetID)` is true when the record names the local player as the owner and the role says this client simulates it (a record that contradicts itself reads as not mine); `GetLocalPlayerID` and `GetHostID` read the two ids everything else is compared against; `SetLocalPlayerID` exists for a game that manages the player id itself, which the session normally seeds for you. `OnEntityRegistered` and `OnEntityUnregistered` (`FOnCrowdyEntityRegistered`, `FOnCrowdyEntityUnregistered`) fire with the NetID as records come and go. The Blueprint-friendly statics on `UCrowdyUtilities` wrap the same registry: `GetCrowdyEntityID`, `GetCrowdyEntity`, `GetCrowdyEntityOwnerID`, `GetCrowdyEntityRole` (an `ECrowdyRole`), `GetAllCrowdyEntitiesByOwner`, `CrowdyIsEntityRegistered`, and `GetLocalPlayerEntityID`.
## Gotchas
- `IsLocallyOwned` means two different things: on the subsystem it takes a NetID and asks the record; on the component it takes nothing and asks whether this client is the entity's authority, host-owned included. See [Entity component](./entity-component.md).
- A spawned entity is owned by its spawner, full stop. To hand it over, use [Ownership transfer](./ownership-transfer.md).
- `InitialState` reaches `OnCrowdySpawned` once on each client that receives the spawn event, on the actor that client draws. A late joiner receives it for a Static entity (from a repeat) but not for a Dynamic one.
- Two unrelated participants can derive the same NetID (a NetID has 32 bits of real entropy). The incumbent keeps it and the newcomer is refused with a log line; a system that binds state to an id should register through `TryRegisterEntity` and give up its claim when the `ECrowdyEntityRegistration` it returns is `RefusedIdHeldByLiveParticipant` (or `RefusedInvalidNetID` for a record with no usable id) rather than `Registered`.
- `UHelperFunctions` has the id helpers a custom identity scheme would reach for: `GetDeterministicID(Seed)` turns a seed into the same `FGuid` on every client, with those same 32 bits of entropy, and `GetNewID` and `GetNewUUID` mint random ones. Prefer the component's identity policies; these exist for code that already has a seed.
## Related
- [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md): NetIDs, policies, owner and proxy.
- [Entity component](./entity-component.md): every callable and event on the component.
- [Ownership transfer](./ownership-transfer.md): moving an entity to another player.
- [Rendering backends](./rendering-backends.md): how a proxy is drawn, and the pooled-proxy gotchas.
- [Continuous state](./continuous-state.md): Dynamic mode and the executor.
# Entity Component
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/entity-component
# Entity Component
`UCrowdyEntityComponent` is the one component the SDK tracks an actor by. It registers the actor in the entity registry, hands out its identity, optionally feeds the continuous state channel, and raises the lifecycle events. This page is the reference; the concepts are on [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md).
## Getting the component
In C++ the actor usually holds the pointer it created in its constructor. From anywhere else, `UCrowdyUtilities::GetCrowdyEntityComponent(Actor)` (Blueprint: **Get Crowdy Entity Component**, `Actor` defaults to self) resolves it GAS-style: if the actor implements `ICrowdyEntityComponentProvider` its answer is used, otherwise `FindComponentByClass`. Implement the interface when the component lives on a child actor or is chosen at runtime; it is deliberately not a node you call directly.

## Settings
All `EditAnywhere`, so a constructor, a Blueprint's Class Defaults, or a placed instance can set them.
| Setting | Default | One line |
|---|---|---|
| `Mode` (`ECrowdyEntityMode`) | Static | Static is event-only. Dynamic also streams the `StateExecutor` snapshot every replication interval. Unrelated to ownership and to Crowdy State, which work in either mode. |
| `IdentityPolicy` (`ECrowdyIdentityPolicy`) | Stable | Stable, PlayerDerived, or Random. Read only when the actor resolves its own identity; a runtime spawn ignores it. |
| `Ownership` (`ECrowdyOwnership`) | Host | Host: the elected host owns it (`Role = HostOwned`). LocalClient: this client owns it (`Role = Owner`). Level-placed actors only. |
| `HostOverride` (`ECrowdyHostOverride`) | Allow | For a LocalClient entity: may the host correct its Crowdy State as a super-user (Allow), or never (OwnerOnly). Hidden when Ownership is Host. |
| `StateHeartbeat` (`ECrowdyStateHeartbeat`) | Inherit | Inherit follows the map profile's keyframe; Off never emits a keyframe for this entity. On-change replication is unaffected. |
| `bAutoApproveOwnershipRequests` | off | The current authority grants any transfer request at once instead of raising `OnOwnershipRequested`. |
| `StateExecutor` (`UActorUpdateExecutor`) | none | Dynamic only. Builds the snapshot; unset means the SDK's transform-only default. |
| `bAutoRegister` | on | Dynamic only. Join the continuous channel at `BeginPlay`; off means you call `StartReplication` yourself. |
| `DestroyDelay` (float) | 0 | Seconds between `OnCrowdyDestroyed` and the actor being destroyed. |
## Callables
Category **Crowdy SDK, Entity Component** in the Blueprint palette.
| Function | One line |
|---|---|
| `GetNetID()` | The entity's shared id, as an `FGuid`. |
| `GetOwnerID()` | The owning player's id; invalid for a host-owned entity. |
| `GetRole()` | `ECrowdyRole`: `Owner`, `RemoteProxy`, `HostOwned`, or `None` before registration. |
| `IsLocallyOwned()` | True when this client is the authority: it owns the entity, or the entity is host-owned and this client is the host. The question to ask before owner work. |
| `GetMode()`, `GetOwnership()`, `GetHostOverridePolicy()`, `GetStateHeartbeat()` | The settings above, read back. There is no getter for `IdentityPolicy`. |
| `StartReplication()`, `StopReplication()` | Join or leave the continuous channel. Meaningful for a Dynamic entity this client owns; `bAutoRegister` calls the first one for you. |
| `SendEvent(Payload, Scope)` | Sends a struct payload addressed at this entity. `ECrowdyEventScope::Everyone` runs the entity's handlers on every client, `OwnerOnly` only on the owner. The lower-level sibling of a `CrowdyEvent`; see [Recipients and routing](./recipients-and-routing.md). |
| `MarkStateDirty(PropertyName)`, `MarkAllStateDirty()` | Schedule one, or every, `CrowdyManualDirty` property to ship on the next replication tick. See [Crowdy State: marking state from outside the actor](./crowdy-state.md#marking-state-from-outside-the-actor). |
| `DestroyEntity()` | Broadcast the destroy event and destroy the owner after `DestroyDelay`. The same path as the subsystem's `DestroyEntity(Actor)` and `UCrowdyUtilities::DestroyCrowdyEntity`. |
`IsLocallyOwned()` here takes no argument and answers for the authority; `UCrowdyEntitySubsystem::IsLocallyOwned(NetID)` answers a narrower question about the record's owner id and does not count the host of a host-owned entity. Do not treat them as interchangeable.
## Events
| Event | Signature | When |
|---|---|---|
| `OnCrowdySpawned` (`FOnCrowdyEntitySpawned`) | `InitialState`, `bIsLocallyOwned` | Runtime-spawned entities only: fired by `SpawnEntity` on the spawner and by the spawn event on every proxy, with the spawn payload. A level-placed entity never receives it; bind `OnCrowdyOwnershipAssigned` there instead. |
| `OnCrowdyDestroyed` (`FOnCrowdyEntityDestroyed`) | `bIsLocallyOwned` | On every client when the entity is torn down, before the delay. |
| `OnCrowdyOwnershipAssigned` (`FOnCrowdyOwnershipAssigned`) | `NewOwnerID`, `NewRole`, `bIsLocallyOwned` | Once the entity knows its owner, on the tick after registration (so a bind in `BeginPlay` still gets it), and again on every reassignment. `NewOwnerID` is invalid for a host-owned entity. |
The example binds all three in `ALantern::BeginPlay`, after `Super::BeginPlay()`; the three `AddDynamic` lines in the block go there. A lantern the player drops at runtime is the kind of entity that receives all three: the spawn path announces `OnCrowdySpawned` right after `BeginPlay`, on the owner and on every proxy, so a bind made there is in place in time. A level-placed post never gets the spawn event; on it only the destroy and ownership handlers ever run. The light shows on spawn, hides on destroy, and brightens or dims as the answer to "do I own this" arrives.
```cpp
// Lantern.h
UFUNCTION()
void HandleSpawned(const FInstancedStruct& InitialState, bool bIsLocallyOwned);
UFUNCTION()
void HandleDestroyed(bool bIsLocallyOwned);
UFUNCTION()
void HandleOwnershipAssigned(const FGuid& NewOwnerID, ECrowdyRole NewRole, bool bIsLocallyOwned);
// Lantern.cpp
CrowdyEntity->OnCrowdySpawned.AddDynamic(this, &ALantern::HandleSpawned);
CrowdyEntity->OnCrowdyDestroyed.AddDynamic(this, &ALantern::HandleDestroyed);
CrowdyEntity->OnCrowdyOwnershipAssigned.AddDynamic(this, &ALantern::HandleOwnershipAssigned);
void ALantern::HandleSpawned(const FInstancedStruct& InitialState, bool bIsLocallyOwned)
{
// Spawned entities only: the spawn path announces it right after BeginPlay, on the owner and on every proxy.
Light->SetVisibility(true);
}
void ALantern::HandleDestroyed(bool bIsLocallyOwned)
{
Light->SetVisibility(false);
}
void ALantern::HandleOwnershipAssigned(const FGuid& NewOwnerID, ECrowdyRole NewRole, bool bIsLocallyOwned)
{
Light->SetIntensity(bIsLocallyOwned ? 8000.f : 2000.f);
}
```
In the spawned lantern's Blueprint, at **Event BeginPlay**, **Get Crowdy Entity Component** feeds **Bind Event to On Crowdy Spawned**, whose `Event` pin is wired to a custom event, `OnLanternSpawned`, that receives the spawn payload and the ownership flag; its body calls **Set Visibility** on `Light`. The other two events bind the same way from the same getter.
[Blueprint snippet: ec-on-spawned, see /bp/unreal-sdk/ec-on-spawned.txt]
## Asking a different question than IsLocallyOwned
The [Quickstart](../quickstart.md) and the identity page both branch on `IsLocallyOwned()`. The accessor family answers finer questions: `GetRole()` distinguishes the owner from the host of a host-owned entity from a proxy, which the boolean cannot. The example grades the post's light by role.
Four more `UCrowdyUtilities` nodes ask role questions without fetching the component: `CrowdyIsRemoteProxy(Entity)` (**Crowdy Is Remote Proxy**) is true when the entity's role is `RemoteProxy`, and `IsCrowdyEntityPlayerControlled(TargetActor)` (**Is Crowdy Entity Player Controlled**) is true for an actor with a Dynamic entity component or a registered remote-proxy record; `SwitchCrowdyIsRemoteProxy` and `SwitchIsCrowdyEntityPlayerControlled` are the same two answers as exec pins.
```cpp
// LanternPost.h
void ShowRole();
// LanternPost.cpp
void ALanternPost::ShowRole()
{
// The same client can own, host, or only mirror this post; each reads a different brightness.
switch (CrowdyEntity->GetRole())
{
case ECrowdyRole::Owner: Light->SetIntensity(8000.f); break;
case ECrowdyRole::HostOwned: Light->SetIntensity(4000.f); break;
default: Light->SetIntensity(1000.f); break;
}
}
```
The generated graphs branch on booleans, so the three-way role grading of the C++ half is shown as its two-way half: **Get Crowdy Entity Component**, **Is Locally Owned**, a **Branch**, and **Set Intensity** to 8000 on `Light` for the owner. For the full grading in Blueprint, replace the Branch with a **Switch on ECrowdyRole** fed by **Get Role**.
[Blueprint snippet: ec-is-owned, see /bp/unreal-sdk/ec-is-owned.txt]
## The two RPC receivers on the component
`RequestOwnership_Implementation(RequesterID)` and `GrantOwnership_Implementation(NewOwnerID, PreviousOwnerID)` are `CrowdyEvent` functions with `CrowdyRecipient = "Multicast"` that carry [ownership transfer](./ownership-transfer.md) over the session channel. Call the static `UCrowdyOwnershipTransfer` nodes instead of these; `GrantOwnershipTo(NewOwnerID)` is the C++ entry the static nodes funnel through.
:::caution[These two events are Multicast with no CrowdyChannel on purpose. Do not re-declare them with a channel.]
They ride the app's session channel, which the SDK always joins, so a request or a grant reaches every client. Re-declared on a channel of their own they would reach that channel's members instead of the entity's audience. Ownership on this plane is a convention in any case; the enforced kind lives in your server code, on the [truth plane](../concepts/two-planes.md).
:::
## Advanced: the static identity helpers
C++ only, pure, and the same functions the SDK's own identity resolution calls. Useful when writing custom identity code; not needed otherwise.
| Static | What it answers |
|---|---|
| `DeriveAuthority(Ownership, LocalPlayerID, OutRole, OutOwnerID)` | The role and owner id an `Ownership` value produces. |
| `ResolveEffectiveOwnership(AuthoredOwnership, bIsLevelPlaced)` | Why a runtime spawn is always owned by its spawner. |
| `ResolveActorInstanceGuid(Actor)` | The engine's per-placement guid, the Stable identity seed. |
| `ShouldUsePlacementGuid(bIsLevelPlaced, bHasInstanceGuid)`, `SelectPlacementGuid(AtRegister, Live)` | When that guid may be used, and why the snapshot taken at `OnRegister` wins over a live read in a cooked build. |
| `ComputeStableNetID(bOutUsedPathFallback)` | The Stable NetID for this actor, and whether it fell back to hashing the path. |
| `DoesOwnershipMatch(OwnerRole, OwnerNetID, TargetRole, TargetOwnerID, HostID)` | The ownership test behind `UCrowdyUtilities::DoesCrowdyEntityOwn`; fails closed when a host-owned side has no host id. |
| `ClassifyPlayerDerivedIdentity(Facts)`, `DescribePlayerDerivedIdentity(Verdict)` | Which condition made PlayerDerived fall back to Random, and the sentence the warning uses. |
| `ShouldReportHostOwnedAutoRegister(Mode, bAutoRegister, Role)`, `DescribeHostElectionState(bHostKnown, bLocalClientIsHost)` | The two authoring diagnostics: a Dynamic host-owned entity ends `BeginPlay` sending nothing, and what this client currently knows about the host. |
| `GetClassID()` | The class hash carried on spawn events. |
`InitIdentity`, `AssignPooledIdentity`, `ClearIdentity`, `MarkPooledDormant`, and `IsPooledDormant` are the injection points the entity subsystem and the actor pool use; game code has no reason to call them.
## Gotchas
- `OnCrowdyOwnershipAssigned` is announced on the tick after registration, deliberately, so a bind in the actor's own `BeginPlay` is not too late.
- A Dynamic entity with `Ownership = Host` and `bAutoRegister` on leaves `BeginPlay` sending nothing: auto-register only starts the channel for an entity this client owns outright. The log says so once per component.
- `DestroyEntity()` exists in three places with three signatures and one behaviour. Use whichever you already hold a reference for.
- The enum getters return the setting, not a live derivation. `GetRole()` is the live answer.
## Related
- [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md): what the settings mean.
- [Entities and spawning](./entities-and-spawning.md): the subsystem's spawn, destroy, and lookup calls.
- [Ownership transfer](./ownership-transfer.md): the static nodes that replace calling the two receivers.
- [Crowdy State](./crowdy-state.md): the properties `MarkStateDirty` serves.
- [Continuous state](./continuous-state.md): `Mode`, `StateExecutor`, and `bAutoRegister`.
# RPC Events in C++
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/rpc-events-cpp
# RPC Events in C++
A CrowdyEvent is a `UFUNCTION` you call like a normal method. The SDK marshals the arguments by reflection, routes them over the transport, and runs the matching `_Implementation` on the receivers the recipient names. No payload struct, no base class, no manual send call.
## When to use one
For a moment, not a value: a flicker, a swing, a chat line, a one-shot trigger. Anything a late joiner must still see is a [Crowdy State](./crowdy-state.md) property; anything a client could gain by lying about belongs on the [truth plane](../concepts/two-planes.md), in your server code.
## The two halves
**The receiver** is a reflected `UFUNCTION` named `_Implementation`, declared by you, tagged `meta=(CrowdyEvent)` plus the routing keys. It is what runs on every recipient, the caller included when the recipient says so.
**The call site** is `CROWDY_EVENT()`, a macro placed in the class body with no trailing semicolon. It generates a template `(Args...)` that forwards to `FCrowdyRPC::SendChecked`, which checks the argument count and each argument's convertibility against the receiver's declared parameters at compile time, marshals them, and routes the call.
Both halves work on any actor or component that carries a `UCrowdyEntityComponent`, and on a [replicated subsystem](./replicated-subsystems.md).
:::warning[The receiver must be a literal UFUNCTION you write by hand, never one emitted from inside another macro.]
UnrealHeaderTool does not expand macros when it scans for reflected declarations. A `UFUNCTION` produced by a macro is invisible to reflection, so `CROWDY_EVENT` generates only the non-reflected thunk and the routing lives on the receiver's metadata.
:::
The lantern's second event is `Sparkle`: the owner announces a one-time ignition sparkle at `BeginPlay`, everyone in range tints the light and reads a set of spark offsets. The four blocks below are facets of that one member. `FLanternSparkleStyle`, the struct the receiver takes, is declared under [Parameters](#parameters) below.
```cpp
// Lantern.h
// The receiver runs on every client in range; Sparkle() is the call that sends it.
UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "SpatialMulticast"))
void Sparkle_Implementation(const FLanternSparkleStyle& Style, const TArray& SparkOffsets);
CROWDY_EVENT(Sparkle)
```
The call, inside `ALantern::BeginPlay` after `Super::BeginPlay()`, behind the owner check that every `SpatialMulticast` send needs:
```cpp
// Lantern.cpp
// The owner announces its ignition sparkle to everyone nearby.
if (CrowdyEntity->IsLocallyOwned())
{
FLanternSparkleStyle Style;
Style.Color = FLinearColor::Yellow;
Style.Intensity = 8000.f;
TArray Offsets = { FVector(0.f, 0.f, 40.f), FVector(0.f, 0.f, 80.f) };
Sparkle(Style, Offsets);
}
```
## Routing keys
The meta keys are read from the receiver at scan time and baked into a registry, so a packaged build never reads metadata at runtime.
| Meta key | Value | Default when omitted |
|---|---|---|
| `CrowdyEvent` | none | Required. Marks the function as a receiver. |
| `CrowdyRecipient` | An `ECrowdyEventRecipient` enumerator name: `SpatialMulticast`, `Multicast`, `OwningClient`, `Host` | `SpatialMulticast` |
| `CrowdyDecay` | An `ECrowdyDecayRate` name | `No_Decay`. Spatial only. |
| `CrowdyDistance` | An `ECrowdyReplicationDistance` name | `Eight_Chunks`. Spatial only. |
| `CrowdyChannel` | A channel name | Empty, the app's session channel. Multicast only. |
Who each recipient reaches, and over which transport, is on [Recipients and routing](./recipients-and-routing.md). One more key, `CrowdyAction`, declares that the parameters describe a one-shot action; it changes nothing for an actor that runs the body itself and matters only to a backend that holds the entity as data.
## Parameters
`FCrowdyRPC::IsSupportedParamType` decides what may ride the wire. A parameter may be:
- a primitive (`bool`, any integer width, `float`, `double`, a byte), an enum, an `FName`, an `FString`, or an `FText`;
- a `USTRUCT`;
- an object or class reference (`UObject*`, `TSubclassOf`, and their soft forms), resolved by identity on the receiver;
- a `TArray` of any of the above, object references included;
- a `TSet` or `TMap` whose element, key, and value are not object references.
Not supported: a return value, a non-const output reference, a delegate, an interface, a `TSet` or `TMap` of object references, and a container buried inside a struct parameter at any depth a real struct reaches (`FCrowdyRPC::StructTransitivelyContainsContainer` is the check; it stops at eight levels). A `const TArray&` is an input and is fine.
:::warning[An unsupported signature is not a compile error. The event is dropped at scan time and every call to it goes nowhere.]
The startup scan runs `FCrowdyRPC::DescribeSignatureProblem` on each receiver and logs an error of the shape `[CrowdyAutoRegistry] CrowdyEvent 'ALantern::Sparkle_Implementation' parameter 'X' has unsupported type 'Y' ... not registered; calls to it will be dropped.` Grep the log for `not registered` when an event compiles but never arrives.
:::
A struct parameter travels as a value, so a small cosmetic struct is the natural way to pass several fields at once:
```cpp
// Lantern.h
// Cosmetic view data for the ignition sparkle; server-owned state never goes here.
USTRUCT()
struct FLanternSparkleStyle
{
GENERATED_BODY()
UPROPERTY() FLinearColor Color = FLinearColor::White;
UPROPERTY() float Intensity = 1.f;
};
```
### Containers and their bounds
A container parameter is decoded with a bound of 65536 elements; a count past that is treated as forged and the whole call is dropped on receipt. The struct-buried form is refused at scan time because that decode cannot be bounded.
```cpp
// Lantern.cpp
void ALantern::Sparkle_Implementation(const FLanternSparkleStyle& Style, const TArray& SparkOffsets)
{
Light->SetLightColor(Style.Color);
// One spark per offset in a real project; here each offset brightens the ignition flash.
Light->SetIntensity(Style.Intensity * SparkOffsets.Num());
}
```
:::warning[A Multicast call has a 1024-byte budget. A call that would exceed it is dropped loudly, never truncated.]
`CrowdyChannelPayloadMaxBytes` caps the encoded payload of a reliable (`Multicast`) call. A receiver whose fixed-width parameters alone can never fit is refused at registration with an error naming the byte count; a call whose actual encoded size exceeds the cap is dropped at send with `[CrowdyRPC] Reliable '' dropped ... over the 1024-byte channel limit`. A payload that might be large, such as a list of offsets, belongs on `SpatialMulticast`, which fragments across datagrams.
:::
### Object references
An object parameter is encoded by stable identity, tagged as one of three kinds: null, a tracked entity (addressed by its NetID), or an asset or class (addressed by its object path). On the receiver an entity resolves against the local registry and a path resolves against loaded objects. A runtime object that is neither a tracked entity nor an asset has no portable identity and is sent as null with a warning.
:::caution[A class or asset reference resolves by path on the receiver, and the receiver only finds what is already resident.]
By default an asset that is not loaded on the receiving client decodes to null with a warning naming it. `crowdy.rpc.allowObjectLoad 1` opts into loading it on demand; leave that off in a shipping build and make sure the asset is loaded on every client that can receive the call.
:::
## What happens on the wire
The call is packed into an `FCrowdyRpcCall`: the declaring class id, a function id hashed from the full signature (class path, name, and the ordered canonical parameter types), and the parameter bytes prefixed with a format version (`CrowdyRpcParamBlobVersion`). The receiving `UCrowdyEventRouter` looks the pair up in the registry `UCrowdyAutoRegistry` built at startup and invokes the `_Implementation` on the local instance of that entity. A signature that drifted between two builds, or a class that is not loaded, drops with `[CrowdyEventRouter] RPC dropped - no function for ClassID=... FunctionID=...`. A receiver that holds the entity as data rather than as an object reads the same parameters by name through `FCrowdyEventParams`, not through a live object. The Crowdy Mass Rendering Backend works this way, and runs your event's body on a stand-in; see [Run your code on a crowd row](../mass/run-your-code-on-a-crowd-row.md).
Tracing:
```text
crowdy.rpc.trace 1 every send and receive: function, entity, addressing, parameter bytes
crowdy.rpc.reliable.trace 1 the channel transport only
crowdy.rpc.loopback 1 deliver your own sends to your own receiver, for a single-client test
crowdy.rpc.dumpfn BP_Hero_C MyEvent print one function's routing info
```
## Gotchas
- `SpatialMulticast` runs the body on the caller too, so guard the call with `IsLocallyOwned()`; a proxy's copy of the trigger must not send a second one.
- A receiver declared with no `CrowdyRecipient` is `SpatialMulticast`. On a subsystem that default is rejected at send; see [Replicated subsystems](./replicated-subsystems.md).
- A `SpatialMulticast` reaches only clients with a fresh actor of their own on the map. A client whose pawn is not sending updates is at no position, so it receives none and its own sends reach nobody; see the [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity).
- A late joiner receives nothing from a past call. If it matters after the moment, it is state.
- Delivery is on the game thread, always. A handler may touch actors freely.
## Related
- [RPC events in Blueprint](./rpc-events-blueprint.md): the same system with a checkbox instead of a macro.
- [Recipients and routing](./recipients-and-routing.md): the four recipients, chunks, decay, and targeted sends.
- [Channels](./channels.md): where a `Multicast` call travels.
- [RPC types](../reference/rpc-types.md) and [RPC meta keys](../reference/rpc-meta-keys.md).
# RPC Events in Blueprint
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/rpc-events-blueprint
# RPC Events in Blueprint
A Blueprint sends and receives CrowdyEvents with no C++: tick one checkbox on a Custom Event, choose a recipient, and call the event by name. The body of the event is the receiver.
## When to use one
The same cases as [RPC events in C++](./rpc-events-cpp.md): a moment other clients should see, not a value a late joiner needs. The Blueprint needs a **Crowdy Entity Component** on the actor (or to be a [replicated subsystem](./replicated-subsystems.md)); a Custom Event on an actor without one has no entity to route by.
## Mark the event
Select the Custom Event node. Its Details panel gains a **Crowdy SDK** category with a **Crowdy Replicates** checkbox; this is the node's own Details panel, not the actor's.

Ticking it writes the `CrowdyReplicates` marker onto the event and clears any Unreal replication flag on the node, since the two cannot combine. A status line under the checkbox tells you at once whether the signature is accepted: green `Replicated (N parameters)`, or a red problem naming the parameter, using the same validator the C++ scan runs. Old assets marked with the earlier spelling `CrowdyReplicate` are still recognised.
:::warning[Crowdy Replicates cannot combine with Unreal's own replication, and the event must be Public.]
The status line reads `Disable Unreal replication - it cannot combine with Crowdy replication`, or `Set Access Specifier to Public - Crowdy Replicates cannot be private`, until you fix the node. A private or protected event is refused because the SDK invokes it from outside the class.
:::
:::caution[Toggling the checkbox needs a recompile before it takes effect.]
The compiler splices a dispatch gate into the head of a marked event at compile time. Until you press Compile the log says `Custom event 'X' Crowdy Replicates ... Recompile to activate.` and the event still runs locally only.
:::
The lantern's `Sparkle` event is the Blueprint twin of the C++ page's `Sparkle_Implementation`, with the style struct flattened into two inputs, `Color` and `Intensity`. Its body is the receiver: **Set Light Color** and **Set Intensity** on `Light`, each fed by a **Get Light** node. The graph below is the whole example; the Details panel of its event node is where Crowdy Replicates is ticked and Spatial Multicast is chosen.
[Blueprint snippet: rpc-mark-event, see /bp/unreal-sdk/rpc-mark-event.txt]
## Pick the recipient
Once Crowdy Replicates is ticked, three more rows appear. **Recipient** is the `ECrowdyEventRecipient` the C++ page uses, with the same default:

- **Spatial Multicast** (the default): everyone in range, over the chunk-based path, thinned by decay.
- **Multicast**: every member of the app's session channel, whatever the distance, over the channel transport.
- **Owning Client**: only the target entity's owner; another client's call becomes a request to that owner.
- **Host**: only the elected host; another client's call becomes a request to the host.
**Decay Rate** and **Replication Distance** sit below the recipient and are greyed out unless the recipient is Spatial Multicast, because only the spatial path reads them. **Channel** appears only for Multicast: type a channel name, or press **Pick** to choose from the app's channels, which the picker fetches live from Crowdy Studio when you are signed in there; free text always works. **Is A One-Shot Action** appears only once a map profile assigned in **Project Settings, Plugins, Crowdy SDK** (a **Map Profiles** entry or the **Default Profile**) selects a backend that draws crowd rows. A project that uses only the actor pool never sees it. See [Play poses and one-shot actions](../mass/play-poses-and-one-shot-actions.md).
:::warning[A channel you type here must already exist. Only the session channel is created for you.]
A typo, or a channel nobody created, logs a warning at connect time and every call to the event silently reaches nobody. See [Channels](./channels.md).
:::
:::caution[Host runs the call on whatever instance it holds, regardless of who owns that entity, and tells nobody else.]
There is no ownership gate on the host. If the rest of the session must see the outcome of a Host event, the host sends a second, Multicast or Spatial Multicast event with the result.
:::
## Call it
Call the event by name from any graph on the actor, exactly like a local Custom Event: drag off an exec pin, type `Sparkle`, and fill the `Color` and `Intensity` pins. There is no separate node kind for the call and none in the figure above; the call node is the ordinary one the editor offers for any custom event. The gate at the head of the event routes the call over the transport and skips the local body when this client is not a recipient; when it is, the body runs here as well.
For a Spatial Multicast event the owner runs the body itself and sends it, so the call sits behind an **Is Locally Owned** check the way the [Quickstart](../quickstart.md) does; a proxy's copy of the trigger must not send a second one.
## Receive it
There is no second node and no second graph. The same `Sparkle` body shown above is the receiver: on every recipient the SDK replays the call through the event, the gate recognises the replay and lets the body run, and `Color` and `Intensity` arrive on the event's own pins with the values the caller passed.
What the checkbox generates is a call to `UCrowdyReplicatedEventLibrary::CrowdyDispatchReplicatedEvent` at the top of the event. It is marked internal-use and never placed by hand; if a marked event ever loses its gate (a manual edit, a bad merge), calling it runs the body locally and announces nothing, which looks exactly like a transport drop. Recompile.
**Success signal.** With one client, `crowdy.rpc.loopback 1` and `crowdy.rpc.trace 1` show the send and the looped-back receive; the sparkle then comes from the receive rather than the local run.
## Gotchas
- The checkbox lives on the Custom Event node's Details, not the actor's.
- A red status line is the fastest signal that a pin type is rejected. Read it before compiling.
- The Channel field is join-only. Create the channel in Crowdy Studio or at runtime first.
- The Blueprint event body runs on the caller for Spatial Multicast and Multicast, and only where the recipient says for Owning Client and Host. Do not add a second local call.
- Cheat-sensitive data never rides a CrowdyEvent; it belongs on the [truth plane](../concepts/two-planes.md), in your server code.
## Related
- [RPC events in C++](./rpc-events-cpp.md): the parameter rules and the routing keys in full.
- [Recipients and routing](./recipients-and-routing.md): what a chunk is, decay, and targeted sends.
- [Channels](./channels.md): creating the channel a Multicast event names.
# Recipients and Routing
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/recipients-and-routing
# Recipients and Routing
Every CrowdyEvent names a recipient, and the recipient decides two things: who runs the body, and which transport carries it. This page is the delivery model behind both the [C++](./rpc-events-cpp.md) and the [Blueprint](./rpc-events-blueprint.md) authoring pages.
## The four recipients
`ECrowdyEventRecipient`, set with `CrowdyRecipient` in C++ or the Recipient dropdown in Blueprint. `SpatialMulticast` is the default when the key is omitted.
| Recipient | Travels over | Distance and decay | Who runs the body |
|---|---|---|---|
| `SpatialMulticast` (default) | The spatial path, addressed by the sender's chunk | Yes: `CrowdyDistance` and `CrowdyDecay` | Every client in range, the caller included |
| `Multicast` | A channel: the session channel, or the one `CrowdyChannel` names | No: every member, any distance, never thinned | Every channel member, the caller included |
| `OwningClient` | A targeted delivery to one client | No | Only the target entity's owner. A non-owner's call becomes a request to the owner; the owner's own call runs locally and sends nothing. |
| `Host` | A targeted delivery to one client | No | Only the elected host. A non-host's call becomes a request; the host's own call runs locally. The host runs it on whatever instance it holds, whoever owns that entity. |
The decision is a pure function of the recipient, the participant kind, and the local authority facts, so it is the same on every client and in every build.
"In range" is measured from the position each client's own actor updates registered, so a `SpatialMulticast` reaches only clients with a fresh actor on the map; a client whose pawn is not sending is nowhere and receives nothing. The [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity) makes the player's pawn that actor before it sends anything.
:::warning[On a replicated subsystem the default recipient is rejected. Set Multicast or Host explicitly.]
A subsystem has no world position, so a `SpatialMulticast` event declared on one has nowhere to be sent from. The send logs `uses SpatialMulticast, which has no location; set CrowdyRecipient=Multicast or Host. Dropping.` and consumes the call so a Blueprint caller does not run the body as a fallback. Since `SpatialMulticast` is what an unannotated event gets, every event on a subsystem needs the key written out. See [Replicated subsystems](./replicated-subsystems.md).
:::
## Spatial: chunks, distance, and decay
The world is divided into chunks of `ChunkSize` units on a side; a spatial event is sent from the sender's chunk and reaches every client whose actor stands within `CrowdyDistance` chunks of it. `ChunkSize` lives on the game session (`UCrowdyGameSession::GetChunkSize`, `SetChunkSize`) and defaults to 1600 units, so `ECrowdyReplicationDistance::Four_Chunks` is 6400 units at the default. A distance is a chunk count, not a world distance: changing the chunk size changes what every distance value means.
To see which chunk a point falls in, `UHelperFunctions::GetChunkCoordinateAtLocation(WorldContextObject, WorldLocation, ChunkX, ChunkY, ChunkZ)` (Blueprint: **Get Chunk Coordinate At Location**) reads the session's chunk size and returns the three `int64` coordinates; the older `GetChunkCoordinatesAtWorldLocation` without a world context is deprecated.
`ECrowdyReplicationDistance` runs from `None` through `One_Chunk` to `Eight_Chunks` (the default). `ECrowdyDecayRate` thins delivery with distance:
| Value | Effect |
|---|---|
| `No_Decay` (default) | Every client inside the distance receives the event. |
| `Exponential_Decay` | Delivery probability falls off exponentially with distance. |
| `Linear_50`, `Linear_25`, `Linear_10`, `Linear_5` | Linear falloff, from the mildest to the most aggressive. |
Decay is a server-side filter applied before the event reaches remote clients. Use it for high-rate cosmetic events where a distant client missing some of them is fine.
## Multicast: what reliable means
A `Multicast` event rides a [channel](./channels.md). "Reliable" here means coverage: every member of the channel is a recipient, at any distance, with no decay. It does not mean guaranteed delivery. The transport is still UDP with no acknowledgement, no retransmit, and no duplicate suppression.
:::warning[Reliable is coverage, not delivery. A late joiner gets nothing from a past Multicast call.]
Treat a Multicast event like any other fire-and-forget call for anything that must not be silently missed. If a value has to be right on a client that missed the event or joined afterwards, put it in a [Crowdy State](./crowdy-state.md) property, or in your server code if it is [truth](../concepts/two-planes.md); an event has no history to replay.
:::
The payload of a Multicast call is capped at 1024 bytes (see the [C++ page](./rpc-events-cpp.md#containers-and-their-bounds)); a large container belongs on `SpatialMulticast`.
## Targeted: OwningClient and Host
Both are delivered by the server as a single-actor message to exactly one client, never as a broadcast. On receipt an actor participant drops a broadcast that claims to carry an owner-only or host-only function, since a real one never arrives that way. On a subsystem there is no single-actor transport, so the same events legitimately arrive as channel broadcasts and are gated by identity on receipt.
:::caution[A Host send needs the host's own entity within the sender's known range.]
The targeted message is addressed to the host's avatar entity and the chunk it stands in. If this client holds no record of that entity, the send logs `Run-On-Host '' dropped ... the host's avatar is not in range` and the call is gone; it is not queued until the host comes into range.
:::
An `OwningClient` event aimed at a world entity nobody owns has no destination and drops; use `Host` for a world entity.
The lantern world uses `Host` for a relight: a client whose lantern has gone dark asks the elected host to relight it, and the host acts on whichever `ALantern` instance it holds. The lantern the host holds is a proxy of another client's entity, so setting `bLit` on it would stay on the host; the receiver marks the property dirty as well, which on an entity the host does not own is the [host push](./crowdy-state.md#on-an-entity-you-do-not-own-the-host-push) that makes the owner adopt the value. `Flicker` from the [Quickstart](../quickstart.md) is the `SpatialMulticast` row; the other two recipients are described in the table above and need no new member.
```cpp
// Lantern.h
UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "Host"))
void RequestRelight_Implementation();
CROWDY_EVENT(RequestRelight)
// Lantern.cpp
void ALantern::RequestRelight_Implementation()
{
Light->SetVisibility(true);
bLit = true;
// The host does not own this lantern; the mark is the host push that makes the owner adopt it.
CrowdyEntity->MarkStateDirty(TEXT("bLit"));
}
```
A `RequestRelight` custom event with Crowdy Replicates ticked and **Recipient** set to **Host**; its body is **Set Visibility** on `Light`, fed by **Get Light**. Call it from any client whose lantern is dark and it runs on the host alone. The figure is the visible half only. To make the owner adopt the value as the C++ does, add two nodes after it: **Set Lit** to true, then **Mark Crowdy State Dirty** with `Lit` as the property name; without them the relight stays on the host's proxy.
[Blueprint snippet: recipient-each, see /bp/unreal-sdk/recipient-each.txt]
## Receiving raw notifications in Blueprint
`UCrowdyBlueprintReceptionLayer` (**Crowdy Blueprint Reception Layer**) is a Blueprintable object that receives the wire-level notifications behind the systems on this page, for a game that wants to see them before the SDK's own routing does: subclass it in Blueprint, list the payload structs it claims in `SupportedEvents` (empty means unclaimed events only) and the actor update types in `SupportedActorUpdateTypes`, create it with **Create and Register Layer** (`CreateAndRegisterLayer(WorldContextObject, Class)`), and bind its two events. `OnEventNotificationReceived` (`FOnGameEventNotificationReceived`) delivers an `FGameEventNotificationBP` (the sender `UUID`, the chunk, the `Timestamp`, and the `Event` as an instanced struct); `OnActorUpdateReceived` (`FOnActorUpdateReceived`) delivers an `FActorUpdateNotificationBP` with the same envelope around an `ActorUpdate` struct. A game that uses CrowdyEvents and Crowdy State never needs one; it exists for custom payloads sent with `SendCrowdyEvent` and for diagnostics.
## Delivery is on the game thread
Every routing and dispatch entry point in `FCrowdyRPC` and `UCrowdyEventRouter` is game-thread only, and every handler runs there. A handler may spawn, destroy, and touch components without marshalling.
## Sending without an actor
Two lower-level entry points exist for callers the ordinary path cannot serve.
**A CrowdyEvent for an entity you hold only by identity.** `FCrowdyRPC::SendToTarget(World, Target, Fn, Frame)` sends a receiver function to an entity described by an `FCrowdyRpcTarget`: its `NetID`, an optional `Location`, its `OwnerID`, and this client's `Role` toward it. It routes by the function's own recipient, all four of them, and where this client already holds a registration for the entity the record's owner, role, and position win over the target's fields. It returns false only when there was no transport at all; every drop returns true, so a caller must not run a local body as a fallback for a refused send. Read the location from the entity's own live state, never from a caller's argument, because the location decides which region the message is addressed to. A plain `UObject` that stands for an entity with no actor can also implement `ICrowdyEventSource` and use the ordinary `CROWDY_EVENT` call; the send path asks the interface for the identity and the position it would otherwise read from an actor. The five overrides are `GetEventEntityID` (the entity's NetID), `GetEventLocation` (where it stands right now, or false when unknown), `GetEventOwnerID` (the owning player, invalid for a world entity), `GetEventRole`, and `IsEventLocallyOwned` (only an owner, or the host for a world entity, may originate). Answer for the entity you stand for, read live; never hand back a location a caller passed in, because the location decides which region the event is addressed to.
**A struct payload with no declared receiver.** `UCrowdyUtilities::SendCrowdyEvent(WorldContextObject, TargetEntity, Payload, Recipient, DecayRate, ReplicationDistance)` in C++, **Send Crowdy Event** (`K2_SendCrowdyEvent`, a wildcard struct pin) in Blueprint, broadcasts any `USTRUCT` at a target actor with an explicit recipient. It goes through `UCrowdyEntitySubsystem::DispatchGameEvent`, whose `ECrowdyTarget` (`Everyone`, `Entity`, `Owner`, `Host`, `AllExceptSender`) is the wire-level addressing the recipient enum maps onto; `DispatchGameEventAt` is the same send from an explicit location, `DispatchGameEventView` and `DispatchGameEventViewAt` borrow the payload instead of copying it, and `DispatchSingleActorMessage` and `DispatchSingleActorMessageTo` are the targeted delivery behind OwningClient and Host. A handler receives the struct, and may declare an `FCrowdyEventContext` as an optional second parameter to read the `SenderID`, the `TargetID`, the `Target`, and `bSentByLocalPlayer`. `UCrowdyEntityComponent::SendEvent(Payload, Scope)` is the same send addressed at the component's own entity, with `ECrowdyEventScope::Everyone` or `OwnerOnly`.
:::caution[The struct send has none of the compile-time checking a CrowdyEvent receiver gets.]
Any struct is accepted, there is no `_Implementation` naming convention to tie a sender to a receiver, and a mismatch is found at runtime or not at all. Prefer a `CrowdyEvent` unless you are building a system on top of the SDK.
:::
## Gotchas
- Omitting `CrowdyRecipient` means `SpatialMulticast`. Write the key out on anything that is not an actor.
- Decay and distance are read only for `SpatialMulticast`. Setting them on a `Multicast` event does nothing.
- `OwningClient` on a host-owned world entity drops. Use `Host`.
- The Host recipient has no ownership gate. The host may act on any entity it holds.
- Changing `ChunkSize` changes the meaning of every distance in the project.
## Related
- [RPC events in C++](./rpc-events-cpp.md) and [in Blueprint](./rpc-events-blueprint.md).
- [Channels](./channels.md): what the Multicast path rides, and the session channel.
- [Host authority](./host-authority.md): who the Host recipient reaches.
- [Replicated subsystems](./replicated-subsystems.md): the non-spatial participant.
- [Enums](../reference/enums.md).
# Crowdy State
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/crowdy-state
# Crowdy State
Mark a `UPROPERTY` with `meta=(CrowdyState)` and the client that owns the entity diffs it at the map's replication cadence and ships only the changed values to every proxy, which write them back onto their own copy of the actor. No wire struct, no executor, no manual send unless you ask for one. It replicates single values: POD leaves and `USTRUCT`s made of them, never containers or object references, and it says so once at startup for each property it drops.
## When to use it
For view state that has to be the same on every client, late joiners included: a lantern's lit flag, an animation stance, a cosmetic colour. It works in either entity mode, Static or Dynamic, and on a [replicated subsystem](./replicated-subsystems.md).
:::danger[Authoritative or cheat-sensitive state never belongs in Crowdy State.]
This is the view plane. The owner writes a value and every other client believes it; the host's precedence below is a convention the receiver honours, not a check. Hit points, currency, inventory: your server code, always. See [The Two Planes](../concepts/two-planes.md).
:::
## Prerequisites
The actor carries a `UCrowdyEntityComponent`, and the map profile's `bUseStateReplicator` is on (the default). A property the class declares is discovered at startup by the auto registry, which builds the class's replication layout once; the layout is baked into the [registry](../studio/inspector-and-registry.md) for packaged builds, so no metadata is read at runtime.
## The metadata keys
All on the `UPROPERTY`, all spelled exactly as shown.
| Key | Value | Effect |
|---|---|---|
| `CrowdyState` | none | The marker. The property replicates on the view plane. |
| `CrowdyOnRep` | a function name | A parameterless `UFUNCTION` run after the value is written, on the receiver and on the sender. Read the property for the new value; there is no previous-value argument. |
| `CrowdyOwnerOnly` | none | Delivered only to the owning client, over the targeted path, never on the spatial broadcast. Scope, not secrecy. |
| `CrowdyHeartbeat` | none | Opt in to the periodic keyframe: the property is re-sent every `StateKeyframeIntervalSeconds` even when unchanged, so a late or desynced observer converges. Off by default in C++; the Blueprint dropdown turns it on. |
| `CrowdyManualDirty` | none | Never auto-diffed. The value ships only when you mark it, with `MarkStateDirty`. For a property that is expensive to compare every tick. See [Marking state from outside the actor](#marking-state-from-outside-the-actor). |
:::warning[An enum without CrowdyHeartbeat can show a stale value to a late joiner.]
An enum is held state: sent on change, never re-sent. A client that starts observing after the last change, or that loses the one datagram carrying it, shows the wrong value until it changes again. The startup scan warns once per such property; `crowdy.state.heartbeat.advisories` lists every one it found. Add `CrowdyHeartbeat`, or tick Keyframe heartbeat in the variable's Crowdy Replication settings, unless the value is meant to be transient.
:::
## Mark a property
The Quickstart's `bLit` is the plain form: assign, and the change ships. This page adds a second property to `ALantern`, `TimesLit`, marked `CrowdyManualDirty`, so the count is pushed once per lighting instead of compared every tick. `RecordLighting` (a plain `void RecordLighting();` member you declare in the header) is called from the owner-gated overlap of the Quickstart's step 3; it increments the count and marks it, and every proxy's `OnRep_TimesLit` scales the light by the count.
```cpp
// Lantern.h
// A manual-dirty property ships only when you schedule it, not every tick.
UPROPERTY(meta = (CrowdyState, CrowdyManualDirty, CrowdyOnRep = "OnRep_TimesLit"))
int32 TimesLit = 0;
UFUNCTION()
void OnRep_TimesLit();
// Lantern.cpp
void ALantern::RecordLighting()
{
++TimesLit;
CrowdyEntity->MarkStateDirty(TEXT("TimesLit"));
}
void ALantern::OnRep_TimesLit()
{
Light->SetIntensity(2000.f * TimesLit);
}
```
Add an Integer variable `TimesLit`, set its **Replication** dropdown under **Crowdy Replication** to **Replicated**, and tick **Update manually** there (the other two toggles are **Keyframe heartbeat** and **Only send to owner**). From **Event ActorBeginOverlap**, the pure **Is Crowdy Entity Locally Controlled** feeds a **Branch**; on the true side **Get TimesLit** feeds a **+**, **Set TimesLit** stores the result, and the static **Mark Crowdy State Dirty** schedules it with `TimesLit` on the Property Name pin and `Target` left as self (the member form on the component would need a component getter as well). Seven nodes, one of them the owner gate the lantern's C++ overlap handler has: on a proxy the mark is not ignored, it becomes the [host push](#on-an-entity-you-do-not-own-the-host-push) described below.
[Blueprint snippet: state-mark, see /bp/unreal-sdk/state-mark.txt]
## The notify
`CrowdyOnRep` names a parameterless function, GAS-style. It fires wherever the value genuinely moved since the last send: on every receiver after the write, and on the sender itself, because a sent delta never comes back to its sender and the owner would otherwise never run it. A keyframe re-sending an unchanged value fires nothing, and a manual dirty mark on a value that did not move fires nothing either. Each slot fires at most once per tick, and a change the notify makes to its own property is folded into the sent baseline rather than re-sent.
The [Quickstart](../quickstart.md) lantern is the `state-onrep` example: `bLit` with `CrowdyOnRep = "OnRep_Lit"`, and a notify that sets the light's visibility.
```cpp
// Lantern.h
// View state: a wrong value is a visual glitch, not a cheat, so it belongs on the Crowdy State plane.
UPROPERTY(meta = (CrowdyState, CrowdyOnRep = "OnRep_Lit"))
bool bLit = true;
UFUNCTION()
void OnRep_Lit();
// Lantern.cpp
void ALantern::OnRep_Lit()
{
Light->SetVisibility(bLit);
}
```
Select the variable, set **Crowdy Replication** to **Replicated**, and a **RepNotify** field appears; the editor creates the `OnRep_` function. The variable's Get and Set nodes then carry the replication badge:

The body of the notify, drawn here as a custom event named `OnRep_Lit` because a generated graph cannot draw the editor-made function itself: **Get bLit** feeds **Set Visibility** on `Light`.
[Blueprint snippet: state-onrep, see /bp/unreal-sdk/state-onrep.txt]
:::caution[The notify runs on the sender too, at the next replication tick, and only on a real change.]
Write the notify as the one place the value is applied to the world, the way `OnRep_Lit` drives the light, and keep it idempotent. The Quickstart calls `OnRep_Lit` itself right after the assignment for an immediate local response; the replicator runs it once more on the tick that ships the change, which is harmless for a notify that only applies the current value and wrong for one that counts its own calls.
:::
## What ships, and when
Only the owning client diffs. A remote proxy never diffs; it applies. The cadence is the map profile's `ReplicationIntervalHz` (1 to 10, default 10), the same clock the continuous channel uses, though the two send independently. Each tick the replicator compares every auto-diffed property of every entity this client drives against a shadow copy of the last sent value and encodes the changed ones into one `FCrowdyStateDelta`; owner-only properties go out as a second, targeted delta. Every `StateKeyframeIntervalSeconds` (default 2, on the map profile; 0 turns it off) each entity also emits a keyframe carrying its `CrowdyHeartbeat` properties, unless the entity's `StateHeartbeat` is Off. A delta travels as far as the profile's `StateRelevanceDistance` (Four Chunks by default), which is tighter than the continuous channel.
A struct with a native net serializer (`FVector_NetQuantize`, `FRotator`) rides the engine's own quantized serializer automatically; a plain `FVector` stays exact. Quantization is chosen by type, not by a key. See [What a state property may be](#what-a-state-property-may-be).
Every delta carries a layout hash that folds each property's name and canonical type in declaration order. A receiver whose own hash for the class differs drops the delta cleanly rather than reading bytes by position, so adding, removing, reordering, or retyping a replicated property on one build and not another costs you deltas, not corruption. An enum property is carried as its underlying integer (blob version 2, `CrowdyStateBlobVersion`); a peer on the older format drops these deltas rather than misparsing them, so every client of an app runs one build.
## What a state property may be
You marked a property `CrowdyState` and nothing arrives, or you are about to mark one and want to know if it will. The classifier every surface shares, `FCrowdyStateLayoutBuilder::ClassifyStateProperty` (with `IsStateReplicatable` as the yes-or-no form and `DescribeStateSupport` for the sentence), decides at discovery; the Blueprint compiler's variable check and the variable-details dropdown reuse it, so every surface reports the same reason.
### Accepted
| Kind | Examples | Notes |
|---|---|---|
| Numeric | `int32`, `int64`, `uint8`, `float`, `double` | Diffed and sent as their bytes. |
| Boolean | `bool` | |
| Enum | `enum class E : uint8`, `TEnumAsByte` | Carried as the underlying integer. Consider `CrowdyHeartbeat`; see [The metadata keys](#the-metadata-keys). |
| Name and string | `FName`, `FString` | |
| Plain struct | any `USTRUCT` whose fields, at every depth, hold no container | One slot in the layout; diffed and sent as one unit. |
| Net-serialized struct | `FVector`, `FRotator`, `FVector_NetQuantize`, any struct with a native net serializer | Rides its own `NetSerializeItem`, quantized as the engine defines it, and exempt from the nested-container check because its serializer bounds the decode. |
### Rejected
:::warning[Containers and object references are rejected at discovery, in any position, including inside a struct. Use a CrowdyEvent, or your server code.]
The error names the escape hatch: `CrowdyState: property 'Players' on '/Script/MyGame.Lantern' is a container; CrowdyState does not replicate containers; use a CrowdyEvent RPC or a Server Object. Omitting it.` A list of who lit the lantern is a `TArray` parameter on a [CrowdyEvent](./rpc-events-cpp.md) if it is a moment, or a [Server Object](../exec/overview.md) if it is [truth](../concepts/two-planes.md). Never re-attempt it here.
:::
| Rejected | The reason discovery logs |
|---|---|
| `TArray`, `TSet`, `TMap` | `is a container; CrowdyState does not replicate containers; use a CrowdyEvent RPC or a Server Object` |
| A plain struct that holds a container at any depth | `is a USTRUCT that transitively contains a container (TArray/TSet/TMap); such nested containers are rejected because a forged element count would drive an unbounded allocation on decode` |
| A fixed-size C array (`float Values[4]`) | `is a fixed-size array; CrowdyState replicates only single-value POD and USTRUCT properties (its positional diff would miss changes past element 0)` |
| `UObject*`, `TSubclassOf`, soft object and class references, interfaces, delegates | `is an object/interface/delegate reference; CrowdyState replicates only POD and USTRUCT values` |
| Anything else, `FText` for one | `has an unsupported type 'X'; CrowdyState replicates only POD and USTRUCT values` |
A rejected property is omitted from the class's layout, so it never corrupts the positional wire order of the ones that were accepted; the rest of the class replicates normally. The line is an error and is not gated by `crowdy.state.trace`, so it is in the log whether or not tracing is on.
### Structs
A plain `USTRUCT` is a single slot. The whole struct is compared with the engine's `Identical` and, when any field differs, the whole struct is sent; on the receiver every field lands together, before the notify runs. That is the reason to use one: two fields that must always be seen together, a colour and an intensity, cannot arrive half-applied the way two separate properties can.
The lantern's glow is such a pair. `FLanternGlow` holds a colour and an intensity; `WarmGlow` on the owner changes both, and `OnRep_Glow` applies both to the light in one step.
```cpp
// Lantern.h
// A plain struct rides CrowdyState as one value; its fields always arrive together.
USTRUCT()
struct FLanternGlow
{
GENERATED_BODY()
UPROPERTY() FLinearColor Color = FLinearColor::White;
UPROPERTY() float Intensity = 1.f;
};
UPROPERTY(meta = (CrowdyState, CrowdyOnRep = "OnRep_Glow"))
FLanternGlow Glow;
UFUNCTION()
void OnRep_Glow();
// Lantern.cpp
void ALantern::WarmGlow()
{
Glow.Color = FLinearColor(1.f, 0.6f, 0.2f);
Glow.Intensity += 500.f;
}
void ALantern::OnRep_Glow()
{
Light->SetLightColor(Glow.Color);
Light->SetIntensity(Glow.Intensity);
}
```
A Blueprint variable of a C++ struct type replicates the same way: set its **Crowdy Replication** dropdown to **Replicated** and give it a RepNotify, exactly as for a scalar. Authoring a new struct type in Blueprint for this is unusual, so this section has no Blueprint graph of its own; the mechanics of marking and notifying are under [Mark a property](#mark-a-property) and [The notify](#the-notify).
:::caution[A struct diffs and ships as one unit.]
Changing one field of a five-field struct re-sends all five. Five scalar properties diff independently and each ships alone. Group fields into a struct when they must land together; keep them separate when they change at different times.
:::
### Where the list you wanted belongs
| You wanted | Put it on |
|---|---|
| The names of the players who lit the lantern this match | Your server code, if it is truth the server should hold; otherwise a `TArray` parameter on a `Multicast` CrowdyEvent when it changes. |
| A ring buffer of recent positions | The [continuous state](./continuous-state.md) snapshot, as fixed fields, or nothing: the proxy interpolates for you. |
| A reference to another actor | Its NetID as an `FGuid`, resolved with `FindEntity` on the receiver. |
| A list of active effects | Your server code. |
## Marking state from outside the actor
`UCrowdyStateBlueprintLibrary` has two static nodes that schedule a `CrowdyManualDirty` property to ship without first fetching the actor's Crowdy Entity Component: **Mark Crowdy State Dirty** and **Mark All Crowdy States Dirty**. Each resolves the target actor's component and forwards to its `MarkStateDirty` or `MarkAllStateDirty`. Use them whenever the caller does not already hold the component: a graph on another actor, a function library, a UI widget. From the actor's own graph either form works; the static one needs no component reference wired in.
### The two nodes
| Node | C++ | What it does |
|---|---|---|
| **Mark Crowdy State Dirty** | `UCrowdyStateBlueprintLibrary::MarkCrowdyStateDirty(Target, PropertyName)` | Schedules one manual-dirty property on the target's entity for the next replication tick. |
| **Mark All Crowdy States Dirty** | `UCrowdyStateBlueprintLibrary::MarkAllCrowdyStatesDirty(Target)` | Schedules every manual-dirty property on that entity. |
Both are `DefaultToSelf` on `Target`: from an actor's own graph you wire nothing and pick the property. From a component graph or a plain object graph, self is not an actor, so wire the actor in; the node shows a "Wire an Actor into Target" hint until you do.
Both are safe no-ops, never a crash, when the target is null, has no entity component, or the map runs no state replicator. On an entity this client does not drive the two differ: **Mark All Crowdy States Dirty** is ignored, while **Mark Crowdy State Dirty** is not a no-op at all; it becomes the host push described below. On an entity this client does drive, marking a property that is auto-diffed anyway is a no-op logged at verbose level as `ignored: not a manual-dirty property`, never an error.
The example is the same `TimesLit` member [Mark a property](#mark-a-property) added; this is the alternate call site, not a second gameplay moment.
```cpp
// Lantern.cpp
#include "Replication/State/CrowdyStateBlueprintLibrary.h"
void ALantern::PushTimesLit()
{
// Schedule the same property from anywhere, without fetching the component first.
UCrowdyStateBlueprintLibrary::MarkCrowdyStateDirty(this, TEXT("TimesLit"));
}
```
From **Event ActorBeginOverlap**, the pure **Is Crowdy Entity Locally Controlled** feeds a **Branch**, and the true side calls **Mark Crowdy State Dirty** with `Target` left unwired (self) and `TimesLit` picked from the Property Name dropdown. No component reference is needed. The branch is the owner gate the C++ caller has: on a proxy the same mark becomes the push described below, which is not what an overlap on a proxy means.
[Blueprint snippet: state-static-dirty, see /bp/unreal-sdk/state-static-dirty.txt]
### The property picker
The Property Name pin carries a dropdown, supplied in the editor by `FCrowdyStatePropertyPinFactory`, listing the resolved target class's properties that are marked `CrowdyManualDirty` and pass the type check. The dropdown is editor-only metadata scanning; the name you pick bakes into the compiled Blueprint as a literal `FName`, so a packaged build never reads metadata for this node.
:::note[If your property is not in the dropdown, it is either auto-diffed already or it failed the type check.]
An auto-diffed property does not need marking; assign it and it ships. A property that failed the check was omitted from the layout at startup with an error; see [What a state property may be](#what-a-state-property-may-be).
:::
### On an entity you do not own: the host push
**Mark Crowdy State Dirty** on an entity another client owns is not a no-op. It becomes a one-shot super-user push: the live value of the named property is read on the next replication tick and broadcast once, with no shadow and no ongoing tracking, stamped host-sourced when the caller is the elected host. It works for any Crowdy State property on that entity, not only the manual-dirty ones, because a host override may need to correct anything. An `OwnerOnly` entity refuses it at the source: the push is dropped before it is sent, and a receiver drops one that arrives anyway.
A host-owned world entity is the other case. On the host it is tracked with auto-diff off, so a plain `CrowdyState` property on it never ships on change; the host's mark works only for a `CrowdyManualDirty` property (the `CrowdyHeartbeat` keyframe still restates the rest). Mark a host-owned entity's changing properties manual-dirty, or give them `CrowdyHeartbeat`.
The receiver honours a push by the entity's `HostOverride` policy: `Allow` (the default) adopts a host-sourced value into the owner's baseline. A push from a client that is not the host is not stamped, so the real owner drops it, and every other proxy applies it, so the proxies disagree with the owner until the owner's next change. That is the hazard behind the owner gate above: a mark issued on a proxy by mistake is a push.
:::warning[A push on an entity you do not own is a live correction with no undo.]
The value on the calling client is broadcast as it stands. There is no shadow to revert to and no acknowledgement. Read the current value first, set what you mean, then mark.
:::
:::caution[Anyone can call this on any entity. Precedence is a convention, not enforcement.]
A modified client can stamp its own pushes host-sourced. This orders who wins on the view plane and nothing more; see [Host authority](./host-authority.md) and [The Two Planes](../concepts/two-planes.md).
:::
## A field that silently does nothing
The first thing to check. Discovery logs an unconditional error for every marked property it cannot carry, of the shape `CrowdyState: property 'X' on '/Script/MyGame.Lantern' is a container; CrowdyState does not replicate containers; use a CrowdyEvent RPC or a Server Object. Omitting it.` The reasons are a container, a struct that buries a container, a fixed-size array, an object or interface or delegate reference, or an otherwise unsupported type such as `FText`; [Rejected](#rejected) lists them. One more line to know: a property whose name and type also appear in the actor's continuous-state executor struct is dropped with `also lives in its executor state struct ... Dropping it from the CrowdyState layout`, so a field lives on exactly one channel.
These fire at class registration and are not gated by the trace variable. Grep the log for `CrowdyState: property`, which all three lines share, before turning anything on. When the property is in the layout but the change is not arriving, that is the other question:
```text
crowdy.state.trace 1 per delta: entity, changed-property count, byte size, spatial or owner-only or keyframe
crowdy.state.loopback 1 decode your own deltas onto a local mirror entity, for a single-client test
```
Two read-only diagnostics on `UCrowdyStateReplicator` answer the same questions from code or Blueprint: `IsStateReplicated(Actor)` is true when the actor's class carries at least one accepted property, whichever client drives it, and `GetLastSentStateBytes(Actor)` is the blob size this client last sent for the actor, or -1 when this client does not drive it. `crowdy.state.scopes` adds a CPU trace scope around each delta decode for profiling.
## Host precedence
When the local client is the elected host, every delta it sends is stamped host-sourced. A receiver that owns the entity adopts a host-sourced value into its own baseline instead of reverting it on the next diff, provided the entity's `HostOverride` is `Allow` (the default); `OwnerOnly` drops even a host correction. A host-owned world entity is driven by the host alone, and only by explicit pushes: its manual-dirty marks and its keyframe, never a background diff. See [Host authority](./host-authority.md).
:::warning[Host precedence is a convention with no server-side check. A forged host-sourced flag is honoured as real.]
It orders who wins on the view plane; it does not make either value trustworthy. If the value matters, it belongs in your server code and the question does not arise.
:::
## Gotchas
- Grep for `CrowdyState: property` first. A property that is not in the layout replicates nothing and says so once, at startup, as an error.
- The notify has no parameters and no old value. Keep the previous value yourself if you need it.
- A Blueprint variable that is both natively Replicated and Crowdy Replicated fails to compile. In C++, keep `Replicated` off a `CrowdyState` property.
- `CrowdyOwnerOnly` is delivery scope, not secrecy. The value still travels in the clear to the owner.
- `StateKeyframeIntervalSeconds` at 0 stops the baseline re-send only; on-change replication is unaffected.
- The loopback and trace variables are test aids. Leave them off in a normal session.
- `FText` is not accepted on this plane, though it is on a CrowdyEvent. Use an `FString` or an `FName`.
- A struct that is net-serialized is exempt from the nested-container check only because its serializer bounds the decode. A plain struct with a `TArray` inside is rejected however small the array.
- Quantization is decided by the struct's type. To ship a quantized position, declare `FVector_NetQuantize`, not `FVector`.
- `Target` on the static nodes defaults to self only where self is an actor. In a component or widget graph, wire the actor.
- The push is scheduled, not sent. It goes out on the next replication tick with the value the property holds then.
- Marking twice in one tick sends once.
- A host-owned world entity is written only through the host's manual-dirty marks and the keyframe; assigning a plain property on it and waiting does nothing. See [Host authority](./host-authority.md).
## Related
- [Continuous state](./continuous-state.md): the sibling channel for many fields that move together.
- [Map profiles](./map-profile.md): the cadence, the relevance distance, and the keyframe interval.
- [State meta keys](../reference/state-meta-keys.md).
- [RPC events in C++](./rpc-events-cpp.md): containers and object references as event parameters.
- [The Two Planes](../concepts/two-planes.md): what belongs on the truth plane instead.
- [Host authority](./host-authority.md): host-owned entities and the host override.
- [Ownership transfer](./ownership-transfer.md): making another client the owner instead of pushing over it.
# Continuous State
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/continuous-state
# 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](./rendering-backends.md). There is nothing to write for that: the SDK's default executor takes the snapshot for you.
:::note[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](./crowdy-state.md) properties: they diff per property and ship only what changed. The two are independent channels, not alternatives; an entity can use both.
:::warning[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](../quickstart.md): 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.
```cpp
// 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 Torch;
UPROPERTY(VisibleAnywhere, Category = "Crowdy")
TObjectPtr CrowdyEntity;
};
// LanternPlayer.cpp
#include "LanternPlayer.h"
ALanternPlayer::ALanternPlayer()
{
Torch = CreateDefaultSubobject(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(TEXT("CrowdyEntity"));
CrowdyEntity->Mode = ECrowdyEntityMode::Dynamic;
CrowdyEntity->IdentityPolicy = ECrowdyIdentityPolicy::PlayerDerived;
CrowdyEntity->Ownership = ECrowdyOwnership::LocalClient;
}
```
On the Crowdy Entity Component set **Mode** to **Dynamic** in the Details panel and leave **State Executor** empty. There is no graph to build: **Auto Register** joins the channel at BeginPlay.
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](./map-profile.md) before the executor.
:::caution[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](./ownership-transfer.md).
:::
## 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.
:::danger[Override the `_Implementation` functions, with `const`, never the bare event names.]
Both are `BlueprintNativeEvent`s, 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](./rendering-backends.md#the-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.
```cpp
// 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(TEXT("Executor"));
FInstancedStruct ULanternPlayerExecutor::GetActorState_Implementation(const UActorComponent* UpdateComponent) const
{
FLanternPlayerState State;
if (const ALanternPlayer* Player = UpdateComponent ? Cast(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();
}
```
Create a Blueprint with **Actor Update Executor** as its parent class. In **My Blueprint**, under Functions, press **Override** and pick the two functions:

**Get State Struct** returns the struct type; **Get Actor State** builds the snapshot with **Make InstancedStruct** from the owning actor's transform. Then set the character's **State Executor** to the new class in the Details panel. An override on an executor subclass is not an actor event graph, so the screenshot above is the reference.
:::caution[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](./map-profile.md) 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.
## Related
- [Quickstart](../quickstart.md): the player pawn as a Dynamic entity, spawned once connected, and your own reflection as the first thing you see.
- [Entity component](./entity-component.md): `Mode`, `StateExecutor`, `bAutoRegister`, `StartReplication`.
- [Rendering backends](./rendering-backends.md): the policy that moves a proxy from the snapshot.
- [Map profiles](./map-profile.md): the cadence and the keyframe and heartbeat intervals.
- [Connection and reconnect](./connection-and-reconnect.md): what `OnUDPConnectionSuccess` and `Connected` mean.
- [Crowdy State](./crowdy-state.md): the per-property alternative.
# Host Authority
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/host-authority
# Host Authority
One client of your app is the elected host, and the SDK tells every client who it is. Use it to decide who runs shared view-plane logic: who drives the world entities, who spawns the things that belong to nobody. Why election is a convention and not a security boundary is on [The Host Is a Convention](../concepts/host-is-a-convention.md); this page is the calls.
## Checking authority
`UCrowdyUtilities::GetCrowdyHasAuthority(WorldContextObject)` returns true when the local client is the host. It is a pure node, **Crowdy Has Authority**, in Blueprint; **Switch Crowdy Has Authority** (`CrowdyHasAuthority`) is the same answer as two exec pins. `CrowdyGetHostID` returns the host's id and whether one is set, and `CrowdyIsConnectedToServer` says whether there is a connection to ask through at all.
The lantern world gates a one-time action on it: at `BeginPlay` an `ALanternPost` turns its light up on the host alone, and a client that is not the host returns early and leaves host work to the host.
```cpp
// LanternPost.h
void RelightIfHost();
// LanternPost.cpp
#include "Utils/CrowdyUtilities.h"
void ALanternPost::RelightIfHost()
{
if (!UCrowdyUtilities::GetCrowdyHasAuthority(this))
{
return; // Not the host; the host's work replicates to us instead.
}
Light->SetIntensity(5000.f);
}
```
In the post's Blueprint, at **Event BeginPlay** the pure **Crowdy Has Authority** feeds a **Branch**; the true side calls **Set Intensity** on `Light` from a **Get Light**.
[Blueprint snippet: host-gate, see /bp/unreal-sdk/host-gate.txt]
:::danger[Host is a convention, not enforcement. Never gate cheat-sensitive state on it.]
Nothing on the wire is rejected for not coming from the host, and a modified client can claim to be it. Use the check to pick who runs shared logic; anything a player could gain by winning that check is decided by your server code, on the [truth plane](../concepts/two-planes.md).
:::
**Success signal.** With two clients, the post is bright on exactly one of them, the host. The effect is local by design: a post the whole village must see bright makes the intensity a [Crowdy State](./crowdy-state.md) property the host pushes. `crowdy.entity.trace 1` shows the election result on both clients.
## The host subsystem
`UCrowdyHostSubsystem` is a world subsystem that tracks the election result and broadcasts it. `IsHost()` is the same answer as `GetCrowdyHasAuthority`; `IsHostSet()` says whether an election result has arrived at all, so an early `IsHost()` false does not mean "someone else is host"; `GetHostID()` is the host's entity id, the id the host's own entities are owned under, and `GetHostUserID()` its raw user id, a different number. The member table, the thread-safety notes, and the server-validated check it carries are on [Host election](../services/host-election.md).
Bind `OnHostElected` (`FOnCrowdyHostElected`, `HostID`, `PreviousHostID`, on the game thread) rather than caching the answer at `BeginPlay`: the host changes when the current one leaves, and a host-owned entity's authority moves with it. A world subsystem of your own that lives next to it must null-check `GetGameInstance()` in `Initialize`; [Replicated subsystems](./replicated-subsystems.md#inherit-a-base) says why.
## World entities and the host override
A level-placed entity with `Ownership = ECrowdyOwnership::Host` is `HostOwned`: whichever client is host at the moment is its one authority, and its `IsLocallyOwned()` is true there alone. Its Crowdy State is written by the host through explicit pushes (`MarkStateDirty`, `MarkAllStateDirty`, or the [static nodes](./crowdy-state.md#the-two-nodes)) and the keyframe, never by a background diff, so an implicit and an explicit write cannot race. On a host change the entity is re-tracked by the new host without any code of yours.
A client-owned entity (`Ownership = LocalClient`) has a `HostOverride` (`ECrowdyHostOverride`): `Allow`, the default, lets the host correct its Crowdy State as a super-user and the owner adopts the value; `OwnerOnly` refuses even a host correction. This is precedence by convention. It orders who wins on the view plane; it does not make either value trustworthy.
Two more client-side helpers on `UCrowdyUtilities` answer ownership questions without a round trip: `IsCrowdyEntityHost(Entity)` is true when the entity is the host player's own entity (its NetID equals the host id), and `DoesCrowdyEntityOwn(OwnerActor, TargetActor)` is true when the first actor's player owns the second, resolving a host-owned side to the concrete host id on both ends. `IsCrowdyEntityLocallyControlled(Entity)` is the same question as the component's `IsLocallyOwned`, as a static.
## The server-validated check
For the moment a local answer is not enough, ask the server. `UCrowdyHostSubsystem::CheckEntityIsHost(Entity, Callback)` in C++, or the latent **Is Crowdy Entity Host (Server)** node in Blueprint, asks the server whether an entity's owner is the elected host and answers Is Host, Is Not Host, or Failed, where Failed means unknown, never a confirmed no. The lantern world uses it where a wrong answer costs something: a claim on a lantern post is granted only after a definite server yes. The check, its pins, its failure cases, and that example in both languages are on [Host election](../services/host-election.md#the-server-validated-check).
The server-validated check is still not enforcement. It tells you the current truth about who the host is; what you do with the answer runs on an unenforced plane.
## Gotchas
- `IsHostSet()` before `IsHost()`. Until an election result arrives, every client reads false.
- `GetHostID()` and `GetHostUserID()` are different ids. Compare `GetOwnerID()` with the first only for an entity a player spawned; a host-owned entity carries no owner id at all, so ask `GetRole() == ECrowdyRole::HostOwned`, `IsLocallyOwned()`, or `DoesCrowdyEntityOwn` instead.
- A host-owned entity is written by explicit pushes. Assigning a property on it and waiting does nothing. A [replicated subsystem](./replicated-subsystems.md) the host owns is the exception: it is auto-diffed.
- `HostOverride` is a view-plane tie-breaker. If the value matters, it belongs in your server code.
- The server check needs the target to exist on the server. A freshly spawned, never-updated entity answers Failed.
## Related
- [The Host Is a Convention](../concepts/host-is-a-convention.md): election as a convention, and the two checks compared.
- [Host election](../services/host-election.md): the host subsystem and the server-validated check in full, from the service side.
- [Ownership transfer](./ownership-transfer.md): the grant flow the server check feeds.
- [Entity component](./entity-component.md): `Ownership`, `HostOverride`, and `IsLocallyOwned`.
- [Crowdy State](./crowdy-state.md#host-precedence): what a host-sourced delta does on the receiver.
# Ownership Transfer
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/ownership-transfer
# Ownership Transfer
An entity spawned at runtime belongs to its spawner and a level-placed one to whoever its `Ownership` says. Ownership transfer moves it: a client asks, the entity's current authority grants or ignores, and the grant lands on every client. `UCrowdyOwnershipTransfer` is a static Blueprint library with the four verbs.
## When to use it
A player picks up a lantern post that belonged to the level, a vehicle changes driver, a world creature becomes a player's pet. Anything where "who simulates this" should change hands without destroying and respawning.
:::warning[Ownership is a coordination convention on the view plane, not enforcement, and a transfer is transient.]
No server checks who may request or grant. And a client that first observes the entity after a grant sees the spawn-time owner, or re-baselines from proxy defaults, not necessarily the transferred one. Cheat-sensitive ownership belongs on the [truth plane](../concepts/two-planes.md), in your server code.
:::
## The flow
1. A client that does not own the entity calls **Request Ownership Transfer**. The request travels as a Multicast `CrowdyEvent` on the session channel (`UCrowdyEntityComponent::RequestOwnership_Implementation`), so every client sees it, and only the entity's current authority reacts: the owning client, or the host for a host-owned world entity.
2. On the authority, either `bAutoApproveOwnershipRequests` on the component is on and the grant happens at once, or `UCrowdyEntitySubsystem::OnOwnershipRequested` fires for game code to decide.
3. Game code approves by calling one of the three grant nodes, or ignores the request. There is no deny node; silence is the rejection.
4. The grant travels the same way (`GrantOwnership_Implementation`), every client re-points the entity's record through `UCrowdyEntitySubsystem::ReassignOwnership`, and `OnEntityOwnershipChanged` fires everywhere once ownership has actually moved.
## The four nodes
| Node | C++ | Who calls it |
|---|---|---|
| **Request Ownership Transfer** | `RequestOwnershipTransfer(WorldContextObject, TargetEntity)` | Any client that does not own the entity. A no-op when the target is not a registered entity or this client is already its authority. |
| **Grant Ownership Transfer** | `GrantOwnershipTransfer(WorldContextObject, TargetEntity, NewOwner)` | The current authority. `NewOwner` is any entity the new owner's player owns, typically their avatar. |
| **Grant Ownership Transfer To Player** | `GrantOwnershipTransferToPlayer(WorldContextObject, TargetEntity, NewOwnerPlayerID)` | The current authority, with the requester's id straight from `OnOwnershipRequested`; no local actor of theirs is needed. |
| **Grant Ownership To Host** | `GrantOwnershipToHost(WorldContextObject, TargetEntity)` | The current authority, to make the entity host-owned again. The one verb an actor reference cannot express, because the host is not an actor. |
Every node resolves the target's entity component and subsystem and is a safe no-op, never a crash, on a null or unregistered target. All three grants funnel through the component's C++ `GrantOwnershipTo(NewOwnerID)`, which is also a no-op with a warning unless this client is the current authority.
:::caution[Grant Ownership Transfer refuses a NewOwner that is not client-owned. It does not fall back to the host.]
Passing a host-owned or unregistered actor logs a warning and does nothing, and the log tells you to use **Grant Ownership To Host** instead. The choice between a player and the host is explicit.
:::
The lantern world's transfer is a player claiming a lantern post. The post is level-placed and host-owned; the player asks for it, and the host approves.
The request: the post's overlap, when this client does not own the post, calls `TryClaim`:
```cpp
// LanternPost.h
// A player who walks into a post they do not own asks to carry it.
virtual void NotifyActorBeginOverlap(AActor* OtherActor) override;
void TryClaim();
// LanternPost.cpp
#include "Replication/Ownership/CrowdyOwnershipTransfer.h"
void ALanternPost::NotifyActorBeginOverlap(AActor* OtherActor)
{
Super::NotifyActorBeginOverlap(OtherActor);
if (!CrowdyEntity->IsLocallyOwned())
{
TryClaim();
}
}
void ALanternPost::TryClaim()
{
UCrowdyOwnershipTransfer::RequestOwnershipTransfer(this, this);
}
```
The grant, on whichever client is the authority: `HandleOwnershipRequested`, bound to `OnOwnershipRequested` in `BeginPlay`, ignores requests for other entities and hands the requester's id to `VerifyThenGrant`, which grants behind the server-validated host check on [Host authority](./host-authority.md#the-server-validated-check):
```cpp
// LanternPost.h
UFUNCTION()
void HandleOwnershipRequested(AActor* TargetEntity, AActor* RequesterActor, const FGuid& RequesterID);
// LanternPost.cpp
#include "Replication/Subsystems/CrowdyEntitySubsystem.h"
GetWorld()->GetSubsystem()->OnOwnershipRequested.AddDynamic(this, &ALanternPost::HandleOwnershipRequested);
void ALanternPost::HandleOwnershipRequested(AActor* TargetEntity, AActor* RequesterActor, const FGuid& RequesterID)
{
if (TargetEntity != this)
{
return;
}
VerifyThenGrant(RequesterID);
}
```
From **Event ActorBeginOverlap**, **Request Ownership Transfer** with `Target Entity` wired to a Self reference; the "not already mine" check the C++ makes is a no-op inside the node anyway, since a request for an entity you already own does nothing:
[Blueprint snippet: own-request, see /bp/unreal-sdk/own-request.txt]
The grant: at **Event BeginPlay**, a **Crowdy Entity Subsystem** getter feeds **Bind Event to On Ownership Requested**, whose custom event `OnClaimRequested` compares its `Target Entity` with a Self reference (**==** into a **Branch**, because the event fires for every entity this client is the authority of) and, when they match, passes its `Requester ID` to **Grant Ownership Transfer To Player** with `Target Entity` wired to self. Eight nodes; the compare is the guard the C++ has. The [server-checked version](./host-authority.md#the-server-validated-check) puts the host check between the event and the grant:
[Blueprint snippet: own-grant, see /bp/unreal-sdk/own-grant.txt]
**Success signal.** `OnEntityOwnershipChanged` fires on both clients with the new owner's id, the post's `IsLocallyOwned()` turns true on the requester and false on the host, and `OnCrowdyOwnershipAssigned` on the post's component fires with `bIsLocallyOwned` matching.
## The two events
On `UCrowdyEntitySubsystem`, both Blueprint-assignable, both on the game thread:
| Event | Parameters | Fires |
|---|---|---|
| `OnOwnershipRequested` (`FOnCrowdyOwnershipRequested`) | `TargetEntity`, `RequesterActor`, `RequesterID` | On the entity's current authority only, when another client requests it. `RequesterActor` is the requester's avatar when this client holds it, else null; `RequesterID` is always valid. |
| `OnEntityOwnershipChanged` (`FOnCrowdyEntityOwnershipChanged`) | `TargetEntity`, `NetID`, `NewOwnerID`, `PreviousOwnerID` | On every client once ownership has actually changed. `NewOwnerID` is invalid when the result is host-owned. |
`bAutoApproveOwnershipRequests` is authored config on the component, read on whichever client is the authority when a request arrives. Changing it at runtime on one client affects only the requests that client receives while it holds authority.
## What happens to in-flight state
The entity record is the single source of truth for both planes, so re-pointing its owner is the whole transfer: RPC routing and Crowdy State tracking follow. The old owner stops sending the moment it relinquishes; the new owner builds a fresh shadow of the entity's properties and starts diffing; bystanders are untouched. The actor's NetID never changes, so every address stays valid.
:::caution[There is a brief window with no writer.]
Between the old owner relinquishing and the reliable grant landing on the new owner, nobody diffs the entity. There is never a double writer, but for that short window there is no writer at all, by design. A Dynamic entity's continuous channel moves with the authority the same way: the new owner joins it and the old one leaves.
:::
A grant carries the previous owner's id, and `ReassignOwnership` compares and swaps on it, so a stale or duplicate grant is dropped rather than applied twice.
## Gotchas
- Silence is the rejection. If you want the requester to know, send them an `OwningClient` event yourself.
- The request short-circuits on the component's `IsLocallyOwned()`, so the host of a host-owned entity cannot accidentally request its own world entity into personal ownership.
- Granting to a player id with no local presence orphans the entity until it is granted again. Check the requester is still in the session before you grant.
- A new owner that never observed the entity re-baselines from proxy defaults. The durable truth lives in your server code, not on the view plane.
- Two Multicast events on the component carry all of this. Never give them a `CrowdyChannel`; see [Entity component](./entity-component.md).
## Related
- [Host authority](./host-authority.md): the host as the authority for a world entity, and the server-validated check before a grant.
- [Entity component](./entity-component.md): `bAutoApproveOwnershipRequests`, `OnCrowdyOwnershipAssigned`, and the two receivers.
- [Crowdy State](./crowdy-state.md): the shadow and the diff the transfer restarts.
- [Entities, Identity, and Ownership](../concepts/entities-identity-ownership.md).
# Replicated Subsystems
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/replicated-subsystems
# Replicated Subsystems
A UE subsystem is not an actor, but it can take part in both view-plane mechanisms: `meta=(CrowdyState)` properties diff and replicate on it exactly as on an actor, and `meta=(CrowdyEvent)` functions ride the reliable channel. Two abstract bases do the enrollment; a function library exists for the cases they do not cover.
One difference from a host-owned world actor, whose Crowdy State moves only on explicit marks and the keyframe: a subsystem is auto-diffed on the host, so assign the property and the change ships. Its deltas ride the reliable channel with no spatial filter.
## When to use one
For a world-level value that belongs to nobody in particular and every client should agree on: a village clock, a weather phase, a match state on the view plane. A subsystem is host-owned, a singleton every client resolves by class path with no handshake, so it is the natural home for "the one of these".
:::danger[Host-owned only, on the view plane. A replicated subsystem is not where a score lives.]
Host precedence is a convention here as everywhere on this plane. A match score, a winner, anything a client could gain by lying about, belongs on the [truth plane](../concepts/two-planes.md); a subsystem is for the shared cosmetic state around it.
:::
## Inherit a base
| Base | Extends | Enrolls |
|---|---|---|
| `UCrowdyReplicatedWorldSubsystem` | `UWorldSubsystem` | Once, in its own `Initialize`; unenrolls in `Deinitialize`. Created only in Play in Editor and game worlds. |
| `UCrowdyReplicatedGameInstanceSubsystem` | `UGameInstanceSubsystem` | Per world: into each new world's registry when that world's actors are initialized, out again as the world is cleaned up. It outlives worlds, and the entity registry it enrolls into is per world. |
Both have one protected hook, `GetReplicatedOwnership()`, which returns `ECrowdyOwnership::Host` and is the only supported answer for a singleton: a per-owner proxy makes no sense for a class every client holds exactly once.
The lantern village's night clock is a world subsystem: `ULanternNightSubsystem` holds one Crowdy State property, `NightPhase`, marked `CrowdyHeartbeat` so a late joiner converges, with a notify, `OnRep_NightPhase`, that sets every lantern's intensity from the phase, one CrowdyEvent, `AnnounceDawn`, a Multicast that resets the phase on every client, and a `BlueprintCallable`, `AdvanceNight`, the host calls to step it.
```cpp
// LanternNight.h
#include "Replication/RPC/CrowdyEvent.h"
#include "Replication/Subsystems/CrowdyReplicatedSubsystem.h"
#include "LanternNight.generated.h"
// A world subsystem replicates both planes at once: a CrowdyState property and a CrowdyEvent.
UCLASS()
class ULanternNightSubsystem : public UCrowdyReplicatedWorldSubsystem
{
GENERATED_BODY()
public:
UPROPERTY(meta = (CrowdyState, CrowdyHeartbeat, CrowdyOnRep = "OnRep_NightPhase"))
uint8 NightPhase = 0;
UFUNCTION()
void OnRep_NightPhase();
UFUNCTION(meta = (CrowdyEvent, CrowdyRecipient = "Multicast"))
void AnnounceDawn_Implementation();
CROWDY_EVENT(AnnounceDawn)
UFUNCTION(BlueprintCallable, Category = "Lantern")
void AdvanceNight();
};
// LanternNight.cpp
#include "LanternNight.h"
#include "EngineUtils.h"
#include "Lantern.h"
void ULanternNightSubsystem::OnRep_NightPhase()
{
for (TActorIterator It(GetWorld()); It; ++It)
{
It->Light->SetIntensity(NightPhase * 1000.f);
}
}
void ULanternNightSubsystem::AnnounceDawn_Implementation()
{
NightPhase = 0;
OnRep_NightPhase();
}
void ULanternNightSubsystem::AdvanceNight()
{
++NightPhase;
OnRep_NightPhase();
}
```
The game-instance twin is `ULanternCalendarSubsystem` on the other base: a `DayCount` declared the same way, which survives travel between village levels and needs no override of `Initialize` at all. The block is the declaration only; a notify and a caller are yours to add:
```cpp
// LanternNight.h
// A game instance subsystem outlives worlds, so a day count survives travel between village levels.
UCLASS()
class ULanternCalendarSubsystem : public UCrowdyReplicatedGameInstanceSubsystem
{
GENERATED_BODY()
public:
UPROPERTY(meta = (CrowdyState, CrowdyHeartbeat))
int32 DayCount = 0;
};
```
A replicated subsystem is defined in C++; a Blueprint may not subclass one. Once the class exists, its Blueprint surface appears by itself: a **Get Lantern Night Subsystem** getter node (the editor offers one for every `BlueprintType` world or game-instance subsystem), and off it every `BlueprintCallable` and `BlueprintPure` the class declares, `Advance Night` here. A `CrowdyState` property with `BlueprintReadWrite` is readable the same way. There is no pasted graph on this page because a graph would name your class, not one the site can ship; the shape is Event BeginPlay, the getter, the call, behind a **Crowdy Has Authority** branch (the [host-gate figure](./host-authority.md#checking-authority)), since only the host's write to a host-owned subsystem ships.
:::warning[If you override Initialize or Deinitialize, call Super. Enrollment lives inside the base's override.]
There is no separate hook and no runtime check. A subclass that overrides `Initialize` without calling `Super::Initialize` is never enrolled, and nothing it replicates ever leaves the machine.
:::
:::danger[A world subsystem must null-check GetGameInstance() in Initialize.]
Any further work you add to `Initialize` after `Super::Initialize` runs on a transient game world the engine creates at startup, whose game instance is set only after the world exists. `GetGameInstance()` returns null there, and reaching through it crashes with an access violation on launch that Play in Editor never reproduces. The SDK's own world subsystems guard for it; so must yours.
:::
**Success signal.** On the host, call `AdvanceNight` and every client's `OnRep_NightPhase` runs at the next tick; `crowdy.state.trace 1` logs the delta. `AnnounceDawn` shows up in `crowdy.rpc.trace 1` as a reliable send over the session channel.
## What a subsystem cannot do
Two consequences of having no actor.
:::warning[Every CrowdyEvent on a subsystem needs an explicit CrowdyRecipient of Multicast or Host.]
The default recipient, `SpatialMulticast`, needs a world position to send from. A subsystem has none, so the send is rejected with `uses SpatialMulticast, which has no location; set CrowdyRecipient=Multicast or Host. Dropping.` `OwningClient` also works, delivered over the channel and gated on receipt, but on a host-owned singleton the owner is the host, so `Host` says what you mean.
:::
- A `CrowdyOwnerOnly` property never transmits on a subsystem: the targeted path addresses an actor's owner, and a host-owned singleton has none. The replicator logs `is non-spatial; owner-only propert(ies) not emitted` once and moves on.
- There is no spawn event and no `InitialState`. A subsystem's first values on a late joiner come from its class defaults and then the keyframe, which is why `CrowdyHeartbeat` belongs on every property a newcomer must see.
## The library, for cases the bases do not cover
`UCrowdyReplicatedSubsystemLibrary` is what the bases call. **Register Replicated Subsystem** (`RegisterReplicatedSubsystem(Subsystem, Ownership)`, `Subsystem` defaults to self) resolves the entity subsystem from the caller's world and enrolls it, returning the participant's NetID or an invalid `FGuid` on failure; **Unregister Replicated Subsystem** reverses it. In a world with no entity subsystem, which is any editor world, unregister is silent and register logs a warning and enrolls nothing.
:::caution[Do not call Register Replicated Subsystem from a game-instance subsystem.]
Its world is the ambiguous "current" world. Inherit `UCrowdyReplicatedGameInstanceSubsystem`, which enrolls per world, instead.
:::
Underneath both is the two-call pattern on `UCrowdyEntitySubsystem`, `RegisterParticipant(Participant, Ownership)` and `UnregisterParticipant(Participant)`, which any `UObject` may use. If you write it by hand in a world subsystem's `Initialize`, the order below is not optional.
:::warning[Initialize both dependencies before enrolling: the entity subsystem, then the state replicator.]
`Collection.InitializeDependency(UCrowdyEntitySubsystem::StaticClass())` and then `Collection.InitializeDependency(UCrowdyStateReplicator::StaticClass())`, before the register call. The replicator binds the registration event in its own `Initialize`; enroll first and the broadcast fires before anyone is listening, the subsystem is never tracked, and no later rescan recovers it. Depending on the entity subsystem alone does not pull the replicator in.
:::
## Gotchas
- Both bases are `Abstract`. Your concrete subclass is what the engine instantiates.
- A subsystem's NetID is the hash of its class path. Rename the class and every client agrees on the new id at the same time, because they all run one build.
- The world base is created only in PIE and game worlds. In an editor world it does not exist, and that is correct.
- The game-instance base ignores worlds owned by another game instance, so two PIE clients in one process each enroll their own.
- A subsystem's traffic shares the session channel with the two ownership-transfer events. Its reliable sends have the same 1024-byte payload cap as any [Multicast call](./rpc-events-cpp.md#containers-and-their-bounds).
## Related
- [Crowdy State](./crowdy-state.md): the properties a subsystem replicates.
- [RPC events in C++](./rpc-events-cpp.md): the events it declares.
- [Recipients and routing](./recipients-and-routing.md): why the default recipient is rejected here.
- [Channels](./channels.md): the transport a subsystem's deltas and events ride.
- [Host authority](./host-authority.md): who drives a host-owned subsystem.
# Rendering Backends
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/rendering-backends
# Rendering Backends
A rendering backend turns the [continuous state](./continuous-state.md) a remote entity's owner sends into something on screen for everyone else. The SDK ships two: the actor pool, which is the default, and the Crowdy Mass Rendering Backend, which draws remote entities as crowd rows (see [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md)). A project customises the actor pool through two policy classes, or replaces the backend by implementing four methods.
## When you touch this
When a Dynamic entity moves on its owner and not on anyone else, when you want your own state struct applied to a proxy, or when pooled actors of the entity's class are the wrong way to draw a crowd; see [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md). A Static entity never comes through here: its proxy is the actor its spawn event made, driven by Crowdy State and events, and a client that joined after the spawn builds it from a repeat of that event.
## The default: the actor pool
`FCrowdyActorManagementConfigStruct::BackendClass` on the map profile defaults to `UCrowdyActorPoolBackend`, so a profile that never chose a backend gets this one. It uses `UCrowdyActorPoolSubsystem` to keep a pool of pre-spawned actors per entity class, checks one out when a remote entity appears, registers it in the entity registry so entity-targeted events reach it like any actor, and hands every update to a `UCrowdyRepApplicationPolicy`: the shipped `UCrowdyTransformRepPolicy` unless you provide one.

The screenshot is the SDK's shipped default profile, and its **Backend Config** reads None: the backend runs on a built-in config in that case.
:::note[No Backend Config means the built-in one: the actor pool with the transform policy.]
When `BackendConfig` is empty, `UCrowdyActorPoolBackend::InitializeBackend` creates a transient **Actor Pool Backend Config** and, since it names no policy, instantiates `UCrowdyTransformRepPolicy`: it reads the default executor's `FCrowdyActorState` and sets the pooled actor's location and rotation, interpolated across a ring of recent samples with bounded extrapolation. A `BackendConfig` of another backend's class is refused: the backend logs `Backend Config is a , but this backend needs a CrowdyActorPoolBackendConfig` and returns false, the actor manager logs `could not initialize, so no remote entity will be drawn on this map`, and remote entities arriving as continuous-state updates are tracked with nothing on screen. Spawn-event proxies and Crowdy State are unaffected either way. See [Map profiles](./map-profile.md).
The fallback arrived in 2.15.0. On 2.14.0 and earlier an empty Backend Config, or one with no Replication Policy Class, tracks remote entities and draws nothing, and the fix there is an **Actor Pool Backend Config** naming `UCrowdyTransformRepPolicy`. See [What's Changed](../guides/whats-changed.md#2026-09-22-sdk-v2150).
:::
### Configure it
`UCrowdyActorPoolBackendConfig`, set as the profile's `BackendConfig`:
| Field | Default | Effect |
|---|---|---|
| `ReplicationPolicyClass` | none | Your `UCrowdyRepApplicationPolicy` subclass. Unset means `UCrowdyTransformRepPolicy`, which moves the proxy from `FCrowdyActorState`; set it when your executor sends more than the transform, and move the proxy yourself in it. |
| `PoolPolicyClass` | none, optional | A `UCrowdyActorPoolPolicy` subclass. Unset means the concrete base, which already hides pooled actors, shows them on activation, and strips proxy movement. |
| `DefaultPoolSizePerClass` | 8 | Pools are created lazily per entity class with this many actors made up front. |
| `PerClassPoolOverrides` | empty | A per-class up-front size; a class listed here is also pre-warmed at map load. |
| `MaxPoolSizePerClass` | 256 | How far a class's pool may grow. When every actor is in use, the pool spawns one more, up to this cap (never below the up-front size). |
| `bRenderLocalOwner` | off | Also draw a pooled copy of entities this client owns, driven by the server's echo of its own updates. Off, the owner sees only its real actor. Turn it on to watch the round trip on a single client. |
When an entity's spawn event already made an actor of the entity's class, the pool draws that actor and takes nothing from the pool, so what `OnCrowdySpawned` set on it stays. When ownership of a drawn entity moves to this client, the pool lets go of the actor, which this client now moves; when it moves away, the pool draws this client's actor from the new owner's updates.
A pool that is full grows by one actor at a time, spawned during play with collision off until it is activated. At the cap, a new entity of that class waits: it is activated as soon as another entity of the class leaves, or, if its spawn event already made an actor for it, that actor is kept and drawn instead of being replaced. Nothing is dropped. The cap is there so a flood of made-up entity ids from a misbehaving peer cannot make your client spawn without limit; raise it if you legitimately expect more than 256 live remote entities of one class.
:::caution[A custom executor needs a matching policy. The default policy reads FCrowdyActorState and refuses anything else.]
`UCrowdyTransformRepPolicy` skips an update whose struct is not `FCrowdyActorState`, so a Dynamic entity whose executor sends its own struct stands still until its profile names a policy that reads it. `UCrowdyRepApplicationPolicy` has two pure virtuals and requires a subclass; `UCrowdyActorPoolPolicy` ships full bodies and is meant to be instantiated as is.
:::
### The replication application policy
The policy is what moves a remote player's proxy. Continuous state is how pawns, players, and AI move, so a policy's first job is to take the location and rotation out of each update and put the proxy where the owner was at `RenderTimeMs`. The shipped `UCrowdyTransformRepPolicy` does exactly that for `FCrowdyActorState`, and for a pawn that only moves it is all you need. You write your own only when your executor sends more than the transform, and then you apply the transform yourself first and your extra fields after, because a custom policy replaces the shipped one rather than adding to it: `UCrowdyActorPoolBackend::ResolvePolicyClass` instantiates the config's class instead of `UCrowdyTransformRepPolicy`, so a policy that never sets location and rotation leaves every proxy under it standing still.
`UCrowdyRepApplicationPolicy` reads a state struct out of each update and applies it to the pooled actor each frame:
| Override | Called | What you do |
|---|---|---|
| `ExtractFields(State, ServerTimestampMs, SlotId)` | Per update, per slot | Read your struct out of `State` into your own per-slot buffers. Return false when the struct type is wrong and the update is skipped. |
| `ApplyToActor(Actor, SlotId, RenderTimeMs)` | Per frame, per active slot | Apply the buffered state, interpolated to `RenderTimeMs`, to the actor. |
| `OnInstanceActivated(Actor, InitialState)` | Once, when a slot activates | A `BlueprintNativeEvent`: set initial state on the actor. |
| `OnInstanceDeactivated(SlotId)` | When the entity leaves | Clean up per-slot state. |
| `GetExpectedSlotCount()` | At startup | How many slots to pre-allocate; 64 by default. Match your expected player count. |
#### The sample ring
Both the shipped policy and the example below keep their per-slot history in `TInterpolatedField`, a small ring buffer the SDK ships in `Data/TInterpolatedField.h`, 32 samples deep by default. `TInterpolatedField::Push(Value, TimestampMs)` stores one sample with the server timestamp the update carried. `TInterpolatedField::Sample(RenderTimeMs, LerpFn)` finds the two samples that bracket the render time by binary search and blends between them with the function you pass (`FMath::Lerp` for a location; a quaternion slerp for a rotation). When the render time runs past the newest sample, `Sample` extrapolates from the last two by calling the same function with an alpha above 1, for at most 0.2 seconds beyond the newest; after that it holds the value extrapolated to 0.2 seconds. The step is measured over at least the average gap of the last four samples (50 ms when only two are held), so two updates that arrive almost together after a sender hitch extrapolate at the sender's normal pace instead of throwing the proxy far ahead for a frame. A render time older than the oldest sample holds the oldest sample; a slot is in that state only briefly after activation, until the render time, which runs one interpolation delay behind, catches up with the samples already held. A location follows that extrapolation in full; the rotation slerp clamps its alpha to 1.2, so a rotation extrapolates by at most a fifth of a step. With one sample it returns that sample; with none, the default you pass. `PhysicalIndex` is the ring's own index arithmetic, nothing you call.
The reason a proxy built on it does not jitter is the render time itself: the actor manager renders at the estimated server time minus the interpolation delay (100 ms), so the proxy is always drawn a little behind the newest sample, between two samples it already holds, and never snaps to the latest one as it lands. Extrapolation only runs when an update is late, and its cap keeps a lost stream from sliding a proxy across the map.
#### The example: the same shape, with your own field added
`ULanternRepPolicy` is what the shipped policy does, plus one field. `FLanternPlayerState`, the struct the [continuous state](./continuous-state.md) page's executor sends for `ALanternPlayer`, carries `Location`, `Rotation`, and `bTorchLit`. The policy keeps one `TInterpolatedField` and one `TInterpolatedField` per slot: `ExtractFields` refuses any other struct type and pushes the location and rotation with the update's server timestamp; `ApplyToActor` samples both at `RenderTimeMs`, calls `SetActorLocationAndRotation` on the proxy, and only then applies the torch flag to the proxy's `Torch` light. The flag is applied as the latest value: a boolean is not something to blend. `OnInstanceDeactivated` resets the slot so a reused slot never starts from a departed player's history. The policy header includes `LanternPlayer.h` for the struct, so this snippet depends on the [`executor-override` block](./continuous-state.md#a-custom-executor) being in your project first.
```cpp
// LanternRepPolicy.h
#include "Data/CrowdyRepApplicationPolicy.h"
#include "Data/TInterpolatedField.h"
#include "LanternPlayer.h"
#include "LanternRepPolicy.generated.h"
// Replaces the shipped transform policy, so it moves the proxy itself and then applies the one field of its own.
UCLASS()
class ULanternRepPolicy : public UCrowdyRepApplicationPolicy
{
GENERATED_BODY()
public:
virtual bool ExtractFields(const FInstancedStruct& State, int64 ServerTimestampMs, int32 SlotId) override;
virtual void ApplyToActor(AActor* Actor, int32 SlotId, int64 RenderTimeMs) override;
virtual void OnInstanceDeactivated(int32 SlotId) override;
private:
TArray> Positions;
TArray> Rotations;
TArray bTorchLitBySlot;
void EnsureSlot(int32 SlotId);
};
// LanternRepPolicy.cpp
#include "LanternRepPolicy.h"
#include "Components/PointLightComponent.h"
bool ULanternRepPolicy::ExtractFields(const FInstancedStruct& State, int64 ServerTimestampMs, int32 SlotId)
{
const FLanternPlayerState* Player = State.GetPtr();
if (!Player)
{
return false;
}
// Each slot keeps a ring of timestamped samples; Sample reads them back at whatever render time the pool asks for.
EnsureSlot(SlotId);
Positions[SlotId].Push(Player->Location, ServerTimestampMs);
Rotations[SlotId].Push(Player->Rotation, ServerTimestampMs);
bTorchLitBySlot[SlotId] = Player->bTorchLit;
return true;
}
void ULanternRepPolicy::EnsureSlot(int32 SlotId)
{
if (SlotId >= Positions.Num())
{
Positions.SetNum(SlotId + 1);
Rotations.SetNum(SlotId + 1);
bTorchLitBySlot.SetNumZeroed(SlotId + 1);
}
}
```
```cpp
// LanternRepPolicy.cpp
void ULanternRepPolicy::ApplyToActor(AActor* Actor, int32 SlotId, int64 RenderTimeMs)
{
if (!Positions.IsValidIndex(SlotId))
{
return;
}
const FVector Location = Positions[SlotId].Sample(RenderTimeMs, [](const FVector& A, const FVector& B, float T) { return FMath::Lerp(A, B, T); });
const FRotator Rotation = Rotations[SlotId].Sample(RenderTimeMs, [](const FRotator& A, const FRotator& B, float T)
{
return FQuat::Slerp(A.Quaternion(), B.Quaternion(), FMath::Clamp(T, 0.f, 1.2f)).GetNormalized().Rotator();
});
// The transform first, then the field of your own: the proxy's torch follows the owner's.
Actor->SetActorLocationAndRotation(Location, Rotation);
if (const ALanternPlayer* Player = Cast(Actor))
{
Player->Torch->SetVisibility(bTorchLitBySlot[SlotId]);
}
}
void ULanternRepPolicy::OnInstanceDeactivated(int32 SlotId)
{
if (!Positions.IsValidIndex(SlotId))
{
return;
}
Positions[SlotId] = {};
Rotations[SlotId] = {};
bTorchLitBySlot[SlotId] = false;
}
```
`ExtractFields` and `ApplyToActor` are C++ only, so the policy class is C++. Set it as **Replication Policy Class** on the Actor Pool Backend Config in the map profile's Details panel. `OnInstanceActivated` is the one hook a Blueprint subclass of the policy may implement.
### The pool policy
`UCrowdyActorPoolPolicy` decides what a pooled actor does at three moments, each a `BlueprintNativeEvent` a Blueprint subclass can implement without C++: `OnActorPooled(Actor)` once per actor right after it is spawned into the pool, `OnActorActivated(Actor, InitialState)` when it is checked out (its `InitialState` is empty in the shipped pool, and the identity lands on the component right after it returns), and `OnActorDeactivated(Actor)` when it goes back. The base hides and shows the actor and disables proxy movement; subclass only to add per-actor behaviour at those moments.
Pooled actors are genuinely spawned (`SpawnActorDeferred` and `FinishSpawning`), so their constructors and Blueprint-added components run. Before `BeginPlay` the pool marks any entity component on them dormant, so a pre-warmed actor registers no entity and no listener while it stands for nothing. `UCrowdyActorPoolSubsystem::RegisterPool(FCrowdyPoolConfig)` (`ActorClass`, `PoolPolicyClass`, `PoolSize`, `MaxPoolSize`) is what the backend calls to create a pool, a no-op for a class that already has one, and `HasPool(Class)` says whether one exists; `AcquireActor` checks an actor out, growing the pool up to `MaxPoolSize` when every actor is in use, and `ReleaseActor` checks it back in. `crowdy.pool.trace 1` logs spawn, release, reuse, and growth.
## What a pooled proxy does and does not do
:::warning[A pooled Dynamic proxy never sees the spawn event's InitialState.]
The spawn event creates a proxy actor and broadcasts `OnCrowdySpawned` with the payload; the actor pool then treats that actor as an orphan, destroys it, and swaps in a pre-warmed pooled actor that never received it. Anything a proxy needs to look right must be a class default or a field of the replicated state struct, never a one-time spawn value.
:::
:::warning[The owner of a client-spawned Dynamic entity sees two visuals.]
The pool is fed only by inbound network state, so the owner ends up with the local actor it drives directly and the pooled proxy the server echoes back, interpolation-delayed. There is no per-entity flag to choose. Hide one side yourself, in the entity's own code: `SetActorHiddenInGame` on the local actor when `IsLocallyOwned()` if the round-tripped proxy is the one that should show. With **Enable Owner Tracking** on (the default) the local player's own avatar is echoed and drawn the same way; turning it off suppresses that one echo and no other owned entity's.
:::
- A proxy's `GetNetID()` and `GetRole()` work: the backend assigns the identity to the pooled actor's component when it checks it out. `RemoteProxy` is the role it reports.
## Swapping at runtime
`UCrowdyActorManager::SetBackend(NewBackend)` (Blueprint: **Set Backend**) swaps the active backend: the previous one is deinitialized, slot state is preserved, and the new one receives `ApplyInterpolation` from the next tick. The new backend must already be initialized, and `InitializeBackend` is C++ only, so this is a C++ operation in practice. `GetActiveBackend()` is the live backend, null when none initialized; a backend that refused to initialize is absent here and present in the profile, which is how to tell the two apart from code.
## Writing a backend
Subclass `UCrowdyRenderingBackend` and pair it with a `UCrowdyRenderingBackendConfig` subclass carrying whatever your backend needs (an instanced mesh, a pool size). Four overrides are required and three optional:
| Override | Required | Contract |
|---|---|---|
| `ActivateInstance(SlotId, UUID, EntityClass, InitialState)` | yes | A remote instance became visible. Acquire the rendering resource for `SlotId`. `EntityClass` is never null. |
| `DeactivateInstance(SlotId, UUID)` | yes | It left. Release the resource and the per-slot state. |
| `ExtractUpdate(State, ServerTimestampMs, SlotId)` | yes | An update arrived. Parse the struct into your interpolation buffers. |
| `ApplyInterpolation(SlotId, RenderTimeMs)` | yes | Apply the interpolated state; `RenderTimeMs` is already offset by the interpolation delay. |
| `InitializeBackend(World, Config)` | no | Acquire subsystems, read the config. Return false, after logging the reason and the remedy, when you cannot draw with what you were given; the manager refuses you rather than installing you. |
| `DeinitializeBackend()` | no | Release what you held. |
| `IsInstanceActive(SlotId)` | no | Whether `SlotId` holds something drawn. Return false when `ActivateInstance` could not acquire a resource (a full pool), and the actor manager calls `ActivateInstance` again once capacity may have freed or the entity registers. The default, true, never retries. |
`DrawsEntitiesAsCrowdRows()` is one more, for a backend that represents entities without spawning an actor of the entity's class; the editor reads it off the class default object to decide whether to show authoring surfaces that only make sense on that representation. The default is false. The Crowdy Mass Rendering Backend that ships in the SDK is the one backend that returns true; choose it as the map profile's **Backend Class** and see [Turn Mass on for a map](../mass/turn-mass-on-for-a-map.md).
`SlotId` is a stable index the actor manager assigns for the instance's lifetime; keep your per-instance state in arrays indexed by it.
:::danger[A custom backend must never subscribe to the actor tracker itself.]
The actor manager is already the single caller of `ActivateInstance` and `DeactivateInstance`. A backend that also subscribes to `UCrowdyActorTracker` activates every entity twice and spawns everything double. The four overrides are your only entry points.
:::
## Gotchas
- `BackendClass` alone draws the default executor's movement through `UCrowdyTransformRepPolicy`. A custom state struct needs `BackendConfig` with a `ReplicationPolicyClass` that reads it, and that policy replaces the shipped one: apply `Location` and `Rotation` yourself before anything else, or every proxy stands still.
- A policy built on `TInterpolatedField` is the smooth choice for anything that moves. Sample it at `RenderTimeMs`, never at the newest sample: the delay is what hides the network.
- `bUseCrowdyActorTracker` off on the profile means no tracker, no manager, and no backend at all.
- A pooled proxy's spawn-time look comes from class defaults or the state struct, never the spawn payload.
- A pool at its cap warns once per class, naming the class and `MaxPoolSizePerClass`; each entity it could not draw is a `crowdy.pool.trace` line, not a warning, since it is retried. Raise `MaxPoolSizePerClass` if you see the warning. On 2.16.0 and earlier, a pool never grew past `DefaultPoolSizePerClass` (8), every entity past that was dropped with a warning and never retried, and its spawn-event actor was destroyed first; see [What's Changed](../guides/whats-changed.md#2026-09-26-sdk-v2170).
- A class that arrives over the wire before it is loaded cannot be drawn; see Preloaded Entity Classes on [Entities and spawning](./entities-and-spawning.md).
## Related
- [Map profiles](./map-profile.md): where `BackendClass` and `BackendConfig` live.
- [Draw a crowd with Mass](../mass/draw-a-crowd-with-mass.md): the Crowdy Mass Rendering Backend, which draws remote entities as crowd rows.
- [Continuous state](./continuous-state.md): the executor that produces what the policy consumes.
- [Entities and spawning](./entities-and-spawning.md): the actor tracker and the spawn event.
- [Entity component](./entity-component.md): the identity a pooled actor is given.
# Channels
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/channels
# Channels
A channel is a named message group within one app: create it, join it, publish to it, and every member receives the message wherever they stand. It is also the transport a `Multicast` CrowdyEvent and a [replicated subsystem](./replicated-subsystems.md)'s state deltas ride, and every client joins one channel, the session channel, whether or not it ever creates another.
`UCrowdyChannels` is a game-instance subsystem. Every server call on it is asynchronous and answers through exactly one of two delegates, success or `FOnChannelError`, including when the request never reaches the server. Publishing is not one of those calls: it goes out over UDP and is not acknowledged.
## When to use one
Guild chat, a party's coordination, a village-wide announcement, trade broadcasts: anything that must reach a group regardless of distance. For a moment that only nearby players need, a `SpatialMulticast` event is cheaper; for a value, [Crowdy State](./crowdy-state.md), or your server code if it is [truth](../concepts/two-planes.md).
## The session channel
Every app has one channel every client joins on connect, named `__crowdy_session_`, created on demand when it does not exist and the app's creation policy lets a member create one (otherwise create it once in Crowdy Studio, or the log warns that default-channel events will drop). It is an SDK-owned transport, not a place for your own messages. It carries every `Multicast` CrowdyEvent with no `CrowdyChannel` of its own and every replicated subsystem's Crowdy State delta.
:::warning[A message you publish on the session channel, or on any channel a Multicast event names, never reaches OnChannelMessageReceived.]
Every channel the connect-time bootstrap joins (the session channel and each channel a Multicast event names) and every channel you hand to `RegisterReliableRpcChannel` is an RPC transport: a payload arriving on it goes to the RPC decoder and is never broadcast to the game. Publish your own notices on a channel of your own, one channel per purpose.
:::
`GetSessionChannelId()` is its resolved id, 0 until joined or created, and `AreReliableChannelsReady()` turns true once the connect-time bootstrap has joined every channel the app's Multicast events name plus the session channel. Both are pure nodes you can poll.
:::warning[A channel a Multicast event names must already exist. Only the session channel is created for you.]
The bootstrap joins named channels; it never invents one, because a missing name is usually a typo. It logs `[CrowdyChannels] Multicast channel 'X' was not found for this app - RPCs targeting it will drop. Create it (or fix the name) in Crowdy Studio.` A channel you create at runtime after the bootstrap has run needs `RegisterReliableRpcChannel(ChannelId, Name)` once you have joined it, or events naming it still drop. Registering it makes it an RPC transport: raw `PublishChannelMessage` payloads on it are decoded as RPC frames and are not delivered to `OnChannelMessageReceived`, so keep one channel per purpose.
:::
## Create and join
`CreateChannel(Name, Description, MembershipPolicy, bMembersCanSend, OnSuccess, OnError)` creates one; the creator becomes its owner with the system `leader` role, which holds every permission including `send_messages`. `ECrowdyChannelMembershipPolicy` decides how others get in: `Open` (join at once), `Request` (a join request awaits approval), `Invite` (managers add members), `Admin` (app admins only). `bMembersCanSend` true creates a default `member` role granting `send_messages` to everyone who joins; false makes an announce-only channel where joiners receive but cannot post until given a role. The app's `FCrowdyChannelPolicy` (read with `GetChannelPolicy`) holds who may create channels at all (`ECrowdyChannelCreationPolicy`: `Admin`, `Member`, `Anyone`), the default membership policy, and the caps `MaxMembers` and `MaxChannelsPerUser` (0 means none); `SetChannelPolicy` sets the two policies, and the caps are set in Crowdy Studio.
The lantern village creates a `village` channel for lit notices once the connection is up: `CreateVillageChannel`, called from the game instance's connected handler (the create needs the game token), and `HandleVillageChannelCreated` stores the id behind `GetVillageChannelId`, the one place the id lives.
```cpp
// LanternGameInstance.h
#include "Queries/Data/Channels/Types/FCrowdyChannel.h"
UFUNCTION(BlueprintPure, Category = "Lantern")
int64 GetVillageChannelId() const { return VillageChannelId; }
private:
int64 VillageChannelId = 0;
void CreateVillageChannel();
UFUNCTION()
void HandleVillageChannelCreated(FCrowdyChannel Channel);
// LanternGameInstance.cpp
#include "Subsystem/CrowdyChannels.h"
void ULanternGameInstance::CreateVillageChannel()
{
// Called once connected (the create needs the game token); one client creates, the rest join.
UCrowdyChannels* Channels = GetSubsystem();
FOnChannelSuccess Created;
Created.BindDynamic(this, &ULanternGameInstance::HandleVillageChannelCreated);
Channels->CreateChannel(TEXT("village"), TEXT("Lantern lit notices"),
ECrowdyChannelMembershipPolicy::Open, true, Created, FOnChannelError());
}
void ULanternGameInstance::HandleVillageChannelCreated(FCrowdyChannel Channel)
{
VillageChannelId = Channel.ChannelId;
}
```
In the Game Mode Blueprint, at **Event BeginPlay** a **Crowdy SDK Subsystem** getter feeds **Bind Event to On UDP Connection Success**, whose custom event `OnConnected` calls **Create Channel**, a latent node with `Name`, `Description`, `Membership Policy`, and `Members Can Send` inputs and **On Success** (a `Channel` struct) and **On Error** pins. Binding to the connection is what keeps the create after sign-in; at BeginPlay there may be no token yet and the create would answer on **On Error**. The Game Mode belongs to one map, so keep the id in a variable on the Game Instance if the next map needs it. (Do not start from the Game Instance's **Event Init**: it runs before the SDK's subsystems exist, see [Authentication](../services/authentication.md#signing-in).) To keep the id, drag a **Break Crowdy Channel** off the `Channel` pin on the success branch and set an Integer64 variable `VillageChannelId` from `Channel Id`.
[Blueprint snippet: ch-create, see /bp/unreal-sdk/ch-create.txt]
Every connecting client running this asks the server to create the channel. A real game has one client create it, the host for instance, or calls `GetChannels` first and creates only on a miss; the server's creation policy is the real gate on who may.
`JoinChannel(ChannelId, ...)` joins an open channel and `RequestToJoinChannel` files a request on a `Request` channel; `LeaveChannel` leaves. The creator is already a member and needs neither, though the local cache (`HasCachedChannels`, `IsPlayerInChannel`) does not show the new channel until `GetMyChannels` has run.
## Ban a member
A manager removes a member for good with `BanChannelMember(ChannelId, UserId, Reason, OnSuccess, OnError)`. It takes the user out of the channel and refuses every later join, request to join and add of that user until a manager lifts the ban with `UnbanChannelMember(ChannelId, UserId, OnSuccess, OnError)`. The caller needs the manage-members permission, and the channel's owner and the caller themselves cannot be banned. A user who is not a member yet can be banned too, and leaving or being removed does not lift a ban.
`Reason` is optional and at most 500 characters. A longer one fails at once with a `PolicyViolation` error, and nothing is sent. Ban answers the member record, whose `Status` is `banned`; unban answers `bWasBanned`, which is false when the user had no ban.
The village's elder bans a member from the `village` channel. `VillageChannelId` is the id the game instance kept when it created the channel.
A Ban button on the member list calls `BanFromVillageChannel` with the member picked. `HandleChannelBanned` receives the member record, and `HandleChannelBanFailed` the error.
```cpp
// LanternGameInstance.h
#include "Queries/Data/Channels/Types/FCrowdyChannelMember.h"
#include "Queries/Data/Channels/Types/FCrowdyChannelError.h"
public:
// A Ban button on the member list calls this for the member picked.
void BanFromVillageChannel(int64 UserId, const FString& Reason);
UFUNCTION()
void HandleChannelBanned(FCrowdyChannelMember Member);
UFUNCTION()
void HandleChannelBanFailed(FCrowdyChannelError Error, FString Message);
// LanternGameInstance.cpp
#include "Subsystem/CrowdyChannels.h"
void ULanternGameInstance::BanFromVillageChannel(int64 UserId, const FString& Reason)
{
UCrowdyChannels* Channels = GetSubsystem();
FOnChannelMemberSuccess Banned;
Banned.BindDynamic(this, &ULanternGameInstance::HandleChannelBanned);
FOnChannelError Failed;
Failed.BindDynamic(this, &ULanternGameInstance::HandleChannelBanFailed);
// Needs the manage-members permission; the owner and yourself cannot be banned. A Reason over 500 characters fails before it is sent.
Channels->BanChannelMember(VillageChannelId, UserId, Reason, Banned, Failed);
}
void ULanternGameInstance::HandleChannelBanned(FCrowdyChannelMember Member)
{
UE_LOG(LogTemp, Log, TEXT("User %lld is banned from the village channel"), Member.UserId);
}
void ULanternGameInstance::HandleChannelBanFailed(FCrowdyChannelError Error, FString Message)
{
UE_LOG(LogTemp, Warning, TEXT("The ban failed: %s"), *Message);
}
```
**Ban Channel Member** is a latent node with `Channel Id`, `User Id` and `Reason` inputs. The figure bans the user that the `OnBanPressed` event passes in, for a fixed reason. **On Success** carries the member record; **On Error** carries `Error` and `Message`, and the figure opens `Error` with **Break Crowdy Channel Error** to print its `Code` and the `Message`.
[Blueprint snippet: ch-ban-node, see /bp/unreal-sdk/ch-ban-node.txt]
## Handle a ban on join
A banned user's `JoinChannel`, `RequestToJoinChannel`, or a manager's `AddChannelMember` for them, fails with the error code `Banned` in `ECrowdyChannelErrorCode`. The code comes from the server's own error code, not from the message text, so it is safe to branch on. A ban lasts until a manager lifts it, so do not retry: tell the player instead.
A Join the Village button calls `JoinVillageChannel`. Its failure handler checks the code first.
```cpp
// LanternGameInstance.h
#include "Queries/Data/Channels/Types/FCrowdyChannelError.h"
public:
// A Join the Village button calls this.
void JoinVillageChannel();
UFUNCTION()
void HandleVillageJoinFailed(FCrowdyChannelError Error, FString Message);
// LanternGameInstance.cpp
#include "Subsystem/CrowdyChannels.h"
void ULanternGameInstance::JoinVillageChannel()
{
FOnChannelError Failed;
Failed.BindDynamic(this, &ULanternGameInstance::HandleVillageJoinFailed);
GetSubsystem()->JoinChannel(VillageChannelId, FOnChannelMemberSuccess(), Failed);
}
void ULanternGameInstance::HandleVillageJoinFailed(FCrowdyChannelError Error, FString Message)
{
// A ban stays until a manager lifts it, so trying again is pointless; tell the player instead.
if (Error.Code == ECrowdyChannelErrorCode::Banned)
{
UE_LOG(LogTemp, Warning, TEXT("You are banned from the village channel: %s"), *Message);
return;
}
UE_LOG(LogTemp, Warning, TEXT("Could not join the village channel: %s"), *Message);
}
```
On the **Join Channel** node's **On Error** pin, open `Error` with **Break Crowdy Channel Error** and compare `Code` with **Banned** in a **Branch**; the figure prints the `Message` on the banned branch.
[Blueprint snippet: ch-banned-join, see /bp/unreal-sdk/ch-banned-join.txt]
## Publish and receive
`PublishChannelMessage(ChannelId, Payload)` sends raw bytes to every active member except the sender. The caller must already be a member holding `send_messages`; an open channel's default member role grants it. The server caps a channel message payload at 1024 bytes. `PublishChannelMessageAs(ChannelId, SenderUuid, Payload)` sends under a sender id you choose, as the Game Kit's chat does, since the session's own id is empty until the player's entity resolves; a receiver sees that id, which proves nothing. `CanPublishChannelMessage()` is true once the game session exists and the realtime connection is routing.
:::warning[Publishing is unacknowledged UDP: no ordering, no delivery guarantee, and the caller is never told if it was dropped.]
It is the right tool for a notice, not for a value. Put anything that must be right on every client in a state property, or in your server code if it is truth.
:::
The lantern's `PublishLit`, called from the owner-gated overlap, publishes a one-byte notice, its lit flag, on the village channel, reading the id from the game instance's `GetVillageChannelId`:
```cpp
// Lantern.cpp
#include "LanternGameInstance.h"
#include "Subsystem/CrowdyChannels.h"
void ALantern::PublishLit()
{
// Tell every village member the lantern changed, over the channel the game instance created.
const ULanternGameInstance* Game = GetGameInstance();
UCrowdyChannels* Channels = Game ? Game->GetSubsystem() : nullptr;
if (Channels)
{
Channels->PublishChannelMessage(Game->GetVillageChannelId(), { uint8(bLit) });
}
}
```
From **Event ActorBeginOverlap**, behind an **Is Crowdy Entity Locally Controlled** branch (the owner gate the C++ has): a **Crowdy Channels** subsystem getter feeds the `Target`, **Get VillageChannelId** (the Integer64 variable the create graph set) the `Channel Id`, and a one-element **Make Array** the `Payload` byte array of **Publish Channel Message**. The array's one element is the lit byte, 0 in the figure; wire your lit flag into it. It is a plain call with no result pins.
[Blueprint snippet: ch-publish, see /bp/unreal-sdk/ch-publish.txt]
`OnChannelMessageReceived` fires on the game thread for every delivery, with the channel id, the sender's `SenderUUID`, and the payload. The village's `HandleVillageNotice`, bound by `WatchVillageChannel` from `Init`, reads the one byte and sets every lantern's visibility from it; the check on the payload's length before the read is the validation the caution below asks for.
```cpp
// LanternGameInstance.h
void WatchVillageChannel();
UFUNCTION()
void HandleVillageNotice(int64 ChannelId, FString SenderUUID, const TArray& Payload);
// LanternGameInstance.cpp
#include "EngineUtils.h"
#include "Lantern.h"
#include "Subsystem/CrowdyChannels.h"
void ULanternGameInstance::WatchVillageChannel()
{
GetSubsystem()->OnChannelMessageReceived.AddDynamic(this, &ULanternGameInstance::HandleVillageNotice);
}
void ULanternGameInstance::HandleVillageNotice(int64 ChannelId, FString SenderUUID, const TArray& Payload)
{
// A channel message is not server-signed; validate it before acting. Here the one byte is the lit flag.
const bool bLit = Payload.Num() > 0 && Payload[0] != 0;
for (TActorIterator It(GetWorld()); It; ++It)
{
It->Light->SetVisibility(bLit);
}
}
```
There is no Blueprint figure for the receive side: `OnChannelMessageReceived` carries a byte-array `Payload`, and the generated graphs on this site cannot express a bound event with an array parameter. In your own Blueprint, drag off a **Crowdy Channels** subsystem getter, choose **Bind Event to On Channel Message Received**, and create the matching custom event from the `Event` pin; it receives `Channel Id`, `Sender UUID`, and `Payload` as an array of bytes.
:::caution[A channel message is not signed on the way down. Validate the payload before acting on it.]
The server does not sign the delivery, and any member with `send_messages` could have sent it. Treat the bytes as untrusted input: bound every length, clamp every value, and never let one decide an outcome that must hold against a modified client. The SDK does not filter payloads on a channel: every payload you are sent reaches your handler, whatever text it begins with.
:::
## The full surface
Cache and queries, on `UCrowdyChannels`: `HasCachedChannels` (has `GetMyChannels` ever completed), `GetCachedMyChannels` (the memberships, no round trip), `IsPlayerInChannel`, `GetMyChannelById`, `IsInAnyChannel`, `HasPermissionInChannel` (an `ECrowdyChannelPermission`: `SendMessages`, `ManageChannel`, `ManageMembers`, `ManageRoles`, `InviteMembers`), `GetMyChannels`, `GetChannel`, `GetChannels`, `GetChannelMembers`, `GetChannelRoles`, `GetChannelPolicy`, `GetPendingJoinRequests`. `OnMyChannelsCacheChanged` fires when the cached membership list changes.
Mutations: `CreateChannel`, `UpdateChannel`, `DeleteChannel`, `JoinChannel`, `RequestToJoinChannel`, `LeaveChannel`, `AddChannelMember`, `RemoveChannelMember`, `BanChannelMember`, `UnbanChannelMember`, `CreateChannelRole`, `UpdateChannelRole`, `DeleteChannelRole`, `SetChannelMemberRoles`, `SetChannelPolicy`.
Every query and mutation also exists as a latent Blueprint node with **On Success** and **On Error** pins, one class each: `UCrowdyChannels_GetMyChannels` (**Get My Channels**), `UCrowdyChannels_GetChannel`, `UCrowdyChannels_GetChannels` (**Get All Channels**), `UCrowdyChannels_GetChannelMembers`, `UCrowdyChannels_GetChannelRoles`, `UCrowdyChannels_GetChannelPolicy`, `UCrowdyChannels_GetPendingJoinRequests`, `UCrowdyChannels_CreateChannel`, `UCrowdyChannels_UpdateChannel`, `UCrowdyChannels_DeleteChannel`, `UCrowdyChannels_JoinChannel`, `UCrowdyChannels_RequestToJoinChannel` (**Request to Join Channel**), `UCrowdyChannels_LeaveChannel`, `UCrowdyChannels_AddChannelMember`, `UCrowdyChannels_RemoveChannelMember`, `UCrowdyChannels_BanChannelMember` (**Ban Channel Member**), `UCrowdyChannels_UnbanChannelMember` (**Unban Channel Member**), `UCrowdyChannels_SetChannelMemberRoles`, `UCrowdyChannels_CreateChannelRole`, `UCrowdyChannels_UpdateChannelRole`, `UCrowdyChannels_DeleteChannelRole`, `UCrowdyChannels_SetChannelPolicy`. A list result arrives wrapped (`FCrowdyChannelsResult`, `FCrowdyMyChannelsResult`, `FCrowdyChannelMembersResult`, `FCrowdyChannelRolesResult`) so a **Break** node unwraps it. `PublishChannelMessage` has no latent twin; it is a plain call on the subsystem.
**On Success** and **On Error** of a node share one set of data pins: the result (`Channel`, `Member`, `Channels`, and so on; a node that returns nothing has none), then `Error` and `Message`. On **On Success**, `Error` and `Message` are empty. On **On Error** the result is empty, `Error` is an `FCrowdyChannelError` (break it to read its `Code`) and `Message` is the text to show.
The types:
| Type | Holds |
|---|---|
| `FCrowdyChannel` | `ChannelId`, `AppId`, `Name`, `Description`, `OwnerUserId`, `MembershipPolicy`, `Status`, `CreatedAt`. |
| `FCrowdyChannelMember` | `ChannelMemberId`, `ChannelId`, `UserId`, `Status` (`active`; `pending` while a request waits; `banned` in the answer to a ban), `Roles` sorted by rank, `CreatedAt`. |
| `FCrowdyChannelMembership` | Your own membership of one channel: `Channel`, `Roles`, the effective `Permissions` across them, `JoinedAt`. |
| `FCrowdyChannelRole` | `ChannelRoleId`, `ChannelId`, `RoleName`, `Rank`, `bIsSystem`, `Permissions`, `CreatedAt`. Every channel has a non-deletable `leader` role. |
| `FCrowdyChannelPermissions` | The five flags: `bSendMessages`, `bManageChannel`, `bManageMembers`, `bManageRoles`, `bInviteMembers`. |
| `FCrowdyChannelPolicy` | `AppId`, `CreationPolicy`, `DefaultMembershipPolicy`, `MaxMembers`, `MaxChannelsPerUser`. |
| `FCrowdyChannelError` | An `ECrowdyChannelErrorCode` (`Unknown`, `NotFound`, `Forbidden`, `PolicyViolation`, `AlreadyMember`, `NotMember`, `NetworkError`, `ServerError`, `Banned`) and the `Message` to show. All but `Banned` are a best-effort classification of the message text; `Banned` comes from the server's own error code. |
Every id is an `int64`; the server's ids are 64-bit.
In C++ each call takes a pair of dynamic delegates: a success type shaped for its result (`FOnChannelSuccess` with an `FCrowdyChannel`, `FOnChannelsSuccess` with the array, `FOnChannelMemberSuccess`, `FOnChannelMembersSuccess`, `FOnChannelRoleSuccess`, `FOnChannelRolesSuccess`, `FOnMyChannelsSuccess` with the memberships, `FOnChannelPolicySuccess`, `FOnChannelVoidSuccess` for the calls that return nothing, `FOnChannelUnbanSuccess` with a `bWasBanned` for an unban) and `FOnChannelError`. The two events are `FOnMyChannelsCacheChanged` and `FOnChannelMessageReceived`. The latent nodes expose the same shapes as multicast pins that also carry `Error` and `Message`: `FChannelAsyncResult`, `FChannelsAsyncResult`, `FMyChannelsAsyncResult`, `FChannelMemberAsyncResult`, `FChannelMembersAsyncResult`, `FChannelRoleAsyncResult`, `FChannelRolesAsyncResult`, `FChannelPolicyAsyncResult`, `FChannelUnbanAsyncResult`, and `FChannelAsyncOnError` for the nodes that return nothing.
## What rides a channel from the SDK itself
A `Multicast` CrowdyEvent is encoded into a channel payload and published over the session channel, or the channel its `CrowdyChannel` names once this client has joined it. A replicated subsystem's Crowdy State delta travels the same way. Both are capped at 1024 bytes by the SDK's own codec and dropped loudly, never truncated, when they would exceed it; a Multicast event whose parameters can never fit is refused at registration. "Reliable" on this path means every member is a recipient, not that delivery is guaranteed; see [Recipients and routing](./recipients-and-routing.md#multicast-what-reliable-means).
`crowdy.rpc.reliable.trace 1` logs the reliable sends and receives on the channel transport.
## Gotchas
- The session channel is always joined, even in a project with no Multicast event.
- A named channel is join-only from the SDK's side. Create it in Crowdy Studio or at runtime, then `RegisterReliableRpcChannel` if you created it after connecting.
- `send_messages` gates publishing, not membership. An announce-only channel is a channel whose joiners lack it.
- The sender never receives an echo of its own message.
- Channel management is a Game API call and needs the app-scoped token the sign-in gives you; see [Authentication](../services/authentication.md). The wire protocol and the GraphQL calls are on the [Game API channels page](/game-api/channels).
## Related
- [Recipients and routing](./recipients-and-routing.md): the Multicast recipient.
- [RPC events in C++](./rpc-events-cpp.md): the `CrowdyChannel` key and the payload cap.
- [Replicated subsystems](./replicated-subsystems.md): deltas over the session channel.
- [Connection and reconnect](./connection-and-reconnect.md): when the channel bootstrap runs.
- [Teams and channels in Studio](../studio/teams-and-channels.md): authoring channels without code.
- [Play together](../services/play-together.md): chat rooms and party chat as text, with Send Chat and On Chat Message.
# Connection and Reconnect
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/connection-and-reconnect
# Connection and Reconnect
The realtime connection opens after sign-in, re-assigns itself a server when it loses one, and tells the game only when that fails. This page is what a game can read about the connection, what it should react to, and the two knobs that shape the traffic.
## When you need this
A connection indicator in the HUD, a "reconnecting" overlay, a decision about what to show while the world is not being updated. Everything else on the view plane depends on the connection and needs nothing from this page.
## The states
`UCrowdySDKSubsystem::GetUDPConnectionState()` (Blueprint: **Get UDP Connection State**) returns an `EUDPConnectionState`:
| State | Meaning |
|---|---|
| `Disconnected` | Not connected, and no reconnect in progress. |
| `Connecting` | The connection is being opened. |
| `Connected` | Up and carrying traffic. |
| `Reconnecting` | The connection lost its server and is assigning another. Reported, not acted on: it recovers on its own. |
| `GateKeep` (shown as Gate Kept) | The server is gatekeeping this client because the app is full. Nothing retries; the player has already been told. |
:::note[Poll it. No delegate is needed for a status indicator.]
`GetUDPConnectionState` is a pure node, safe to read every frame. Reserve the events below for the moments you must react to.
:::
The events on `UCrowdySDKSubsystem`, all Blueprint-assignable: **On UDP Connection Success** (`OnUDPConnectionSuccess`, an `FOnUDPConnectionSuccess`) whenever the connection comes up, including after a re-assignment, since that is a new session; **On UDP Timed Out** (`OnUDPTimedOut`, an `FOnUDPTimedOut`) when the transport gave up on a re-assignment, with the state at `Reconnecting`; **On UDP Address Notify** (`OnUDPAddressNotify`, an `FOnUDPAddressNotify` with `bSuccess` and `GateKeep`) when the server answers the access request. `RequestUDPAccess()` asks for the connection; the [Quickstart](../quickstart.md) never calls it because the sign-in does. Underneath sits `UCrowdyUDPSubsystem`, whose `GetConnectionState()` is the C++ read that needs no game thread and whose `GetUDPNetworkStats()` returns an `FUDPNetworkStatistics` (bytes and datagrams each way, messages per second, `Ping`, the client-notify counters and loss percentage); `ResetUDPNetworkStats()` zeroes it.
Coming up does more than flip the state: the same handler that broadcasts **On UDP Connection Success** joins the reliable-RPC channels, which is what makes the [session channel](./channels.md) "always joined".
:::note[Connected means the server is ready for your traffic, not only that the socket is open.]
After the server assigns this client, it needs a moment to install the session before it accepts spatial traffic. The SDK waits that out itself: the state stays `Connecting` until the assignment plus the readiness wait have elapsed, and only then does it turn `Connected` and broadcast **On UDP Connection Success**. You never time anything. What the SDK does not do is hold your sends for that moment: an actor update or a spatial event sent while the state is still `Connecting` goes out, or is dropped, and is never replayed once connected; the next replication interval is the recovery. So gate the thing that starts sending, the pawn's spawn or the entity component's activation, on this event or on `Get UDP Connection State` reading `Connected`, as the [Quickstart](../quickstart.md#1-make-your-player-a-crowdy-entity) does. A `Multicast` event is the one exception, queued until the channel bootstrap finishes (see the Gotchas).
:::
## When it drops
The low-level connection re-assigns a server on its own; the SDK only surfaces it as `Reconnecting`. A failed re-assignment is where the game hears about it: the SDK marks the state `Reconnecting`, broadcasts **On UDP Timed Out**, and immediately asks for the connection again itself, so a handler that polls the state inside that event reads `Reconnecting`, never `Disconnected`. `Disconnected` is what you read only after that request fails too. If the failure was the app being full, the state is `GateKeep` instead and nothing retries. The timeout that starts a re-assignment is `UDPTimeoutSeconds` in the project settings, and the address family is `UDPProtocol` (`ECrowdyUDPProtocol`: `Auto` takes the IPv4 address, `IPv4` records that choice deliberately, `IPv6` takes the IPv6 address and does not fall back). Both are managed by Crowdy Studio and shown read-only in Project Settings; see [Config Sync](../studio/config-sync.md).
`StopNetworkOperations()` reports the session as disconnected for the UI to read; it does not close the socket or interrupt a recovery already under way, deliberately. `ToggleNetworkMessageProcessing()` discards received messages for development.
:::caution[StopNetworkOperations changes what the UI reads. It does not stop the connection.]
The connection belongs to the transport and recovers on its own. Asking the UI to show a disconnect must not be able to cancel that.
:::
## Reacting to a drop
There is no reconnect code to write. The SDK asks for the connection again itself, and the game hears two events: **On UDP Timed Out** when a re-assignment failed and the SDK is opening the connection again, and **On UDP Connection Success** when it is back. Bind both once, in your Game Instance's `Init` after `Super::Init()` in C++ or from your Game Mode's Event BeginPlay in Blueprint, and do the visible thing in each.
:::note[Do not call RequestUDPAccess from the timeout handler.]
The SDK calls it itself as soon as the event has been broadcast. A second request from the game is a second reconnect in flight for the same client.
:::
The lantern village dims every placed lantern while the connection is down and restores them when it is back: `WatchConnection`, called from `Init`, binds the two events to `HandleConnectionLost` and `HandleConnectionRestored`.
```cpp
// LanternGameInstance.h
void WatchConnection();
UFUNCTION()
void HandleConnectionLost();
UFUNCTION()
void HandleConnectionRestored();
// LanternGameInstance.cpp
#include "EngineUtils.h"
#include "Lantern.h"
void ULanternGameInstance::WatchConnection()
{
// The SDK reconnects by itself after a timeout; a game only reacts to the two events.
UCrowdySDKSubsystem* Sdk = GetSubsystem();
Sdk->OnUDPTimedOut.AddDynamic(this, &ULanternGameInstance::HandleConnectionLost);
Sdk->OnUDPConnectionSuccess.AddDynamic(this, &ULanternGameInstance::HandleConnectionRestored);
}
void ULanternGameInstance::HandleConnectionLost()
{
for (TActorIterator It(GetWorld()); It; ++It)
{
It->Light->SetIntensity(0.f);
}
}
void ULanternGameInstance::HandleConnectionRestored()
{
for (TActorIterator It(GetWorld()); It; ++It)
{
It->Light->SetIntensity(5000.f);
}
}
```
In the Game Mode Blueprint, from **Event BeginPlay**, a **Crowdy SDK Subsystem** getter feeds **Bind Event to On UDP Timed Out** and then **Bind Event to On UDP Connection Success**; the custom events `OnConnectionLost` and `OnConnectionRestored` are theirs. Dimming every lantern needs a loop, which is more than a six-node figure shows, so the graph binds the events and stops.
[Blueprint snippet: conn-events, see /bp/unreal-sdk/conn-events.txt]
## When the server refuses a send
A send leaves this client before the server judges it, so a refusal arrives later, on its own. **On Send Refused** on the realtime subsystem (`UCrowdyCppReplicationSubsystem::OnSendRefused`; in Blueprint, **Get Game Instance**, **Get Subsystem (Crowdy Cpp Replication Subsystem)**, **Bind Event to On Send Refused**) fires on the game thread for each one (an `FOnCrowdySendRefused`), with an `FCrowdySendRefusal`: the **Code** and **Code Name**, whether it was a channel message and its **Channel Id**, the **Message Type**, **What** was refused in words and a **Remedy**. The same line goes to the log as a Warning. C++ that reads the bridge's own send errors can turn one into the same struct with `UCrowdyCppReplicationSubsystem::DescribeSendRefusal(Error, OutRefusal)`, which returns false when the server's error names no send this client has on record.
The usual one is **Unauthorized** for a permission the player's access tier does not grant. The permissions belong to each access tier of the app: `use_text_chat` for channel messages (party and guild chat, chat rooms, reliable CrowdyEvents such as Multicast events, and CrowdyState sent on a channel) and proximity text, and `use_direct_messages` for a message to one actor (an Owning Client or Host event, and CrowdyState only its owner reads); without them the server refuses every such send from that tier's players. The tiers an app had when these permissions arrived were given both; check a tier's permissions on the web console when its players' sends are refused. A refused channel message can also mean the player is not a member with the send messages permission, and the remedy says both.
The server names a refused send only by a one-byte sequence number, so on a busy connection a refusal can be put down to the wrong send. Show it to the player or the log; do not undo game state because of it.
## Bundled sends
Outbound replication messages of one network pass are packed into bundle datagrams. `crowdy.net.send.bundle` (default 1) controls it; 0 sends one datagram per message, a diagnostic switch rather than a setting a shipping build needs. The variable is read when a connection opens, so flipping it takes effect on the next connect, not on the live socket.
## Signed inbound bundles
Once the connection is up, the transport tells the replication server what this client can read (a `CLIENT_CAPABILITIES` message), and repeats it every 15 seconds so a token refresh or a server-side move, both of which reset the server's record without saying so, never leaves it stale. A server at v0.30.0 or later then packs the notifications it sends you into `MESSAGE_BUNDLE_SIGNED` datagrams: the members inside carry no signature of their own, and one HMAC at the end covers the whole datagram. The transport checks that one signature, drops the datagram if it fails, and delivers the members exactly as it delivers any other notification. Your handlers see no difference; a datagram that used to cost one verification per member costs one. An older server ignores the message and keeps signing every member, which is also what the tagged 2.14.0 plugin receives, since it never announces itself.
`crowdy.net.recv.signedbundles` (default 1) is the switch; 0 stops the announcement and keeps the per-member form, for a before-and-after comparison rather than for shipping. Like `crowdy.net.send.bundle` it is read when a connection opens.
## The receive budget
Inbound messages are delivered on the game thread, per frame, under two budgets: `crowdy.net.receive.maxmessages` (default 3072) caps how many messages one frame may deliver, and `crowdy.net.receive.maxdrainms` (default 4.0) caps the milliseconds one frame may spend delivering them. Whichever binds first ends that frame's drain and the rest waits for the next frame. The budget covers every message the connection receives, channel notifications and video fragments included, not only actor updates.
:::note[Raise the message count first.]
If drains keep ending on the time budget instead, delivery is costing more per message than the frame can afford, and the count is not the lever.
:::
`crowdy.net.receive.poolactorupdates` (default 1) reuses actor update message objects instead of allocating one per message; `crowdy.net.routes` prints the network router's subscription table; `crowdy.net.trace 1` logs socket, send, and receive activity, and `crowdy.serialize.trace 1` the encode and decode of each payload. `crowdy.serialize.scopes` adds CPU trace scopes inside decode for profiling.
## Deprecated calls
Five functions on `UCrowdySDKSubsystem` are deprecated (the compiler warns on each call) and do nothing useful: `StartUDPTimeoutMonitoring`, `StopUDPTimeoutMonitoring`, `TriggerUdpHeartbeat`, `DeregisterAllReceptionLayers`, and `SetQueryEndpoint`. The connection tracks its own liveness now; remove the calls.
## Gotchas
- `Reconnecting` needs nothing from you. Only `Disconnected` after a timeout does.
- `GateKeep` is an answer, not a failure. Retrying against a full app asks a server with no room whether it has room.
- Coming up joins the channels. A Multicast event sent before **On UDP Connection Success** is queued and flushed when the bootstrap finishes; see [Channels](./channels.md).
- `crowdy.net.send.bundle` and `crowdy.net.recv.signedbundles` changes take effect on the next connect.
## Related
- [Channels](./channels.md): what connecting joins.
- [Video frames](./video-frames.md): the largest consumer of the receive budget.
- [Authentication](../services/authentication.md): the sign-in that requests the connection.
- [Console variables](../reference/console-cvars.md).
# Video Frames
Source: https://docs.dev.crowdedkingdoms.com/unreal-sdk/runtime/video-frames
# Video Frames
`UCrowdyVideoFrameReceiver` turns the video fragments the server fans out into whole frames and hands each one to you once, as an encoded image. The SDK does not decode it, does not know its dimensions, and has not checked that it is a valid image; turning the bytes into a texture is your code.
This SDK receives only. It has no public call, node, or subsystem that sends a video frame; the frames come from a client on the same app that publishes them through another Crowdy client, such as a browser on the JavaScript SDK ([Webcam video](/crowdyjs/webcam-video)). A game built on this SDK alone never produces one.
## When you need it
A picture-in-picture of a nearby player, a video wall in the village square, any feature that shows another client's camera. If the game never shows video it pays for none of this: with no listener bound a fragment is dropped the moment it reaches the receiver, with no copy and no partial frame held.
## Receive a frame
The receiver is a world subsystem, created for Play in Editor and game worlds only. Bind `OnVideoFrameAssembled` (an `FOnCrowdyVideoFrameAssembled`) and it fires on the game thread once per completed frame with an `FCrowdyVideoFrame`. C++ code may bind `OnVideoFrameReady` instead, a plain multicast delegate (`FOnCrowdyVideoFrameReady`) at the same firing point with no dynamic-delegate overhead.
The lantern village's player character binds at `BeginPlay`, and its `HandleVideoFrame` sets the torch's intensity to the byte count of each frame: a visible stand-in for the decode the example deliberately leaves out, because decoding is not the SDK's, as the page says below.
```cpp
// LanternPlayer.h
#include "Replication/Subsystems/CrowdyVideoFrameReceiver.h"
UFUNCTION()
void HandleVideoFrame(const FCrowdyVideoFrame& Frame);
// LanternPlayer.cpp
void ALanternPlayer::BeginPlay()
{
Super::BeginPlay();
if (UCrowdyVideoFrameReceiver* Video = GetWorld()->GetSubsystem())
{
Video->OnVideoFrameAssembled.AddDynamic(this, &ALanternPlayer::HandleVideoFrame);
}
}
void ALanternPlayer::HandleVideoFrame(const FCrowdyVideoFrame& Frame)
{
// The bytes are an encoded image the SDK never decodes; a consumer turns them into a texture.
Torch->SetIntensity(static_cast(Frame.Bytes.Num()));
}
```
In Blueprint the receiver is reached like any world subsystem: a **Crowdy Video Frame Receiver** getter node, **Bind Event to On Video Frame Assembled** off it, and a custom event with a `Frame` pin; **Break Crowdy Video Frame** opens the struct. The twin below, in the player character's Blueprint, binds at **Event BeginPlay**: a **Crowdy Video Frame Receiver** getter feeds **Bind Event to On Video Frame Assembled**, whose custom event `OnVideoFrame` calls **Set Visibility** on `Torch` (a Point Light component you add). Break the `Frame` pin to reach `Bytes`, `Sender Id`, and `Codec`.
[Blueprint snippet: video-receive, see /bp/unreal-sdk/video-receive.txt]
:::caution[Binding late costs you the frame already in flight, and nothing else.]
Reassembly is turned on by the first bind. The one frame that was assembling when you bound may be missed; every frame after it is delivered. Nothing before the bind was kept.
:::
**Success signal.** With a publishing client (not one built on this SDK) sending video into the same app, the torch's intensity jumps with every completed frame, and `GetPendingSenderCount()` reads above zero while a frame is half arrived.
## The frame
`FCrowdyVideoFrame`, all fields read-only:
| Field | What it is |
|---|---|
| `SenderId` (`FString`) | The sender's 32 wire octets as text. Exact and unambiguous; empty for an unset id. |
| `SenderUUID` (`FGuid`) | The key derived from those octets, the id the rest of the SDK addresses an actor by. Lossy: the derivation reads the octets as hexadecimal, so two senders whose ids differ only outside that alphabet derive the same key. `USerializationFunctionLibrary::ToGuid` (Blueprint: **To Guid**) derives the same key from a `SenderId` string, and returns an invalid guid for text that is not an actor id. |
| `Codec` (`ECrowdyVideoCodec`) | `Jpeg` or `WebP`. `Unknown` is what a byte this build has no entry for maps to; a fragment carrying one is dropped before it reaches you. |
| `FrameId` (`int32`) | The sender's own counter. It wraps, orders that sender's frames, and means nothing across two senders. |
| `Bytes` (`TArray`) | The encoded image, JPEG or WebP according to `Codec`. |
| `ChunkX`, `ChunkY`, `ChunkZ` (`int64`) | The chunk the last fragment was broadcast over. |
| `ServerTimestamp` (`int64`) | When the server stamped that last fragment, in milliseconds since the epoch. |
:::warning[Use SenderId, not SenderUUID, to tell two senders apart for certain.]
`SenderUUID` is the convenient key for matching a frame to an entity you already track by `FGuid`. It can name more than one sender. When correctness depends on which camera this is, compare `SenderId`.
:::
:::danger[There is no decode anywhere in the SDK.]
`Bytes` is a JPEG or WebP file in memory. Turning it into a `UTexture2D` is your own code, with the engine's image wrapper module or a decoder of your choice, and validating it as an image is part of that job: the bytes came off the network from another client.
:::
## Housekeeping
| Call | What it answers or does |
|---|---|
| `GetPendingSenderCount()` | Senders with a frame half arrived. Zero while nothing is listening, since nothing is ingested. |
| `GetDroppedFragmentCount()` | Fragments dropped for a malformed header, or for a frame their sender had already moved past. |
| `GetAbandonedFrameCount()` | Frames abandoned incomplete: superseded by a newer frame, timed out, or the sender left. |
| `ForgetSender(SenderGuid)` | Drops whatever is half assembled for one sender and forgets its frame counter. Exposed because a sender the server never announced as gone is otherwise never released, and a sender that rejoins and restarts at frame zero has every fragment dropped as a straggler until its counter climbs back past the old value. One key can name more than one sender, and every sender it names is forgotten. |
The receiver releases a sender itself when the server announces its departure or the tracker times it out, using the same lossy key.
## What is not delivered
- A client's own fragments. The server fans a video packet out over the chunk it was sent to, which includes the sender, and a frame of your own camera is one you already have. They are discarded, never reassembled.
- A frame that never completes. A fragment for a frame the sender has moved past is dropped; the incomplete frame is abandoned and counted.
Video fragments share the per-frame receive budget with every other message the connection receives; see [Connection and reconnect](./connection-and-reconnect.md#the-receive-budget).
## Gotchas
- The receiver is a world subsystem. There is none in an editor world, and a new world has a new receiver with nothing bound.
- Nothing is reassembled until something is bound. A diagnostic that reads the counters with no listener sees zeros.
- `FrameId` is per sender. Do not use it to order frames from two cameras.
- `SenderUUID` is the SDK's actor key and is lossy by construction; `SenderId` is exact.
## Related
- [Connection and reconnect](./connection-and-reconnect.md): the receive budget these fragments share.
- [Voice chat](../services/voice-chat.md): the sibling communication feature.
- [Entities and spawning](./entities-and-spawning.md): the actor tracker whose departures release a sender.