< cd /devlog
$ cat /devlog/multiplayer-is-alive.md

Multiplayer Is Alive

Two weeks of refactoring, networking bugs, and silent assumptions — how Revision went from single-player prototype to working multiplayer.

unity gamedev multiplayer devlog

The past two weeks turned Revision from a single-player prototype into a working multiplayer game. The journey was mostly debugging assumptions I didn’t know I’d made.


Where it started

By February 11th, the single-player game worked. Skills, turret placement, modules, inventory with drag-and-drop, wave combat, XP, coins, save/load. Two scenes, one loop. It played fine alone.

Then I added networking.

Adding multiplayer (Feb 13-15)

First step was refactoring the module system to be data-driven with a central ModuleRegistry. This made serializing module data across the network less painful later.

The actual multiplayer implementation landed on February 14th. Unity Netcode for GameObjects, Relay and Lobby services. Host creates a game, gets a join code, client enters code and connects. Both load into the Gameplay scene together.

The session UI ended up inline with the inventory screen—no separate lobby view, just a session info bar showing join code and player count. This way players could manage loadout while waiting.

I also had to extract a CharacterModel component. The original approach of baking the 3D model directly into the character setup broke when prefabs started being created programmatically at runtime for NGO’s registration system. Player visibility and models needed to be managed independently from network sync.

The bug gauntlet (Feb 16-20)

This is where most of the time went. Getting two players into a game took a day. Getting them to play together without things breaking took four more.

Movement sync: rb.MovePosition() on kinematic Rigidbodies doesn’t work with NetworkTransform—it defers to the physics step so transforms read stale positions. Switched to setting transform.position and rb.position directly.

Bullet system: Complete rethink. Bullets can’t be NetworkBehaviours because they’re spawned at runtime via AddComponent<Bullet>() on primitives. The solution: owner shoots, sends ServerRpc with position and direction, server spawns the authoritative bullet and broadcasts ClientRpc so clients render a visual-only copy.

Camera targeting: Subtle race condition. FindGameObjectWithTag("Player") returns the host’s player on the client because host spawns first. Switched to LocalClient.PlayerObject everywhere.

Scene transitions: Playing a second game after returning to lobby would crash. NGO’s scene management was destroying the DontDestroyOnLoad prefab templates during scene loads. Fixed by re-registering prefab templates on all clients when entering Gameplay scene.

Phantom bullets: AutoShoot components were checking their local NetworkObject reference to decide if they should run. That reference could become null during scene transitions while the component survived. The guard failed silently and every enemy started shooting locally on every client. Fixed with a global NetworkManager.Singleton.IsServer check and disabling AI components entirely on non-server clients in OnNetworkSpawn.

Client inventory: Two separate issues. First, FindLocalPlayer() was returning the host’s player object on the client due to tag-based fallback, so items populated on the wrong player—client could see them in UI but couldn’t use them. Second, client never saved loadout before scene transitions, so items vanished after returning to lobby. Added inventory caching in GameManager with fallback saves on destroy, and made MainMenuManager save loadout in OnDestroy.

Turret spawning: Broke for host because FindObjectOfType<TurretSpawner>() was only called once during PlayerInput initialization. If timing was off it came back null. Added lazy-find fallback.

Item sync: Needed a dedicated manager. Created ItemSyncManager as server-authoritative NetworkBehaviour handling item spawn/despawn via ClientRpc, with catch-up for late-joining clients.

Where it stands

Two players can host and join, fight through waves together, collect items, use weapons and skills, return to lobby with progress saved. It’s not polished—more edge cases are definitely lurking—but the core multiplayer loop works.

The architecture ended up hybrid authority: players are owner-authoritative (each client controls their own movement), enemies and items are server-authoritative, health is server-authoritative via NetworkVariables, and bullets are replicated through RPCs rather than being networked objects.

Most of the work wasn’t writing networking code. It was finding and fixing all the assumptions single-player code makes—that there’s only one player, that FindGameObjectWithTag returns your player, that components are alive when you expect them, that scene transitions are instant and clean. Multiplayer breaks all of those assumptions, usually silently.

What’s next

The multiplayer foundation exists. The game needs content—more weapons, enemy types, depth in the modular system. The facility setting and narrative layer from the concept doc are still waiting. But at least now, two test subjects can die together.

< cd /devlog

// end of transmission

EOF