Registering entity types
EntityTypesMap<T> maps your entity classes to numeric class ids taken from an enum; both server and client build this map at startup, and it is what allows the library to construct the right class when an entity syncs in.
When to use this
- Once at startup, on both sides, before creating a
ServerEntityManagerorClientEntityManager— the manager takes the finished map as its first constructor argument. - For every class that exists in the synchronized world: entities, pawns, controllers, singletons.
- AI controllers (
AiControllerLogicsubclasses) may be registered on the server only — they are never synchronized to clients.
When not to
- Pure view classes — effects, UI, camera scripts — are not entities and are never registered; they live entirely in engine code.
- Don't branch registration by game mode or scene: class ids must mean the same thing everywhere. Branching by side is allowed only in two supported forms — server-only AI controllers, and server/client subclass variants of a shared base (see below).
Minimal example
Shared code, executed identically by server and client:
GameTypes.cs
using LiteEntitySystem;
public enum GameEntities : ushort
{
Player,
PlayerController,
Projectile
}
public static class GameTypes
{
public static EntityTypesMap<GameEntities> Build() =>
new EntityTypesMap<GameEntities>()
.Register(GameEntities.Player, e => new BasePlayer(e))
.Register(GameEntities.PlayerController, e => new BasePlayerController(e))
.Register(GameEntities.Projectile, e => new SimpleProjectile(e));
}
How it works
The class-id enum
The enum is the wire contract: each member's numeric value becomes the entity's class id in every packet. Names are irrelevant to the network — values are not. Renaming a member is safe; reordering or inserting members shifts the values of everything after them and breaks compatibility with older builds.
Register and the constructor delegate
Register ties an enum member to an EntityConstructor<TEntity> — a delegate that receives EntityParams and returns a new instance, typically just e => new BasePlayer(e). The library calls it on the server when you spawn the entity and on the client when the entity first arrives in a state update. Register returns the map, so calls chain.
One map, two sides
Pass the finished map to the manager constructor. Registration must be identical on both sides: when the server sends an entity with class id 2, the client must have class id 2 registered as the same kind of entity with the same synchronized fields. The single exception is AiControllerLogic subclasses — they never sync, so the client may skip them (in the example project, the client registers everything except BotController).
Server and client variants of a type
Under the same enum id, each side may construct its own subclass of a shared entity base:
Registration per side
// shared assembly: class BasePlayer : PawnLogic { /* all SyncVars, RPCs */ }
// server assembly: class ServerPlayer : BasePlayer { /* server-only logic */ }
// client assembly: class ClientPlayer : BasePlayer { /* views, effects */ }
// on the server:
map.Register(GameEntities.Player, e => new ServerPlayer(e));
// on the client:
map.Register(GameEntities.Player, e => new ClientPlayer(e));
The synchronized layout — every SyncVar, SyncableField and RPC declaration — must stay in the shared base; the side subclasses add only logic. This is the supported way to keep server-only and client-only code in separate assemblies (separate .asmdef in Unity), so a client build ships without server logic and vice versa. Queries work through the base class: GetEntities<BasePlayer>() returns the ServerPlayer or ClientPlayer instances, since filters match entities by their base types as well.
The hash handshake
EvaluateEntityClassDataHash() computes a hash over the synchronized layout of every registered type — its SyncVar fields and RPC declarations, in class-id order, with server-only AI controllers excluded. Send it in your join packet and compare on the server; on mismatch, disconnect the client instead of letting two incompatible builds desync mid-game. The example does exactly this with its JoinPacket.GameHash.
Behavior details
The map is fixed at manager construction. Spawning a type that was not registered throws an exception immediately (Unregistered entity type). Class ids also size the internal per-type serializers, so the map must be complete before the manager exists.
Warning
Common mistakes
- Inserting or reordering enum members between releases — every id after the change shifts, and old builds construct wrong classes. Append new members at the end instead.
- Skipping the hash handshake — incompatible builds then fail with confusing per-entity errors and desync instead of one clean rejection at join.
- Registering different sets on client and server (other than AI controllers) — the missing side hits
Unregistered entity classas soon as such an entity syncs. - Declaring a
SyncVaror RPC in a server-only or client-only subclass variant — the synchronized layouts diverge and the hash handshake rejects the connection. Synchronized members belong in the shared base.