Skip to main content

Entities, Identity, and Ownership

An entity is an actor that other players can see and react to. It carries one UCrowdyEntityComponent, which gives it a shared identity, a role on each client, and a place in the entity registry. RPC events and Crowdy State are both built on top of that.

What an entity is​

Add a UCrowdyEntityComponent to an actor and it becomes an entity. On every client the entity has:

  • a NetID, the name every client uses for this one thing;
  • a role: Owner on the client that simulates it, RemoteProxy everywhere else, or HostOwned for a world entity the elected host drives;
  • an entry in the entity registry, which is how an event aimed at "this rock" finds the right rock on each machine.

The NetID​

The SDK hands you the NetID as an FGuid (GetNetID()), which is convenient to compare and store. On the wire it is 32 opaque bytes; the FGuid is a derived form. Treat it as an opaque handle: compare it, pass it along, never build one by hand.

Never construct a NetID yourself.

The wire identity is not an RFC 4122 UUID, and a hand-made FGuid does not name anything. Let the identity policy below, or a spawn call, mint it.

FCrowdyActorId, the wire type​

FGuid is the convenient face; FCrowdyActorId is what actually travels the wire, 32 raw octets rather than an RFC 4122 UUID. Build one only through its named constructors: FromGuid converts an FGuid, FromOctets and the fallible TryFromOctets build from an existing 32-byte view (the fallible one returns bool instead of asserting), FromStringOrUnset and TryFromString parse a hex string, and FromAnsiLiteral builds a compile-time one from a string literal for tests and defaults.

Once you have one, IsSet() says whether it names anything, AsOctets() gives a read-only view of the 32 bytes, AppendTo and WriteTo copy them into a buffer you already own, ToString() renders the hex form back, and Reset() clears it to unset.

Identity policies​

IdentityPolicy on the component decides how the NetID is chosen.

  • Stable (the default). Derived from the actor's placement in the level, so every client computes the same NetID for the same level-placed actor with no spawn message and no hand-typed seed. Use it for anything that already exists when the map loads: props, spawners, doors.
  • PlayerDerived. Derived from the signed-in user id. Valid only on the locally controlled player pawn.
  • Random. A fresh id each time, with no cross-client agreement. Use it for a purely local actor nobody else needs to address.

An actor you spawn during play through the SDK's spawn call is told its identity by the spawner, so its policy does not matter; the spawn travels to every client carrying the NetID.

PlayerDerived on anything but the local player's pawn falls back to Random.

The SDK logs one warning and the actor gets an id no other client shares. Keep PlayerDerived for the pawn. The identity is read once: at the component's BeginPlay when the pawn already has a controller, otherwise inside its first possession, after the pawn's own Possessed and Controller Changed events, since a pawn spawned during play (RestartPlayer, a respawn) is possessed only after BeginPlay. The first controller of any kind completes it; an AI controller gets the fallback. A respawn that leaves the earlier pawn alive moves the account id to the new pawn and clears the old one, which stops replicating. Read the id from the component's On Crowdy Ownership Assigned or the entity subsystem's OnEntityRegistered, never from BeginPlay or Possessed; Is Locally Owned on the pawn is false until the registration.

The entity component's Details panel: Mode, Identity Policy, and Ownership

Stable identity in a packaged build​

Stable identity prefers the engine's per-placement instance guid, which is saved in the level and is identical on every client. In a cooked build the engine releases that guid before BeginPlay, so the SDK snapshots it when the component registers and uses the snapshot. You do not have to do anything, but if you write your own identity code, read placement data at OnRegister, never at BeginPlay.

In a cooked build, read the placement guid at OnRegister, never at BeginPlay.

Editor and Play in Editor keep the guid alive and hide the bug; only a packaged build shows it.

Owner and proxy​

For each entity exactly one client is the authority, and only the authority writes the entity's view state.

Ownership on the component chooses the authority for a level-placed actor:

  • Host (the default): whichever client is currently the elected host owns it (Role = HostOwned). Every client shares one authority, which is what a world or AI entity wants.
  • LocalClient: this client owns and simulates it (Role = Owner).

An entity spawned at runtime is always owned by the client that spawned it, whatever the component says. Ownership only applies to level-placed actors that resolve their own identity.

Ask IsLocallyOwned() before doing owner work. It is true when this client is the entity's owner, or when the entity is host-owned and this client is the host. A remote proxy never sends state; it receives and plays back.

A player's own avatar has NetID == OwnerID == the local player id, and every entity that player spawns carries that id as its OwnerID. This is Crowdy ownership and has nothing to do with AActor::GetOwner().

Example​

A lantern post is a lantern that belongs to the level rather than to a player: the Quickstart lantern with the three settings a level-placed world entity wants. It is event-only, every client derives the same NetID for it, and the elected host is its one authority.

All three settings are EditAnywhere, so a constructor (or the Details panel, or Class Defaults) is where they are set. ALanternPost inherits everything else from ALantern, so a post flickers and lights like any lantern, and its IsLocallyOwned() checks are true on the host alone:

// LanternPost.h
#include "Lantern.h"
#include "LanternPost.generated.h"

UCLASS()
class ALanternPost : public ALantern
{
GENERATED_BODY()

public:
ALanternPost();
};

// LanternPost.cpp
#include "LanternPost.h"

ALanternPost::ALanternPost()
{
// A level-placed post: event-only, the same NetID on every client, and one shared authority, the elected host.
CrowdyEntity->Mode = ECrowdyEntityMode::Static;
CrowdyEntity->IdentityPolicy = ECrowdyIdentityPolicy::Stable;
CrowdyEntity->Ownership = ECrowdyOwnership::Host;
}

Gotchas​

  • IsLocallyOwned() is the question to ask. Do not compare ids by hand to decide who is in charge.
  • A proxy of a Static entity never exists on its owner; the owner has the real thing, and an event it sends never comes back to it. A Dynamic entity's owner may see its own echoed proxy as well; see Rendering backends.
  • Stable identity depends on level placement. An actor you create in code with Stable set does not agree across clients; spawn it through the SDK instead.
  • Identity Policy has no Blueprint getter. If a graph needs it, it is probably asking the wrong question; ask Get Role instead.