Skip to main content
You host a room, let other players join it by code or invite link, and pass messages between them. The host’s game runs the match, and OpenGame runs the broker that introduces players and a relay for players who cannot connect directly, so you run no server. Rooms are one option. A game may also connect to a multiplayer server of your own over wss:, as long as it still draws its first frame when that server is out of reach. What a running game may reach covers the rules for that.

Before you start

  • Your game loads @opengamehq/sdk and calls og.init(). Connect the SDK covers that.
  • For local tests, one browser with two tabs open on the same page.
  • For network tests, two browsers on different networks and either a published release or the private preview of a ready release, with multiplayer.mode set to p2p.

Host and join a room

1

Declare multiplayer in opengame.json

opengame.json
The broker allows up to 16 players a room. Measure the host’s upload bandwidth and frame time for your game before you choose the limit.
2

Import the multiplayer module

It is a separate import, so a single-player game does not download it. Without a bundler, load opengame-multiplayer.js after opengame.js and use OpenGame.multiplayer. The complete global, opengame-global.js (imported as @opengamehq/sdk/global), already includes it, so leave out opengame-multiplayer.js if you load that one.
main.js
3

Join from an invite, or host

joinInvite() resolves to null when the page was not opened from an invite link, so one line covers both cases. Of the link a player opened, only the invite code reaches the game, so do not put game state in your own ?param.
main.js
A hosted room uses the maxPlayers from the release’s opengame.json. Pass the same number, or leave it out.
4

Join with a typed code

main.js
5

Handle a failed connection

host and join take { signal } so you can cancel them, and reject with a RoomError. Read code for the failure and retryable before you try again.
main.js
Fix a problem lists the common codes.

Test with two players

Test game logic between tabs first, then the network between browsers.
  1. Open your game locally in two tabs of one browser. Host in one, then join in the other with the code or by opening room.inviteUrl. This tests game logic only: rooms run over BroadcastChannel, no broker is involved, and each tab gets a random guest ID in place of og.player.id.
  2. Open a published release, or the private preview of a ready release with multiplayer.mode set to p2p, in two browsers on different networks. For a private preview, sign both browser profiles in to the creator account.
  3. Host in one browser and join from the other.
  4. Send reliable messages and unreliable snapshots both ways.
  5. Close one peer and confirm the game recovers.
Preview rooms have not been tested on the live broker yet. The private preview of a ready release gets anonymous preview rooms when opengame.json sets multiplayer.mode to p2p, with maxPlayers of 2 or more, and the page then adds the multiplayer capability. OpenGame runs the broker, so if rooms still fail to connect, send the error code to the OpenGame team. The preview invitation includes the release and room code and gives no other account access. Preview rooms are separate from published rooms and from other releases.

Send messages

main.js
  • A message is finite JSON, at most 24 levels deep and at most 16 KiB once wrapped.
  • send returns { sent, dropped, closed }. It throws only for a payload that can never be sent.
  • When a peer falls behind, unreliable sends to it stop once 64 KiB is queued, and the peer is disconnected at 1 MiB.
  • peer.player comes from the broker’s signed ticket, not from the peer, so a guest cannot claim someone else’s name or slot. peer.slot stays with a player, so one who reconnects gets theirs back. peer.seat counts from zero and is reused after someone leaves, so use it for colours and spawn points.
  • Without a migration adapter, losing the host ends the room. Handle close. host-left fires first when the host leaves on purpose.

Room events

Use the OpenGame lobby

The OpenGame lobby, drawn by the page around your game, gives players a shared player list, invitations, readiness and the host’s settings. It resolves with the room when the host starts.
main.js
Your game keeps its own transport and simulation. The OpenGame page shows metadata and settings from the release, with room updates from the SDK. To draw your own lobby, multiplayer.createLobby(options) returns the state without rendering anything. mountLobby(lobby, { container }) renders the SDK’s view, for a parent with no lobby controls. Players can set a room nickname of up to 32 characters. A nickname does not change an account handle or prove who someone is. In a connected room, call await room.setDisplayName(name) and read the updated roster from room.lobbyMembers or the members event. Names in local rooms are development data.

Offer room settings

