Lua for Game Development โ€” Chapter 16: Multiplayer, Netcode, Sync Models & Lag Compensation

A complete, production-grade guide to multiplayer netcode using Lua: lockstep, snapshot interpolation, client prediction, reconciliation, entity replication and lag compensation.

Multiplayer is the hardest part of game development.
This chapter covers the exact models used by:

  • Fortnite
  • Apex Legends
  • Overwatch
  • Rocket League
  • Factorio
  • StarCraft II
  • Roblox
  • Unity Netcode / Unreal replication

You will learn the following sync models:

  1. Lockstep (deterministic strategy / SLG)
  2. Snapshot interpolation (action games)
  3. Client-side prediction & reconciliation
  4. Entity replication (position/rotation/state)
  5. Combat sync
  6. Bullet/projectile sync
  7. Lag compensation (server rewind, hit-scan)
  8. Anti-cheat considerations

Lua is excellent for netcode:

  • lightweight simulation
  • deterministic math (if careful)
  • fast serialization
  • cross-platform
  • embeddable
  • extremely flexible

1. Networking Architecture Overview

NEVER trust the client.
Server is always the authority.

A typical architecture:

Client 1 โ†’
โ†’ Dedicated Server (Lua Simulation) โ†’ Game State
Client 2 โ†’

Clients send inputs, not authoritative states.

Server sends snapshots or replicated entity data.

2. Netcode Models Overview

Model Use Cases Pros Cons
Lockstep RTS, SLG, turn-based Deterministic, low bandwidth Requires 100% determinism
Snapshot+Interp Action games, shooters, platformers Smooth, flexible Higher bandwidth
Client Prediction + Reconciliation Fast action / shooter Very responsive More complex
Authoritative Server w/ Replication General MP design Secure, scalable Requires server tick

We will implement all of them.

3. Lockstep Simulation (SLG / RTS)

Used by:

  • StarCraft
  • Dota
  • Factorio
  • CoH
  • Many SLGs

Idea:

  • All clients run the same deterministic simulation.
  • Every frame, clients send commands, not states.
  • Server distributes the authoritative command list.
  • Every machine simulates the same world โ†’ perfect sync.

3.1 Tick Structure

-- lockstep tick:
function tick()
  apply_commands(current_frame_commands)
  simulate_world(FRAME_TIME)
end

3.2 Command Package

{
  frame = 120,
  player_id = 1,
  command = {type="move_to", x=200, y=160}
}

Server collects all commands per frame:

pending_commands[frame][player_id] = command

When all players submit commands โ†’ advance tick.

3.3 Determinism Rules

To keep all clients synced:

  • NO floating-point randomness (use seeded RNG)
  • Use integer or fixed-point math
  • No iteration over unordered tables
  • No using pairs() for critical loops (use indexed arrays)
  • Avoid OS time
  • No math.random() unless seeded identically

Lua is deterministic if you follow strict rules.

4. Snapshot Interpolation (Action Games)

Used by:

  • Overwatch
  • Fortnite
  • Call of Duty
  • Apex Legends
  • Most realtime games with large movement

Server sends world snapshots at N Hz:

20 snapshots/sec 
60 Hz client interpolation

4.1 Snapshot Example

snapshot = {
  time = 1234.56,
  entities = {
    {id=1, x=200, y=150, vx=10, vy=0, hp=80},
    {id=2, x=450, y=300, vx=0, vy=0, hp=100},
  }
}

Serialized and sent by UDP (ENet, RakNet, custom socket).

4.2 Client Interpolation Buffer

Clients keep last N snapshots:

buffers[id] = {
  {time=t0, x=..., y=...},
  {time=t1, x=..., y=...},
}

4.3 Interpolation

function interpolate(a, b, ratio)
  return a + (b - a) * ratio
end
entity.x = interpolate(s0.x, s1.x, t)
entity.y = interpolate(s0.y, s1.y, t)

Smooth gameplay even with 100ms ping.

