Publish

Make games for PixelJS

Tutorial: make an online multiplayer game for PixelJS

Online play is optional: add it to a game when it makes sense, from 2 up to 16 players per room. This tutorial uses the online starter, a small game of tag for 2 to 4 players, and explains each step so you can apply it to your own game. Read the single-player tutorial first if you have not made a PixelJS game before.

How online play works on PixelJS

Games on PixelJS have no network access: they run in a sandbox. For online play, the portal page holds the connection and relays your game's messages to the other players' pages.

The portal does Your game does
Signs players in, finds players (quick match) and runs private rooms with invite codes and links Shows its "Play online" buttons and its own game screens
Shows its waiting and room screens, with block and report buttons Decides what messages to send and how to keep players in sync
Gives every match the same random seed, numbers the players (slots) and picks a host Uses the seed, the slots and the host to run the match
Relays messages, within the size and rate your manifest declares Keeps its messages small and few

There is no chat on PixelJS, and games must not add one: no free text, names or links between players. Fixed phrases and emotes chosen by your game are fine.

1. Get the online starter

Download the online starter from the developer page, unzip it and run:

npm install
npm run dev

Outside the portal you can play a practice round against two simple bots. Online play itself works on pixeljs.com, where players are signed in.

2. Declare online play in pixeljs.json

"capabilities": ["pause", "mute", "multiplayer"],
"multiplayer": {
  "min_players": 2,
  "max_players": 4,
  "modes": [{ "id": "tag", "name": "Tag" }],
  "quick_match": true,
  "private_rooms": true,
  "join_in_progress": false,
  "max_message_bytes": 512,
  "max_messages_per_second": 20
}
  • min_players, max_players: the room size, 2 to 16. Your game page shows it ("Online · 2–4 players").
  • modes: optional, each with its own size within the section's (for example a 2-player "Duel" next to a 4-player "Free for all").
  • quick_match: the portal matches strangers automatically. private_rooms: a player creates a room and invites friends with a code or a link.
  • join_in_progress: whether quick match may add players to a match that already started.
  • max_message_bytes, max_messages_per_second: the relay's limits for your game (at most 4,096 bytes and 60 messages a second per player). Messages beyond them are dropped.

Online play works together with everything else: the same game can have levels and leaderboards for its solo mode.

3. One game, several ways to play

Board and card games are often played three ways: against the computer, by two people on the same device, and online. Declare the modes your game has, and the portal asks the player before the game starts:

"play_modes": ["solo", "local", "online"],
"local_players": 2

The game page then shows one button per mode (1 player, 2 players on this device, Play online), and your game reads the choice when it starts:

switch (portal.launch.mode) {
  case 'local':  startMatch({ players: portal.launch.players }); break;   // hot seat: players take turns
  case 'online': showOnlineMenu(); break;                                 // quick match or private room (step 4)
  default:       startMatch({ against: 'computer' });                     // "solo"
}
  • Without play_modes, a game with a multiplayer section has the modes solo and online.
  • Outside the portal, portal.launch.mode is always "solo": keep your own menu so players can choose a mode anywhere.
  • For a two-player game such as chess, checkers or dominoes, set "min_players": 2, "max_players": 2. Turn-based games stay simple: send each move ({ t: 'move', from, to }) to the other player and let both games apply it; use the seed to decide who starts.
  • Local matches report no results; solo runs keep using levelStart and levelEnd.
  • Starting a board game? npm create @pixeljs@latest my-game -- --template board creates a two-player game with the three modes, built around a small rules file (tic-tac-toe as a placeholder) that you replace with your game's rules.

4. Offer online play in your menu

const online = portal.multiplayer;

if (online.available) {
  menu.add('Quick match', () => online.find({ mode: 'tag' }));
  menu.add('Private room', () => online.host({ mode: 'tag' }));
} else {
  menu.add('Practice', () => startPractice());   // outside the portal, offline
}
  • find() puts the player in the quick-match queue. The portal shows "Looking for players…" with a Cancel button, then sends your game a room and a start event.
  • host() creates a private room. The portal shows the room code, an invite link (your game page with ?room=CODE) and a Start button for the host. Friends who open the link join the room directly.
  • Guests are asked by the portal to sign in; the call answers { ok: false, reason: "guest" }.

