In a multiplayer game, if two players stand in the same room and one sees the enemy dead ahead while the other sees it three steps to the left, the problem almost always lives in the game state synchronization layer. The "world state" shared by the server and the clients drifts apart over time, and that drift becomes visible. In this article I explain how we build cross-client consistency: the authoritative server model, tick-based updates and interpolation, framed in practical terms.
What is state, and why must it be synchronized?
From a game's point of view, state is the snapshot of the world at a given instant: the positions, velocities, health and inventories of players, NPC behavior, open doors, dropped items. In a single-player game this picture lives in one region of memory and no inconsistency is possible. In a networked game, however, each client keeps its own copy of the state and the server keeps its own. Because the speed of light is finite, these copies can never update at the same moment; our goal is not to eliminate inconsistency but to keep it small and short-lived enough to go unnoticed.
Synchronization solves two core questions: whose word counts as "true," and how do we fill in the intermediate frames? The first is a matter of authority, the second of interpolation.
The authoritative server: a single source of truth
The foundation of modern multiplayer architecture is the authoritative server model. Here the single source of truth is the server. When a client presses a key it does not say "I am at position X right now"; it sends an input such as "I want to move forward." The server processes that input in its own simulation, computes the result, and broadcasts the updated state back to all clients.
The biggest benefit of this approach is cheat resistance. The client cannot directly write state like "the enemy's health is 0" or "I'm inside the wall"; it only states intent, and the server makes the decision. The same principle holds for MMORPG servers like Metin2: damage, drops and position validation happen server-side, otherwise a client editing its own memory could break everything.
- Client → server: sends only input/commands (move direction, attack, use item).
- Server: runs the simulation, applies collisions and rules.
- Server → clients: broadcasts the new state snapshots.
Tick-based simulation
The server does not advance the world continuously but at fixed intervals. Each step is called a tick. A 20 tick/s server updates the world 20 times per second (every 50 ms). The reason for using a fixed tick is determinism: when the same inputs are processed in the same order with the same time step, the result comes out identical on every machine.
// Fixed time-step server loop (simplified)
const double TICK_RATE = 20.0; // ticks per second
const double DT = 1.0 / TICK_RATE; // 0.05 s
double accumulator = 0.0;
double previous = now_seconds();
while (running) {
double current = now_seconds();
accumulator += current - previous;
previous = current;
while (accumulator >= DT) {
process_inputs(); // apply client inputs
step_world(DT); // physics + rules
accumulator -= DT;
}
broadcast_snapshot(); // push state to clients
}
Tick rate is a balancing act. A high tick (e.g. 60) gives smoother, more precise gameplay but raises bandwidth and CPU cost. Fast-paced shooters demand a high tick; for an MMO, 10–20 ticks are usually plenty.
Snapshots, deltas and bandwidth
Sending the entire world to every client on every tick is wasteful. There are two common optimizations:
- Delta compression: send only the fields that changed relative to the previous snapshot. No data flows for a stationary NPC.
- Area of interest: send a player only the entities within their view/influence range. They don't need to know about a fight on the far side of the map. At MMO scale this is non-negotiable.
You should also serialize only the fields that matter over the network (position, rotation, animation state, health) rather than all of them. Quantizing position into a bounded range (e.g. 16 bits) instead of sending it at full precision shrinks packets considerably.
Hiding latency: interpolation and prediction
Even if snapshots arrive only 20 times a second, the player wants to see smooth motion on a 144 Hz display. There are two techniques for filling the gaps.
Interpolation (for remote players): the client smoothly samples position between the last two received snapshots. To do this it deliberately renders a small buffer (e.g. 100 ms) behind, so it always has two real data points to transition between.
// Linear interpolation between two snapshots
Vec2 lerp(const Vec2& a, const Vec2& b, float t) {
return { a.x + (b.x - a.x) * t,
a.y + (b.y - a.y) * t };
}
// t: ratio of the render moment between the two snapshot times (0..1)
Client-side prediction (for your own player): making your own character wait for the server's reply creates annoying input lag. Instead, the client applies the input locally at the same time it sends it to the server. When the official state arrives from the server, if the prediction was correct there is no difference; if it was wrong, reconciliation fixes it: the client rewinds to the server-confirmed state and replays the inputs not yet acknowledged. If there is a mismatch you see a small "teleport"; to soften it, the correction is blended over a few frames.
Sources of inconsistency and practical tips
- Packet loss: over UDP, snapshots can be lost. Use a reliable channel/acknowledgment mechanism for critical events (death, item pickup); don't try to send continuous data like position reliably, since the next snapshot already brings fresh data.
- Clock skew: client and server clocks drift. Stamp snapshots with a server tick number/timestamp, and interpolate against that time rather than the wall clock.
- Float non-determinism: floating-point results can vary slightly across compilers/platforms. If you need full determinism, consider fixed-point arithmetic; most authoritative servers don't need it because the source of truth is singular.
- Testing: inject artificial latency and packet loss during development (with
tc netemon Linux). A system that feels good at 200 ms latency will be flawless at 20 ms.
Frequently Asked Questions
Should I use TCP or UDP?
For fast-paced, position-heavy games UDP is generally preferred, because TCP's in-order delivery guarantee makes new packets wait while an old lost packet is retransmitted (head-of-line blocking), inflating latency. For turn-based or latency-tolerant games TCP is more than enough. Many engines build their own reliability layer on top of UDP.
How high should I set the tick rate?
It depends on the gameplay. In shooters where reaction time is critical, 30–64 ticks are common; in MMOs and strategy games 10–20 ticks are sufficient and more scalable. Decide by measuring the gameplay feel first, then the server cost.
Does client-side prediction create a cheating risk?
No, because prediction is only a visual estimate; the final decision is always made on the authoritative server. If the client's local prediction conflicts with the server, the server's state is considered valid and the client is corrected.
Building a multiplayer server? Let's plan the authoritative architecture, tick design and latency-hiding strategies together, tailored to your project's genre. Get in touch with me and let's talk about what you need.