DEV Community

Erol
Erol

Posted on

How I Sync Real-Time Multiplayer Game State with Socket.io and Node.js

I built a multiplayer board game that runs in the browser. Keeping everyone's screen in sync was the hardest part by far.

Here's how I did it in TileLord.

Server owns everything

Never trust the client. The client sends what the player wants to do. The server checks if it's valid, updates the game, and tells everyone what happened.

// Client
socket.emit("placeTile", { x: 3, y: -1, rotation: 2 });

// Server
socket.on("placeTile", (data) => {
  const result = game.tryPlaceTile(player, data);
  if (result.valid) {
    io.to(roomId).emit("tilePlaced", result.state);
  } else {
    socket.emit("actionRejected", { reason: result.error });
  }
});
Enter fullscreen mode Exit fullscreen mode

Even for a chill board game, people will try to mess with the payloads.

One room per game

Each game gets its own Socket.io room. Players join when they enter, and updates only go to that room.

socket.join(`game:${roomId}`);
io.to(`game:${roomId}`).emit("gameStateUpdate", sanitizedState);
Enter fullscreen mode Exit fullscreen mode

Don't send everything though. If your game has hidden info (like a tile deck), strip that out before sending. Each player should only see what they're allowed to see.

Log every action

I store every move as an event instead of just keeping the current state:

actionLog.push({
  type: "TILE_PLACED",
  playerId,
  position: { x, y },
  rotation,
  timestamp: Date.now(),
});
Enter fullscreen mode Exit fullscreen mode

You get a lot from this:

  • Replays - play the log forward
  • Reconnects - rebuild what a player missed while offline
  • Debugging - replay the exact sequence that caused a bug

The replay feature took about a day to build and players use it all the time. Worth it.

Disconnects are normal

People lose wifi. Their phone locks. Their laptop sleeps. Handle this from day one.

Turn timeouts: if someone doesn't move in time, a bot takes their turn so the game keeps going.

const turnTimer = setTimeout(() => {
  botPlayer.takeTurn(game);
  io.to(roomId).emit("botMove", game.getState());
}, TURN_TIMEOUT_MS);
Enter fullscreen mode Exit fullscreen mode

Reconnects: when they come back, send them the current state.

socket.on("rejoinGame", ({ roomId, playerId }) => {
  const room = rooms.get(roomId);
  if (room) {
    socket.join(`game:${roomId}`);
    socket.emit("fullState", room.game.getStateForPlayer(playerId));
  }
});
Enter fullscreen mode Exit fullscreen mode

Spectators are free

Once you have rooms, spectators just join without being able to send game actions. They get the same updates as players.

TL/DR

  • Server decides everything. Clients just ask.
  • Socket.io rooms, one per game.
  • Log every action. Replays and debugging come for free.
  • Handle disconnects early. They will happen constantly.

Get the sync layer right first. The rest falls into place after that.


I'm building TileLord at tilelord.com, a free Carcassonne alternative you can play with friends or bots online.

Top comments (0)