Ownership and players
Every entity carries an OwnerId — the byte id of the player it belongs to, or 0 for the server — and that single byte decides who predicts the entity, who receives which fields, and whose perspective lag compensation uses.
Players
On the server, each connection accepted through AddPlayer becomes a NetPlayer with an id from 1 to 254; id 0 (EntityManager.ServerPlayerId) is reserved for the server itself. The client learns its own id with the baseline — PlayerId on the manager — and exposes its NetPlayer as LocalPlayer. Server code can look up any player by owner id via GetPlayer(entity.OwnerId).
The three checks
| Check | True when | Typical use |
|---|---|---|
IsServerControlled |
OwnerId == 0 |
World objects, singletons — nobody's, everybody sees them the same |
IsLocalControlled |
OwnerId equals this manager's player id |
Prediction-side logic, owner-only feedback |
IsRemoteControlled |
Anything else | Entities this side only observes, interpolated |
One asymmetry worth internalizing: the server's own player id is 0, so on the server every server-owned entity reports IsLocalControlled == true. The checks describe "mine / not mine" relative to whichever manager is asking, not "server / client".
Minimal example
A bomb inherits its planter's ownership, so the planter's client predicts it:
Bomb.cs
using LiteEntitySystem;
[EntityFlags(EntityFlags.Updateable)]
public class Bomb : EntityLogic
{
public SyncVar<float> Timer;
public Bomb(EntityParams entityParams) : base(entityParams) { }
protected override void Update()
{
Timer.Value -= EntityManager.DeltaTimeF;
if (IsServer && Timer.Value <= 0f)
Destroy();
}
protected override void OnConstructed()
{
if (IsClient)
{
// IsLocalControlled → this client planted it: friendly outline, own ticking sound
// IsRemoteControlled → someone else's bomb: hostile view
}
}
}
On the server
// server-owned: OwnerId == 0, nobody predicts it
var crystal = manager.AddEntity<ScoreCrystal>();
// owned by the planter: passes the parent's owner down
var bomb = manager.AddEntity<Bomb>(planterPawn, b => b.Timer.Value = 3f);
How it works
How entities acquire an owner
Game code never assigns OwnerId directly — there is no public setter. Ownership always flows in from structure:
- A plain
AddEntity<T>()produces a server-owned entity. AddController<T>(player, …)creates a controller owned by that player, and possession passes the owner on to the pawn.AddEntity<T>(parent, …)andAddPredictedEntity<T>inherit the parent's owner.SetParentre-propagates the new parent's owner down the whole child subtree.
The consequence: ownership is a property of where an entity hangs in the possession and parent structure, and it changes when that structure changes — a pawn released by its controller reverts to server ownership, and every child follows.
What ownership drives
Prediction: a client simulates only entities it owns; everything else is interpolated. RPC routing: ExecuteFlags.SendToOwner / SendToOther split on this byte. Field visibility: SyncFlags.OnlyForOwner / OnlyForOtherPlayers filter per-field, and EntityFlags.OnlyForOwner hides whole entities (this is how controllers stay private). Lag compensation rewinds other players' entities relative to an owner — the owner is never rewound for their own shot. And on the client, owned Updateable entities are the ones that run Update() locally.
Ownership changes at runtime
Possession and reparenting rewrite OwnerId on the fly, and the client reacts automatically: an entity that becomes locally controlled starts predicting, one that stops being owned drops back to interpolation. React to these transitions in the hooks that cause them — OnControlledEntityChanged on controllers, OnParentChanged on entities — rather than polling the checks.
Behavior details
The server updates every entity regardless of owner; OwnerId matters for packet composition — which player receives which fields and RPCs — and as the key for GetPlayer when kicking, inspecting input state, or enabling lag compensation for a shooter.
Warning
Common mistakes
- Caching
IsLocalControlledat construction — possession and reparenting change ownership at runtime, and the cached flag silently goes stale; check it where you use it, or react inOnControlledEntityChanged/OnParentChanged. - Using
IsClientwhere you meanIsLocalControlled— anIsClientgate runs for every client, so "my" feedback (sounds, UI) plays for spectators of other players too. - Treating the server's
IsLocalControlled == trueas a client indicator — on the server it merely means server-owned; combine withIsServer/IsClientwhen the side matters.