“Can you add multiplayer?” is probably the question indie developers hear most, and it is also the one that gets underestimated fastest. Once a game goes networked, input handling, game state, UI, testing and deployment all get harder. There is no polite way around that.

For retro-style 2D games, though, the problem is usually less ugly than it is in 3D games with heavier physics and animation. Multiplayer still changes the shape of the project, but the technical load is often more manageable. At Relish Games, the approach is pragmatic: build for what the game actually needs, and be clear about the trade-offs.

Networking architectures that fit 2D games

Client-server

In a client-server setup, one machine - the server - is authoritative over game state. Every client sends inputs and receives state updates.

The upside is straightforward. The server can validate actions, so cheat prevention is stronger. Everyone reads from the same source of truth, which keeps game state consistent. Player count also scales more cleanly, because you are not multiplying peer connections every time someone joins.

The costs are just as clear. Hosting is not free, the server can fail, and every player pays at least one server-trip of latency.

It suits competitive games, games with more than four players, and any project where fairness matters more than shaving a few milliseconds off the feel.

Peer-to-peer

Peer-to-peer keeps all machines talking directly. Each peer handles its own entities.

That cuts out server hosting costs and usually gives lower latency between players because the connection is direct. It can also be simpler to get running at first.

The trouble starts later. Cheat prevention is harder. Connection count grows quadratically as player count rises. NAT traversal is a nuisance. Desyncs are also harder to untangle once they appear.

It works best for cooperative games, local or LAN games, two-player experiences, and anything where players are trusted to behave.

Rollback netcode

Rollback is a specific technique used mostly in fighting games and other fast-action games. Each client runs its own simulation and predicts the opponent’s input. When the real input arrives and does not match the prediction, the game rolls back to the last confirmed state and re-simulates.

The appeal is obvious: the game feels responsive even with moderate latency, because local input is applied immediately.

The price is complexity. The simulation has to be deterministic, and visual rollbacks become noticeable when latency gets high.

Rollback is best reserved for 1v1 or small-player-count action games where responsiveness matters more than implementation simplicity.

Keeping game state in sync

The hard part is not sending data. It is keeping every player’s view of the game aligned.

Full state sync

This sends the complete game state on every update. It is simple, and sometimes that is exactly why it is the right starting point.

It fits small-state games such as card games, turn-based games and puzzle games. It falls apart quickly once the game has hundreds of entities or frequent updates.

Delta compression

Delta compression sends only what has changed since the previous update. That cuts bandwidth sharply.

In practice, each update includes only the modified entity properties, and clients apply those deltas to their local copy of the state.

The catch is packet loss. If a delta does not arrive, the client’s state can drift. Periodic full-state snapshots are the usual safety net.

Input synchronisation

Instead of syncing state, sync inputs. Every client runs the same simulation with the same inputs and reaches the same result.

That only works if the simulation is perfectly deterministic. The same input has to produce exactly the same state on every machine, so floating-point inconsistencies, unordered iteration and system-dependent randomness are out.

For simpler 2D physics, deterministic simulation is achievable. Fixed-point math or tightly controlled floating-point handling, plus a seeded PRNG, provides the base. The same seeded PRNG approach is also used in procedural content generation.

Latency compensation without hand-waving

Networked games always show a slightly out-of-date view of the world. The only question is how visible that delay becomes.

Client-side prediction

Client-side prediction lets the local machine simulate the player’s own actions immediately, without waiting for the server. If the server agrees, nobody notices the latency. If it does not, the client corrects to match the server.

For 2D games, movement prediction is usually straightforward because the same movement logic can run on client and server. Shooting is the awkward bit. Does the shot count against where the target was on the shooter’s screen, or where it was on the server? That choice matters.

Entity interpolation

Other players do not arrive continuously. They arrive in chunks. Between updates, their positions are smoothed by interpolation:

Render position = Previous position + (Current position - Previous position) × interpolation_factor

That adds a small visual delay - one update interval - but it avoids the jerky movement raw network updates create.

Input delay

The simplest form of latency compensation is to add a few frames of input delay. Inputs are buffered briefly so there is enough time for the network round-trip before the game applies them.

The trade-off is plain. It feels a little less responsive, but it avoids rollbacks and corrections. Many successful fighting games use 2-3 frames of input delay.

What changes in 2D, and what does not

2D simplifies some of the work

There are usually fewer simultaneous entities than in 3D, so bandwidth needs are lower. Collision and movement are easier to make deterministic. Position often means two floats instead of three, rotation is usually a single float rather than a quaternion, and sprite state is compact - current animation frame and facing direction are small values.

2D does not make the hard parts disappear

Input responsiveness still matters. 2D action games need tight control no matter how simple the networking stack looks on paper.

Visual desyncs are also more obvious. In a pixel-precise game, jitter and teleporting stand out immediately. Then there is design fit: some single-player systems do not translate cleanly to multiplayer at all, especially pause, time manipulation and a camera that follows one player.

A practical way to build it

Step 1: Make the game state serialisable

Before any networking code goes in, the game state needs to be capturable and restorable:

struct GameState {
    EntityState entities[MAX_ENTITIES];
    int entityCount;
    int frameNumber;
    uint32_t checksum;
};

If you can save and load the full state, you can move it across a network as well.

Step 2: Separate input from game logic

Input should be abstracted away from hardware events:

struct PlayerInput {
    bool moveUp, moveDown, moveLeft, moveRight;
    bool fire;
    int aimX, aimY;
    int frameNumber;
};

Game logic should consume PlayerInput structs, not raw controller or keyboard input. That makes it possible to feed network-received inputs into the same logic path.

Step 3: Ship local split-screen first

Two-player local play is a useful checkpoint. It validates input abstraction and state management before networking complicates the picture.

Step 4: Add networking on top

Choose a networking library rather than writing the transport layer from scratch. ENet is a lightweight reliable UDP library and a solid fit for games. SteamNetworkingSockets is free, handles NAT traversal and includes relay servers. Custom UDP gives maximum control, but it also means maximum work.

Start with a simple architecture such as lock-step or client-server with full state sync. Optimise later.

Step 5: Test on real networks

Local testing hides the problems that matter. Use real network conditions, or simulate them with artificial delay and packet loss.

When multiplayer earns its keep

Worth the work

Multiplayer makes sense when the core loop is social, such as cooperative puzzles or competitive combat. It also makes sense when the target audience expects it - fighting games and party games are obvious examples - or when multiplayer materially extends the game’s lifespan. The final condition is less glamorous but just as real: the team has to be able to maintain it after launch.

Not worth the detour

If multiplayer is only there as a checkbox, leave it out. The same applies when the game is mainly a single-player narrative experience, when there is no plan to maintain servers or matchmaking, or when the engineering cost would push back a game that is already strong as a single-player title.

Be honest about capacity. A single-player game that ships beats a multiplayer game that never quite lands.

What to do first

Design for multiplayer from the start if it is a core feature. Retrofitting it later is painful.

Start with cooperative play rather than competitive play if the game allows it. Sync requirements are usually more forgiving.

Use an existing networking library. Writing your own UDP layer is a fine way to spend time on plumbing instead of the game.

Test with real latency early, not after launch. And keep the single-player experience in good shape even if multiplayer is the headline feature. People still play when their friends are offline.

Networking is messy, but in 2D the scope is manageable if the work is methodical.

Discuss multiplayer implementation with other developers in our community forum, or explore our projects page to see how we approach game architecture.