5. Listen to the room and the start of the match

Register these handlers when your game starts, not when the player opens your online menu: a player who opens an invite link joins a room without going through your menu.

online.on('room', (room) => {
  // { code, mode, private, state: "lobby" | "playing" | "ended",
  //   host, me, min, max, players: [{ slot, handle, avatar, ready }] }
  lobby.show(room);
});

online.on('start', (match) => {
  // match = the room, plus the seed every player received
  random = createRandom(match.seed);    // the same random numbers for everyone
  startMatch(match.players, match.me, match.host);
});
  • Slots number the players of a room (0, 1, 2…). me is your slot; host is the host's slot.
  • The seed is the same for every player of a match. Use a seeded random generator for everything random in the match (spawn points, items, who starts), and every game sees the same world.
  • A private room's host can also start from your own lobby with await online.start() once enough players have joined.

6. Choose how to keep players in sync

The portal relays messages; your game decides what they mean. Two models cover most games.

A. Everyone plays the same world (shared seed)

Each player plays their own copy of the same world and the games exchange only what others need to see, for example the score. Simple and robust; ideal for races and score battles. Star Catcher's Versus mode works like this: everyone gets the same falling stars for 60 seconds and the best score wins.

setInterval(() => online.send({ t: 'score', score }), 250);       // 4 times a second

online.on('message', ({ from, data }) => {
  if (data.t === 'score') scores[from] = data.score;              // show the others' scores
});

B. The host runs the match (host-authoritative)

The host's game simulates the whole match; the other games send their inputs to the host and draw the state the host sends back. Use it when players interact directly (tag, sports, combat). The online starter works like this:

// Every game: send your input to the host when it changes (at most 15 times a second).
online.send({ t: 'input', dx, dy }, { to: match.host });

// The host: apply everyone's inputs, simulate at a fixed rate, and send the state.
online.on('message', ({ from, data }) => {
  if (data.t === 'input' && isHost) inputs[from] = data;
});
setInterval(() => { if (isHost) online.send({ t: 'state', players: snapshot() }); }, 1000 / 15);

// The other games: draw the latest state, moving smoothly between snapshots.
online.on('message', ({ from, data }) => {
  if (data.t === 'state' && from === host) applySnapshot(data);
});

Keep messages small: short keys, whole numbers, only what changed. Send at a steady rate rather than every frame.

7. When players leave

online.on('left', ({ slot }) => removePlayer(slot));
online.on('room', (room) => {
  if (room.host !== host) {        // the host left: the next player became host
    host = room.host;
    if (host === room.me) continueAsHost(lastState);
  }
});
online.on('end', ({ reason }) => backToMenu());   // the room closed

When the host leaves, the player with the lowest slot becomes host, and every game receives a new room event. With model B, the new host continues the simulation from the last state it received.

Call online.leave() when the player leaves a match from your own menu.

8. Finish the match

The host reports the final order, best first:

if (isHost) await online.result([2, 0, 1]);    // player slots, winner first

Online results are not ranked during the beta; players earn a little experience for finished matches. After the result, a private room returns to its lobby, so the host can start another match; quick-match players can search again with find().

9. Test online play

  • In the studio: upload your build and open the preview in two browser tabs. In preview, online play uses private rooms only, and the same account may join from both tabs: create a room in one tab and join it from the other with the code. The bridge inspector below the game shows every mp. message.
  • Outside the portal: keep a practice mode (bots, or two players on one keyboard), so your game is playable anywhere.
  • After publishing, try a quick match with a friend's account.

10. Before you submit

  • Online play works with the minimum and the maximum number of players.
  • A player leaving, or the host leaving, does not break the match.
  • Messages stay within your declared size and rate.
  • There is no free-text chat, and no way to send names, links or contact details.
  • The game still works offline (practice), and solo modes still report their results.

Then publish as described in the single-player tutorial. The full API is in the reference (section 9A).