First time I read about the Quake network model, it blew my mind.
Netcode is an all-encompassing term to define the logic managing your multiplayer… experience. It’s a lot.
I used to be a progamer in the early 2000s, mainly competing in Unreal Tournament, but did my tour of duty with Quake3. I knew what good netcode felt like, and knew how to play to its strengths, as a player. No idea what it took to actually make it.
ARMAJET was really my first foray into real netcode. I wanted to build a shooter with nostalgia for Gen X and early millennials, while showing Gen Z what their mobile phone was capable of.
What language to use? What protocol to use? What networking model to use?
If it’s your first time working on a multiplayer game, or your 10th, you’re probably asking the same questions. And there are lots of different answers.
This is the first article in a series that I look forward to publishing, covering our approach to Multiplayer and how we achieved our goals with ARMAJET.
Here’s the full set of articles if you want to jump right ahead:
ARMAJET was set out to prove that what John Carmack came up with in the 90s was still the best networking model to adapt to a modern shooter designed to run on a phone from 2010 (the iPhone4, at the time).
I didn’t write ARMAJET’s core networking libraries. They’re based off of Glenn Fiedler’s fantastic netcode.io / reliable.io open source work. He’s also a big Quake model fan. He contributed to the networking stack for Titanfall and Apex Legends, and I was fortunate enough to have him consult with us and show us what it took to deliver solid multiplayer that scales.
Our objectives were simple:
- Write a multiplayer-first game. You’re a client programmer? Now you’re a server one, too.
- Quake3 ran well on a wired 56k. Let’s make this run well on a 3G.
- Our client is built in Unity, so use C# for everything.
Writing Multiplayer first
The 90s were all about RTS and single player shooters - internet was nascent.
In the 2000s we saw quite a few studios hiring outside dev teams to “add” multiplayer to their single player shooter games. Domain knowledge was still niche, but multiplayer required less expensive content (to some degree) compared to lengthy dialogue-filled, cutscene-requiring campaigns, and with with far higher reusability. It’s what a growing audience wanted and devs started designing for multiplayer first, and for single player second.
Now, in the 20s (yup, we can say that) single-player FPS campaigns are being dropped left and right in favor of multiplayer-only, live-ops driven experiences. And, of course, the metaverse, which we’re all going to live in soon.
So, what does it mean to write multiplayer first?
- All core gameplay logic is server logic.
- Want offline? Start up your server, and play through a loopback interface.
- Want dedicated? Build server-only logic/dependencies and deploy.
- Want peer to peer? Start up your server, and punch your NAT.
Discussing P2P really warrants a separate discussion, and with server costs decreasing with cloud, mobile network limitations, battery concerns, and the insurmountable issues of latency, network instability, host migration, NAT/firewalls, it’s no wonder dedicated servers are the way to go.
Neon is the name we’ve given the stack we’ve build on top of netcode/reliable.io. This is what the full stack looks like, from bottom to top:
- ARMAJET Game Server
- ARMAJET Unity Client
Netcode.io deal with establishing connections over UDP with full-encryption with signed packets. Its minimal header size really keep things packed to a minimum, providing protection against man-in-the-middle attacks and packet replay.
Reliable.io is an application layer that, in short, recreates the reliability layer of TCP, by guaranteeing packet ordering and managing fragmentation. This is used for our chat and a few other latency tolerant features.
Neon is the next layer that we’ve built in-house, which is responsible for serializing the game state and producing delta snapshots to be sent to each client connected. We pack down things to the bit where possible, and this is what allows our game to run a 4v4 deathmatch on a 64kbit connection (8kb/s download, 3–4kb/s upload).
ARMAJET Game Server is the gameplay logic which contains all our custom physics, player and projectile entity simulations. It has a data layer that’s accessible by the Unity client so that a developer can create entities and configure them in editor, serializing the data to a non-Unity dependent configuration.
Unity Client handles rendering on the client hardware, capturing player input (keyboard, mouse, controller and touch) to be sent to the game server at regular intervals.
How we ship it
The game server binary is essentially all of the above, minus the Unity client, with zero Unity library dependencies. This means when we build the server, we run a C# compiler on full source code, and spit out a headless binary we can deploy. To give you a sense of how thin that is, the whole game server with content and configurations is less than 5Mb.
By now, we automate it all, so the right commit produces a new binary and automatically deploys to the right environment, with zero downtime, including live, in minutes. We’re pretty proud of that!
The client we ship for PC, Mac, iPhone and Android contains all of the above. All server code (minus some configurations) is included, so we can run the server locally when we need to. We don’t have a single player campaign in ARMAJET, but we do have a practice mode you can play offline against bots.
Had we built the game any differently, we would’ve had to construct a new way to handle player input and rendering changes to the screen, which would’ve been extremely expensive to produce and maintain in the long run.