The goal
Low friction for players hopping on with each other — seamless connection for up to 8 players. When deciding on the authority model, server-authoritative stood out to me because of the “single source of truth” idea: it prevents cheating and provides a clean way to keep every client's information in sync.
Constraints
Two constraints shaped the design. Building solo with no server budget pushed me directly toward a P2P model rather than dedicated servers. And minimizing lag meant structuring objects to send as little information across the network as possible — which became a real challenge because GEODE's worlds are procedurally generated. Replicating all of that generated state across the network was a big issue at first.
The design
Rather than replicate the generated world, I shared a seed and the world-generation parameters with each client and let them generate the world on their own end — completely removing the need to sync generation across the network. On top of that, world objects spawn in as chunks load, similar to Minecraft, so undiscovered areas take up no processing or network bandwidth.
The server-authoritative model runs on ServerRPCs that handle any network-relevant change — the health of a mob or tower, the progress of the day/night cycle, and so on. The server stays the single source of truth, and clients defer to it for anything that affects shared state.
To keep things responsive, clients do some local prediction before sending ServerRPC data. Crucially, part of the hitbox logic — the color changes on hits and the sound effects — is handled client-side, so combat feels snappy instead of waiting on host confirmation.
Iteration
My first version had the host instantiate every world object as a NetworkObject. That guaranteed perfectly accurate replication across all clients — which sounded great until I hit the load times and heavy bandwidth usage that came with it, and the lag that followed. Next I tried spawning objects locally on each client and only “promoting” an object to a NetworkObject when its data actually needed to be shared — a tree taking damage, a structure breaking. But that introduced a noticeable delay every time a client interacted with something, so I scrapped it. I finally landed on a chunk-based object loading system. By no longer loading the entire world at once, it cut world-loading times dramatically and improved performance enormously.
The client-side hit feedback came straight out of playtesting. Players were immediately put off by the slight delay when hitting an object — and I agreed; smacking trees is a core gameplay element in any survival game, so it had to feel snappy. I moved to a local-prediction → server-confirmation flow, which worked great. I ended up taking the same approach for the item container systems, like sharing items between chests: predict locally, then confirm with the server.
Outcome
GEODE shipped with full multiplayer for up to 8 players — the original goal, fully met, and I'm very happy with where it landed. The biggest single win was the move to chunk-based loading. Before, large worlds took around 30 seconds to load and suffered noticeable FPS drops from all the network syncing eating into processing time. After, large worlds load in about 5 seconds, with no performance penalty compared to a small world.
What I'd change
Honestly, I'm no longer sure server-authoritative was the right call. GEODE is meant to be played with friends — there are no public lobbies — so cheating isn't really a concern. If players want to cheat in their own private session, that's fine by me; it's contained. A lighter authority model might have saved me a lot of complexity for very little real benefit.
The item container system is the clearest thing I'd redesign. The driving logic between item slots, chests, and inventories currently stores wrapped item data — essentially just displaying ScriptableObject data. That worked until I wanted to add durability and enchantments, which simply isn't possible with this implementation. What I actually needed was instanced item data: an editable, per-item layer so two items of the same type could carry different modifiers and durability values.
I'd also design the main-menu → gameplay → main-menu flow in much more depth. I didn't think enough about cleaning up world data when returning to the menu, which led to a lot of bugfixing around “second-generation” worlds within a single playthrough. I hadn't accounted for which data needs to persist from launch, through world creation, and back again.
And one constraint I'll own outright: art quality. I won't sugarcoat it — I'm not an artist, the game doesn't look visually amazing, and I'm okay with that. My focus was the systems, and that's where I wanted the project to shine.