UserIdentity
Identity tag for a player entity. Attaches the Rust `PlayerOwned` marker via `__native` in `awake` and removes it in `onDestroy`. No public state, no update loop, no visuals.
Player
Identity tag for a player entity. Attaches the Rust PlayerOwned marker via __native in awake and removes it in onDestroy. No public state, no update loop, no visuals.
entity(id).component.add("UserIdentity")
entity(id).component.has("UserIdentity") -- true
Future rules (not yet enforced):
- Non-serialising. Scene and world saves omit this component. Players are attached at runtime per peer, not inherited from the save file.
- Non-removable. Once an entity is a player, removing this component is a no-op. A player stays a player for its lifetime outside an explicit identity swap.
Visuals, cameras, input, and movement live in separate components (e.g. PlayerController, PlayerTemplate bundle content) so adding or removing them never affects identity.
Interface
What this asset declares: the schema it conforms to, what it exposes, and the rendered structured payload.
conforms to
zero/source-extract/v2UserIdentity The per-user identity anchor. A hidden, registry-only entity (named "user") with no world presence: never rendered, never parented to anything, never a parent, never surfaced by entity queries or the inspector's default view. It exists only so a connected user has a stable, synced handle in the room. It owns exactly two things: 1. `avatar` — a synced string entity id pointing at the user's BODY, a live world entity carrying the visual, controller, and transform. This is the identity → body pointer; the body carries the reverse link (`PlayerAvatar.owner` = this identity's userId). 2. `userId` / `displayName` — the connected user's account identity, synced so every peer resolves the real user behind the body. The body is a standalone entity at the scene root, connected to the identity only by its `PlayerAvatar` component. Everything spatial — movement, camera-follow, raycasts — targets the body, never the identity, which has no transform of its own. ONE path binds the body: assign a live entity to the player's avatar — `layers.active.players.localPlayer.avatar = entityRef` (a `.bundle` / `.avatar` asset is not a world entity, so it is never accepted here; spawn it into the world first, then bind the resulting entity). The bind runs in `onPropertyChanged` on the LOCAL identity: it marks the body synced + `PlayerOwned`, attaches `PlayerAvatar.owner`, and tears down the previously-bound body. A remote identity receives its `avatar` field and its body over sync, so its `onPropertyChanged` only drives the readiness latch — the local bind never runs for a body this peer does not own.
writePlayerAvatar(avatarEid: ?, ownerIdentity: ?) → void
The avatar carries a first-class `PlayerAvatar` component whose `owner` field is the owning user identity id (the account id). It is the visible, inspector-facing link from the body back to who owns it — the identity is registry-only and is never a parent, so a proper component link (not a hierarchy walk, not a hidden attribute) is how scripts find the owner. They read `PlayerAvatar.owner` and resolve the live player via the registry.
| arg | type | description |
|---|---|---|
| avatarEid | ? | |
| ownerIdentity | ? |
clearPlayerAvatar(avatarEid: ?) → void
| arg | type | description |
|---|---|---|
| avatarEid | ? |
despawnTree(eid: ?) → void
Walk an avatar's entity tree post-order and despawn from the leaves up. `entity.despawn` does not cascade — without this descendant entities become orphans with detached scripts still ticking.
| arg | type | description |
|---|---|---|
| eid | ? |
markPlayerOwned(eid: ?) → void
Attach the Rust `PlayerOwned` marker to the AVATAR (body) entity. The identity entity carries its own `PlayerOwned` marker (inserted in awake), but the body is a standalone entity at the scene root — not under the identity in the hierarchy — so the sync layer's `root_carries_player_owned()` parent-walk can never reach the body through the identity. The body must carry the marker itself, or the edit-mode relay gate (and the broadcast filter) skip it and the body never replicates. The slot setter/teardown own the body's copy.
| arg | type | description |
|---|---|---|
| eid | ? |
clearPlayerOwnedMarker(eid: ?) → void
Drop the avatar's `PlayerOwned` marker when the slot clears (counterpart to markPlayerOwned). Id-first form — the avatar is a different entity than the identity component's host, so the implicit-context remove would target the wrong entity.
| arg | type | description |
|---|---|---|
| eid | ? |
teardownAvatar(avatarId: ?) → void
Tear down the body currently bound to the avatar slot: drop its `PlayerAvatar` link + `PlayerOwned` marker, then despawn it. The identity owns its body's lifecycle — on rebind the previous body is despawned, and on the identity's onDestroy the current body is despawned, so a body is never left orphaned in the scene tree with no way to reach it through the registry.
| arg | type | description |
|---|---|---|
| avatarId | ? |
markSynced(eid: ?) → void
Flip a single entity to multiplayer-synced at the root. Idempotent. Used on the identity entity in awake and on a bound body. The body root replicates; its Asset-spawned visual (mesh, skeleton, bones) is a per-peer local reconstruction driven by the synced `Asset.source` ref, which each peer's `Asset.awake` rebuilds.
| arg | type | description |
|---|---|---|
| eid | ? |
markSessionScoped(eid: ?) → void
Flip an entity to session-scoped: it dies with its owning peer's relay session. The relay despawns session-scoped entities owned by a peer when that peer disconnects (broadcasting EntityDespawn to the remaining peers so they clean up too) and omits them from the snapshot sent to new joiners. User identity entities are pure per-session identity — without this they are non-session-scoped synced entities the relay keeps after their owner leaves and re-snapshots to every future joiner, which is exactly how ghost identities accumulate across reconnects. `entity(id):setSessionScoped(true)` is idempotent and also tags the entity SyncEntity, so it is safe alongside `markSynced`.
| arg | type | description |
|---|---|---|
| eid | ? |
awake( ) → void
onDestroy( ) → void
onPropertyChanged(key: ?, value: ?, oldValue: ?) → void
The one path that binds a body to this identity. Assigning the `avatar` field a live entity id runs here — via the public surface `layers.active.players.localPlayer.avatar = entityRef`, or the authored spawn adopting its clone body. On the LOCAL identity it despawns the previously- bound body (rebind or clear), then marks the new body synced + `PlayerOwned` and attaches its `PlayerAvatar.owner` back-link, and drives the per-scene `players:onLocalReady` latch — the first moment body-touching game code can run (awake is too early, no body is bound yet). A REMOTE identity receives its avatar field and body over sync, already marked by the owning peer, so it takes none of these actions.
| arg | type | description |
|---|---|---|
| key | ? | |
| value | ? | |
| oldValue | ? |
onAssetReload(_field: ?) → void
Asset-content reload (any VFS write under the body's directory). The slot's stored value is an entity id; the body's content is reloaded via the bundle.instantiate machinery elsewhere, and the slot value survives.
| arg | type | description |
|---|---|---|
| _field | ? |
Sub-parts
Everything contained inside this part. Assets are composite children (clickable cards). Files are leaf payloads. Expand any row to view its source.
Problems
Everything affecting this asset right now: its own problems, anything wrong inside it, and problems on its direct dependencies.
agent_score is exposed.+ quality × 0.35
+ performance × 0.25
± compat factor
Usability ratings
Did the part work as advertised when consumers tried to drop it in. Separate from upvotes: those are taste; this is "did it function".
Scoped to this part · feeds back into the world's score.