5. Client Prediction & Server Reconciliation

Used by fast games:

  • shooters
  • beat-em-ups
  • platformers
  • physics-based games

Principle:

  1. Client predicts movement immediately.
  2. Sends inputs โ†’ server simulates authoritative version.
  3. Server sends back correct state.
  4. Client rewinds to server state, replays unacknowledged inputs.

5.1 Client Prediction Code

function client_update(dt)
  apply_input(player, input, dt)
  record_input(input)
  send_to_server(input)
end

5.2 Server Response

server_state = {
  tick = 1052,
  player_state = {x=100, y=200}
}

5.3 Reconciliation

-- rewind to server state
player.x = server_state.x
player.y = server_state.y

-- replay unacknowledged inputs
for _, inp in ipairs(unconfirmed_inputs) do
  apply_input(player, inp, dt)
end

This is how Apex, Rocket League, and CS:GO work.

6. Entity Replication (Server โ†’ Client)

For each entity:

  • id
  • position
  • velocity
  • state (jumping, shooting, reloading)
  • animation
  • health

Server sends minimal changes via delta compression.

6.1 Replication Structure

replicate = {
  id=12,
  pos={x=100,y=150},
  vel={x=0,y=0},
  state="run",
  hp=90,
}

Clients update their local entity copies.

7. Lag Compensation (Server Rewind)

Used in hit-scan shooters:

  • server rewinds to historical position
  • checks if shot hit in the past

Supports fair combat with latency.

7.1 Server Stores N Frames of History

history[tick][entity_id] = {x,y}

7.2 Hit-Scan Check

function server_hit_scan(client_time, origin, dir)
  local snapshot = history[client_time]
  check_line_intersection(snapshot, origin, dir)
end

8. Projectile Sync (RTS, RPG, Shooter)

Two models:

  1. Server-simulated projectiles (bullet has physics)
  2. Client-only view projectile (visual only, hit registered by server)

For fast shooters, bullets are not real objects โ€” only hit-scan.

For RPGs and SLGs, projectiles are server-side entities:

projectile = {
  id=pid,
  x,y,vx,vy,
  owner=player_id,
}

9. Combat Sync

Combat actions:

  • melee
  • ranged attack
  • cast ability
  • spell effects
  • area damage
  • buffs/debuffs

Server must verify:

if not is_in_range(attacker, target) then return end
if target.hp <= 0 then return end

10. ANTI-CHEAT Essentials

Do NOT trust:

  • client HP
  • client inventory
  • position changes without inputs
  • attack commands without cooldowns
  • client damage numbers

Server must check:

  • physics
  • movement constraints
  • cooldowns
  • hit validity
  • item ownership
  • range/distance checks

11. Lobby, Matchmaking & Rooms

Room server structure:

rooms = {
  [room_id] = {
    players = {},
    world = {},
    tick = 0,
  }
}

Server updates room:

function room_tick(room, dt)
  update_world(room.world, dt)
  send_snapshots(room.players)
end

12. Putting It All Together โ€” Full Multiplayer Loop

Client Loop

function client:update(dt)
  collect_input()
  predict_position(dt)
  send_input_to_server()
  process_server_snapshots()
  reconcile()
  interpolate_other_players(dt)
end

Server Loop

function server:update(dt)
  receive_inputs()
  simulate_world(dt)
  send_snapshots()
  update_history()
end

13. Summary of Chapter 16

You now understand:

  • Lockstep deterministic simulation
  • Snapshot interpolation
  • Client prediction & reconciliation
  • Delta-based replication
  • Lag compensation
  • Projectile and combat sync
  • Anti-cheat fundamentals
  • Server architecture for rooms/lobbies
  • Full client and server update loops

This is AAA-level netcode, implemented in Lua.

Keep Reading

Follow the engineering thread

Get the next practical Birdor note, or browse the archive for related systems, tooling, and architecture work.

Join newsletter Browse articles