Declare the choices in the release’s opengame.json. OpenGame shows their labels in the lobby, the host picks choice IDs, and the broker checks those IDs against this release’s list.
opengame.json
Read the selection from the room. Your game’s rules give each ID its meaning: short is not a duration the platform defines.
main.js
To change the choice list, deploy and submit a new release. Players get the new choices once a reviewer approves and publishes it. A changed selection clears readiness, and players confirm the current settings before the match starts. These APIs run on OpenGame’s broker, so local rooms between tabs do not exercise them.

Start a round together

room.phase() schedules a moment that starts at the same time for every peer, on a clock synchronised to the host.
main.js
Each player calls room.ready() when they are ready. room.readyState and the readiness event report how many are. Each new phase raises the epoch, so a countdown cancelled with room.phase(null) cannot come back.

Restore the match after losing the host

Host migration is opt-in, and it works only in hosted rooms, not local ones between tabs. Each peer supplies a matching schema and a restore function. The host captures JSON state and sends it to a successor, which acknowledges it. While connected, the host also renews a lease with the broker. The broker hands authority to the successor only after that acknowledgement and once the lease has run out.
main.js
Treat a failed restore or a missing checkpoint as the end of the match, and keep handling close. Local BroadcastChannel rooms do not exercise hosted succession. OpenGame has not yet tested host migration between two players on the live broker.

Check connection quality

main.js
relay is true when the connection goes through our relay, which adds latency. When there is no relay, relayReason says why: A direct connection lets the other players see a player’s network address. Pass relayOnly: true to host, join or joinInvite to send that player’s traffic through the relay instead. It hides their address from peers, uses more bandwidth and can add latency.

Keep the host in charge

We learned these from bugs in our own games. They belong to your simulation, so the SDK cannot enforce them.
  • Guests send inputs, not outcomes. Send “fired at this angle”, not damage numbers or “I hit them”. The host works out the result.
  • Validate every message against one table that sender and receiver both import, and drop anything with an extra key. defineMessages in @opengamehq/sdk/netcode does this.
  • Send the last few inputs with every packet, and apply no more of a guest’s inputs than host ticks have passed. InputHistory resends them and InputQueue drops repeats, but drain returns everything pending. Applying all of it lets a guest move several times per tick by sending more commands, which is a speed hack the host runs for them. Give each guest one move per tick, banked for a few ticks so inputs delayed by a lost packet still count, as guest-inputs.js in the repository’s templates/multiplayer starter does. The starter is not in the CLI or SDK package; ask the team for it if you have no repository.
  • Quantise on a grid with an exact zero. Without one, an idle guest drifts. Use symmetricQuantizer.
  • Calculate the lag-compensation window from round trip, snapshot age, interpolation delay and jitter. Use lagCompensationMs.
  • Correct the simulation at once and smooth only the camera. Use smoothCamera, cyclicLerp for angles, and extrapolateScalar with a cap.
The SDK reference lists every helper.

Fix a problem

host() and join() reject with a RoomError instead of returning a result. error.code names the failure for you, not for the player. Tell them in plain words what happened and what to do, such as “Couldn’t reach the room. Try again?”, and offer a retry only when error.retryable is true.

Fix by error code

These are the codes you are most likely to see. errorCatalogue, exported from @opengamehq/sdk/multiplayer, lists all of them, each with a hint.

Fix by symptom

host and join reject with unavailable_in_preview when the preview lacks the multiplayer capability. The page adds that capability only for a ready release whose opengame.json sets multiplayer.mode to p2p, so set it, deploy, and open the new release’s preview. An invite does not grant another account access, so sign both browser profiles in to the creator account.
Local rooms only connect tabs of one browser on the same page and origin. A tab on another port or another browser is a different channel. A page with no BroadcastChannel gets local_unavailable.
Unreliable sends to a peer stop once 64 KiB is queued for it, and the peer is disconnected at 1 MiB. Send snapshots less often, or make them smaller.
That is the default. Add a migration adapter on every peer, in a hosted room. Passing one to a local room throws migration_unavailable.
Read room.stats(peer.id) for that player’s rtt, jitter, loss and relay. When relay is true, their traffic goes through our relay, which adds latency.

Where next

Read the SDK reference

Look up every room, lobby and netcode export and its signature.

Read the limits

Look up the room size, packet size and backlog limits.

Connect the SDK

Identify players and follow pause, mute and sign-in.

Enter a jam

Submit your multiplayer release for review.