For AI Agents
This page is the routing layer for a coding agent working inside a studio's Unreal project. It does not re-explain the SDK; every row links the page that owns the fact, and the full text of every page is available as one file for your context window. Read this page, then pull only the pages a task needs.
Three machine-readable files sit beside the docs. This page is where they are described:
| File | What it is |
|---|---|
/helpers/unreal-sdk/llms-full.txt | Every page in this section as plain text, in sidebar order, with every C++ snippet inlined and a pointer line for every Blueprint graph. |
/helpers/unreal-sdk/sdk-surface.json | The generated inventory of every reflected symbol the plugin ships. Check it before you go looking through headers; see SDK surface manifest. |
/helpers/unreal-sdk/AGENTS.md | The drop-in for a project root, CLAUDE.md, or .cursorrules: the locked rules and one example per marker, under 8 KB. |
Where does this state go
Ask one question: can a malicious client benefit from lying about this value? If yes, it belongs on the truth plane, in your app's server code on ck-exec, which the SDK reaches from C++ or Blueprint through Server Logic. If no, it is view state. The Two Planes owns the rule and every row but the door's; that one applies the same test.
| State | Plane | Mechanism | Owning page |
|---|---|---|---|
| Movement | View | Dynamic entity mode, continuous state channel | Continuous state |
| Animation blend state | View | meta=(CrowdyState) property, diffed on change | Crowdy State |
| A one-shot trigger (muzzle flash, impact) | View | meta=(CrowdyEvent) RPC, not a state field | RPC events |
| Hit points, inventory, currency | Truth | Your server code | The Two Planes |
| Match score | Truth | A hub for the match in your server code | The Two Planes |
| A chat line | View | CrowdyEvent with the Multicast recipient, session channel | Channels |
| A door's open flag | View | CrowdyState boolean: a lie is a visual glitch. A placed door is host-owned, so the flag is also CrowdyManualDirty and the host calls MarkStateDirty after each write. If the door gates progress (a lock), the lock state belongs in your server code | The Two Planes, the host push |
| A player's display name | View | CrowdyState property; route it through the avatars service if it must persist | The Two Planes, Avatars |
Crowdy State is written by the owning client and believed by everyone else. Low latency is not a reason to move a value to the view plane; predict locally and let the server's confirmed value overwrite the prediction. The Two Planes.
The marker vocabulary
Two of the three are meta=(...) keys; the entity is a component. One canonical example each, all from the same lantern the Quickstart builds; the snippet id names the file the docs render.
| Marker | Goes on | Canonical example | Owning page |
|---|---|---|---|
CrowdyEntity | A UCrowdyEntityComponent on an AActor (the component is the marker; set Ownership) | qs-entity | Entities, identity, ownership |
CrowdyState (+ CrowdyOnRep) | A UPROPERTY on an entity actor. On a client-owned entity (Ownership = Local Client, the lantern) the owner assigns and calls the notify itself; on a host-owned entity (Ownership = Host, the default a placed actor keeps) a plain assignment never ships: add CrowdyManualDirty and call MarkStateDirty after each write, or add CrowdyHeartbeat | qs-state | Crowdy State, the host push |
CrowdyEvent + CrowdyRecipient | A UFUNCTION named X_Implementation, plus CROWDY_EVENT(X); recipient is one of SpatialMulticast, Multicast, OwningClient, Host | qs-event | RPC events |
Four rules the examples assume: the first thing a client needs is its own pawn as a Dynamic, PlayerDerived, LocalClient entity spawned after On UDP Connection Success (qs-player), because the server knows a client by its actor updates and a SpatialMulticast reaches only clients with a fresh actor; that pawn registers inside its first possession, since the engine possesses a runtime spawn after its BeginPlay, so read its id and IsLocallyOwned() from OnCrowdyOwnershipAssigned, not at BeginPlay; a SpatialMulticast body runs on the caller too, so guard the call with IsLocallyOwned() (RPC events); a CrowdyOnRep function takes no parameters, read the property for the new value (Crowdy State); and the lantern's assign-and-stop is the client-owned pattern, because the Quickstart sets its Ownership to Local Client: a host-owned entity (Ownership = Host, the default a placed actor keeps: Static, Stable, level-placed) never ships a plain CrowdyState write, so mark the property CrowdyManualDirty and call MarkStateDirty after each write, or give it CrowdyHeartbeat (the host push).
Three things that break silently in a cooked build
Editor and Play in Editor hide all three. Only a packaged build shows them.
| Break | Why it is silent | Fix | Owning page |
|---|---|---|---|
Reading HasMetaData at runtime | Cooked and Shipping builds strip UObject metadata; the check answers false and marker-driven code goes dark with no error | Read UCrowdyBakedRegistry; the SDK already does this for RPC and Crowdy State metadata | Packaging |
| A marker added after the last cook | The baked registry is a snapshot taken at cook time; a new marker is invisible until it is rebaked | Cook again: the cook rebakes the registry at its start, so the break is a package cooked before the marker existed. Tools > Rebuild Crowdy Registry or Studio's Rebuild (Deep Scan) only refresh the in-editor view of the bake | Packaging, Inspector and Registry |
A placement guid read at BeginPlay | The engine releases the per-placement guid before BeginPlay in a cooked build; the editor keeps it alive | Read placement data at OnRegister, never at BeginPlay | Stable identity in a packaged build |
A fourth hazard is loud, not silent: Shipping sets WITH_DEV_AUTOMATION_TESTS to 0, and a target that forces dev tests back on can then fail to compile a test file through a !UE_BUILD_SHIPPING region it reaches via a header. What Shipping strips.
Settings never to hand-edit
Config Sync in Crowdy Studio writes the Network category of UCrowdySDKDeveloperSettings into Config/DefaultGame.ini under [/Script/CrowdyReplication.CrowdySDKDeveloperSettings]. The fields are read-only in Project Settings on purpose; a hand-pasted value is overwritten by the next sync, and a drifted project fails to connect with no obvious error.
| Never hand-type | Owned by you (fine to edit) | Owning page |
|---|---|---|
Environment, DiscoveryUrl, AppID, OrgId, GameApiHttpUrl, GameApiWsUrl, UDPProtocol, UDPTimeoutSeconds, HostPollIntervalSeconds | MapProfiles, DefaultProfile, BakedRegistry, PreloadedEntityClasses, IDOverrides, ClassIDOverrides | Config Sync, Project settings |
Do not copy the plugin's [CoreRedirects] into the project's DefaultEngine.ini either; Plugins/CrowdySDK/Config/DefaultCrowdySDK.ini already carries them. Packaging.
Testing without a second client
Both switches are off by default. Turn them back off before testing real two-client routing. Testing locally.
| Console variable | What it does | Owning section |
|---|---|---|
crowdy.rpc.loopback 1 | A sent CrowdyEvent is also delivered to your own receive path, once; a call made from inside the replayed body does not loop again | Single-client RPC loopback |
crowdy.state.loopback 1 | A state delta for an owned entity is decoded onto a lazily spawned local mirror (RemoteProxy role, its own NetID), so OnRep fires with one client. The mirror's trace line always says (targeted); that label is cosmetic | Single-client Crowdy State loopback |
Trace console variables
Every area exposes crowdy.<area>.trace, off by default; warnings and errors print regardless. The four to reach for first, and the two to leave alone:
| Console variable | Shows | Owning page |
|---|---|---|
crowdy.rpc.trace | Every CrowdyEvent send and receive: function, entity, addressing, owned or delegated | Testing locally |
crowdy.state.trace | Owned-entity diffing, delta emission, datagram sizes | Testing locally |
crowdy.entity.trace | Registry add and remove, event routing and dispatch, actor tracking | Testing locally |
crowdy.net.trace | UDP transport: socket open and close, sends and receives, pool churn | Testing locally |
crowdy.serialize.scopes, crowdy.state.scopes | CPU trace scopes around every message decode, a real per-message cost; leave off unless reading that timing | Console variables |
The full table, including behavior switches and diagnostic commands, is on Console variables; which log category pairs with which trace is on Log categories.
Locked rules in one place
| Rule | Owning page |
|---|---|
Every id is int64; the app id travels as a JSON string and the SDK encodes it, never you | The Two Planes |
| The host is a convention, not enforcement; never gate cheat-sensitive state on it | Host is a convention, Host authority |
| Presence is the player's actor: a client with no fresh actor is at no position and receives no spatial event | Sessions and presence |
A world subsystem null-checks GetGameInstance() in Initialize(); it is null in the transient world at engine start | Host authority, Replicated subsystems |
An array parameter on a delegate handler is const TArray<T>&; a by-value parameter does not match and AddDynamic will not compile | Teams |
A Multicast call has a 1024-byte payload budget and is dropped loudly, never truncated; large payloads go on SpatialMulticast | RPC events |
Related
- Quickstart: the lantern every example above comes from.
- Best Practices: the four rules stated in the abstract.
- Packaging: the checklist before a cooked build.