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/sdkand callsog.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.modeset top2p.
Host and join a room
1
Declare multiplayer in opengame.json
opengame.json
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
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
Test with two players
Test game logic between tabs first, then the network between browsers.- 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 overBroadcastChannel, no broker is involved, and each tab gets a random guest ID in place ofog.player.id. - Open a published release, or the private preview of a ready release with
multiplayer.modeset top2p, in two browsers on different networks. For a private preview, sign both browser profiles in to the creator account. - Host in one browser and join from the other.
- Send reliable messages and unreliable snapshots both ways.
- 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.
sendreturns{ 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.playercomes from the broker’s signed ticket, not from the peer, so a guest cannot claim someone else’s name or slot.peer.slotstays with a player, so one who reconnects gets theirs back.peer.seatcounts 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-leftfires 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
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’sopengame.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
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
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
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.
defineMessagesin@opengamehq/sdk/netcodedoes this. - Send the last few inputs with every packet, and apply no more of a guest’s inputs than host ticks have passed.
InputHistoryresends them andInputQueuedrops repeats, butdrainreturns 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, asguest-inputs.jsin the repository’stemplates/multiplayerstarter 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,cyclicLerpfor angles, andextrapolateScalarwith a cap.
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
Two tabs never find each other
Two tabs never find each other
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.A guest falls behind, then drops out
A guest falls behind, then drops out
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.
The match ends whenever the host leaves
The match ends whenever the host leaves
That is the default. Add a migration adapter on every peer, in a hosted room. Passing one to a local room throws
migration_unavailable.Latency is high for one player
Latency is high for one player
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.