Core model in five minutes
Every feature in LiteEntitySystem is a consequence of five ideas; this page walks through them once so the rest of the documentation can build on them without re-explaining.
The world is a set of entity classes
A game world is a collection of entity objects: players, projectiles, items, score counters. Entity classes are plain C# — no engine base classes — and both server and client register the same set of classes under the same ids, then construct instances of them. The server decides which entities exist; each client holds a synchronized copy of the entities it is allowed to see.
Visuals are separate. An entity may create engine objects (a Unity GameObject, a sprite, a sound) as its view, typically when it is constructed, but the view is game code — the library synchronizes only entity state.
Time advances in fixed ticks
All game logic runs at a fixed tick rate (Tickrate), chosen when the server starts. Once per tick the manager calls Update() on entities that update — this is the logic tick, and it is the only place where gameplay state should change. You pump the manager's own Update() from your engine loop every render frame; it accumulates real time and runs zero or more logic ticks internally.
Rendering is decoupled from ticks. Entities also get VisualUpdate() once per render frame on the client — that is where views read interpolated values. The current tick number is Tick (a ushort, it wraps around).
The server owns all state
The server's simulation is the single source of truth. Changed SyncVar fields are collected and sent to clients as delta-compressed state updates at the configured send rate; a joining client first receives the whole world as one compressed baseline, then only the changes. RPCs travel in the same stream, ordered relative to state. The consequences of that model — sampling, late joins, bandwidth — are on how state gets to clients.
A client cannot create or destroy synchronized entities and cannot make a lasting change to a SyncVar. Anything a client writes is either a prediction (corrected by the next server state) or a purely local change.
Clients send inputs, not state
The only thing a client continuously uploads is its input: one small unmanaged struct per tick, delta-compressed, resent until the server confirms it. The server applies each input on the matching tick of its own simulation and moves the player's entities itself.
This is why there are no client-to-server RPCs. For occasional non-input communication — buy an item, change loadout — there is a reliable request channel with a server response callback.
The client predicts and interpolates
For entities owned by the local player, the client does not wait for the server: it applies its own input immediately, simulating those entities ahead of server time (client-side prediction). When a server state arrives, predicted fields are reset to the authoritative values and the entity is re-simulated from stored inputs — a rollback. If the prediction was right, the correction is invisible.
Entities owned by others are shown slightly in the past instead: the client keeps a small buffer of server states and interpolates between the last two. So one client screen mixes two timelines — your own entities ahead of the server, everyone else behind it — and lag compensation on the server exists to reconcile the difference when you shoot at what you see.
Data flow at a glance
inputs: 1 struct per tick, delta-compressed, unreliable
┌────────────────────────────────────────────────────────────────►
CLIENT SERVER
predicts own entities fixed-tick simulation,
interpolates the rest single source of truth
◄────────────────────────────────────────────────────────────────┐
on join: full world baseline (LZ4, reliable)
then: state diffs + RPCs (at send rate, unreliable)
One cycle, side by side
Read buffered client inputs for this tick → run Update() on entities → record history for lag compensation → every send-rate ticks, serialize changed fields per player and send diffs and pending RPCs.
Warning
Common mistakes
- Changing gameplay state in
VisualUpdate()— it runs per render frame, is never predicted or rolled back, and diverges from the server. Logic belongs inUpdate(). - Expecting a server-side
SyncVarwrite to be visible on clients in the same instant — it arrives with the next state update, delayed by send rate and network latency. - Treating the client's
Tickas server time — it is an independent counter that starts from zero at connection; the server timeline on the client isServerTick.