#103 Mirror vs other Unity networking

We ship Ball-Aqua and COLLAPSE MACHINE on Mirror. Still happy. This is the cheat sheet of operations we actually write, next to Unity Netcode for GameObjects (NGO 2) and Fish-Net.

Not in the table: Photon (rooms + string RPCs) and Fusion (ticks, interest, paid - a different job).

Mirror 81.4 in Ball-Aqua. Listen server, Steam host, Telepathy for local editor play.

ThingMirrorNGO 2Fish-NetCaveats
Start host or clientStartHost() / StartClient()NetworkManager.Singleton.StartHost() / StartClient()ServerManager.StartConnection() + ClientManager.StartConnection()Host is a listen server (server and a client). Register spawn prefabs before start. Mirror's StartHost is not virtual - hide with new if you need a hook. Transport must be ready (tiny delay after Steam / Fizzy). Port already in use: catch SocketException, or just StartClient when testing two editors.
Send command to server[Command] CmdFoo()[Rpc(SendTo.Server)][ServerRpc]Needs authority (usually the owner). Host: the Command still runs on the server side. Don't call it from objects you don't own.
Send command to one client[TargetRpc] + connectionToClient[Rpc(SendTo.Owner)] or RpcTarget.Single(id)[TargetRpc]TargetRpc needs the connection. Host-as-player has connectionToClient; a server-only object may not. Ball-Aqua ball bumps: TargetRpc the owner, else apply locally if that ball is the host.
To all clients[ClientRpc] RpcFoo()[Rpc(SendTo.Everyone)][ObserversRpc]Host is also a client, so ClientRpc runs on the host too. Don't also apply the effect locally or you double it. Chat in Ball-Aqua: Command sanitizes, then ClientRpc to everyone.
Sync script variable[SyncVar] / [SyncVar(hook = ...)]NetworkVariable<T>SyncVar<T>Server start: set in OnStartServer. Set: assign on the server (or via Command) - hook fires for everyone already in. Client start: the value is already set; the hook does not auto-fire for a new joiner (intentional). Call the apply yourself in OnStartClient only if you want that side effect. Clients don't write SyncVars.
Spawn entityNetworkServer.Spawn(go)NetworkObject.Spawn() / InstantiateAndSpawnServerManager.Spawn(go)Server only. Prefab needs NetworkIdentity (or NGO NetworkObject) and must be on the spawn list. Player objects: NetworkServer.AddPlayerForConnection. Scene objects already in the scene are a different path.
Destroy entityNetworkServer.Destroy(go)NetworkObject.Despawn()ServerManager.Despawn(go)Server only. Mirror Destroy is deferred - hide with SetActive(false) the same frame if something still scans the scene. COLLAPSE uses UnSpawn when an entity leaves interest, so it can come back without a full destroy.
Get all entitiesNetworkServer.spawned / NetworkClient.spawnedSpawnManager.SpawnedObjectsServerManager.Objects.SpawnedDon't FindObjectsByType every frame. Server dictionary is identities the server spawned; client dictionary is what this client knows. Players: walk NetworkServer.connections and use conn.identity.
Get local playerNetworkClient.localPlayerLocalClient.PlayerObjectClientManager.Connection.FirstObjectNull until the player object exists. Host has a local player too. isLocalPlayer on the behaviour is the usual check.

SyncVar: server start, set, client start

[SyncVar(hook = nameof(OnModeChanged))]
PlayerCollisionMode syncedMode;

public override void OnStartServer()
{
    syncedMode = PlayerCollisionMode.All; // default lives on the server
}

[Command]
void CmdSetMode(PlayerCollisionMode mode)
{
    syncedMode = mode; // hook fires for everyone already connected
}

public override void OnStartClient()
{
    ApplyMode(syncedMode); // joiner: value is set, hook never auto-fires. Call apply only if you want the effect.
}

void OnModeChanged(PlayerCollisionMode _, PlayerCollisionMode next) => ApplyMode(next);

From the games

Ball-Aqua - Steam lobby host, up to 99 in Freeroam. Telepathy in the editor, FizzySteamworks on Steam. Level props go on spawnPrefabs right before StartHost because Enter Play Mode without scene reload skips Awake and leaves a stale list. Player list comes from connections / NetworkClient.spawned, not a scene search. Local player is NetworkClient.localPlayer once the ball exists.

COLLAPSE MACHINE - after Steam + entity registry: StartClient if we have a saved IP, else StartHost. Character is not the default player prefab auto-spawn; after auth the client sends a NetworkMessage and the server AddPlayerForConnection. Open world interest: UnSpawn when nobody is looking, spawn again from save. Same Commands / ClientRpcs / TargetRpcs / SyncVars as the ball game.

Why we stay on Mirror

The API is the old UNET shape. We already think in Command / ClientRpc / SyncVar / spawned. Host-as-listen-server matches how we ship Steam sessions. Open source, no seat license, transports we can swap.

NGO 2's unified [Rpc(SendTo.)] is cleaner on paper. Fish-Net is the closest cousin and often faster. Neither is a reason to rewrite two shipping codebases that already do the job.

Looked through both projects. Mirror usage is the boring kind: attributes, spawn list, dictionaries. No smoking gun.