logo

Babylon.js Market

P2PNetwork

networking · webrtc-mesh

P2PNetwork

1
Unlock

Install with the CLI:

bjs download P2PNetwork

P2PNetwork

P2PNetwork is a peer-to-peer network for the browser, with the game taken out of it. It opens a WebRTC network where every peer is equal — no host, no authoritative server — and hands your game a single, boring contract on the EventBus: messages arrive as net.message, you send with net.broadcast, peers come and go as net.peerJoined / net.peerLeft. What those messages mean is entirely your game's business. Poker rides on it; a co-op shooter or a quiz buzzer could ride on the same component unchanged.

What it does

Two browsers behind two routers can't just find each other. The whole job splits in two: a brief signaling handshake to introduce the peers, and then a direct DataChannel between them that the server never touches again. P2PNetwork owns both halves. On join it offers a channel to every peer already in the room, answers incoming offers, and exchanges the offer/answer SDP by polling a room endpoint during setup — then every message after that flows peer-to-peer. It survives the messy parts a real peer-to-peer network hits: a peer that drops and rejoins re-handshakes cleanly, a mid-handshake "glare" collision is skipped rather than wedging the connection, and a stale peer connection is recycled instead of reused.

It speaks only opaque NetMessages — { type: string, …anything }. It never inspects them, so it never needs to know your game. The one thing it does understand is seats: each peer has an integer seat, and the component tracks the live occupiedSeats set as channels open and close, publishing it as net.seatsChanged.

A pure, renderer-free, browser-free core (P2PNetwork.core) carries the determinism primitive every peer-to-peer game eventually wants: a seeded Mulberry32 PRNG (createRng / hashSeed / createGameRng). Seed it from a shared secret plus a round number and every browser computes the identical sequence — a shuffle, a spawn wave, a loot roll — with nothing on the wire. None of it imports a renderer, a network, or the DOM.

The whole transport sits behind a typeof window / RTCPeerConnection / fetch guard, so under Node, SSR, or a test the System constructs and ticks cleanly with no network at all — and the connection factory is injectable, so a fake transport can drive every branch headlessly.

Use it in a scene

Drop P2PNetwork on an entity and have your game's own System translate its net.* traffic. The connection inputs are resolved at boot (from a room/join step) and injected before the scene loads, so the network opens with a real seat:

A scene is just data — a list of entities, each with its components. At startup the SceneLoader turns this JSON into a live world; edit the file and reload to rebuild it.

Build it from scratch with the bjs CLI:

  1. 1Scaffold a project
    npm create @babylonjsmarket/arcade@latest my-game

    World, renderer, and dev server — ready to run.

  2. 2Install dependencies
    cd my-game && npm install
  3. 3Add P2PNetwork
    bjs download P2PNetwork

    Copies its source into src/ so the scene resolves.

    First time? Run bjs login once.

  4. 4Paste the scene into src/scenes/arcade-room.ts and run
    npm run dev

    SceneLoader builds the world from the JSON; reload to rebuild.

JSON
{
  "Net": {
    "components": {
      "P2PNetwork": {
        "roomCode": "table-7",
        "mySeat": 0,
        "myPlayerId": "seat-0",
        "occupiedSeats": [0, 2],
        "autoConnect": true,
        "signalBasePath": "/api/poker/room"
      }
    }
  }
}

Your game System listens for net.message and turns each one into a domain event; when it wants to send, it emits net.broadcast. See PokerMultiplayer for a worked example — it's a thin translator that maps net.message {type:'action'} to a poker turn and relays the local player's moves back out, all without touching an RTCPeerConnection.

Props

PropTypeDefaultNotes
roomCodestring''Public room id — the path segment peers signal through.
mySeatnumber0This client's absolute seat (0..n).
myPlayerIdstring''This client's stable id, used to address signaling messages.
occupiedSeatsnumber[][0]Seats live at connect time; grows/shrinks as peers join and leave.
existingPlayers{ id, seat, name }[][]Peers known at connect time, for the initial WebRTC offers.
autoConnectbooleantrueOpen the network on attach (in a browser). Leave false to connect manually or load headless.
signalBasePathstring'/api/poker/room'Signaling endpoint base — requests go to ${signalBasePath}/${roomCode}/…. Point it at your own room server.

Events

Emits:

NameWhenPayload
net.connectedThe network finished connecting (or failed){ connected, seats }
net.peerJoinedA peer's data channel opened{ seat }
net.peerLeftA peer's data channel closed{ seat }
net.seatsChangedThe live occupied-seat set changed{ occupiedSeats }
net.messageA peer message arrived off the wire{ fromSeat, msg }

Listens:

NameEffectPayload
net.broadcastSend a message to every open peer channel{ msg }

That's the entire contract. A game layer never sees a DataChannel or an SDP — only these six events.

Dependencies

None. P2PNetwork is a leaf: pure network glue over the EventBus, with no other component required to run it. (It does need a room/signaling server to reach — see Notes.)

Notes

  • It requires a room server. Signaling — the offer/answer exchange that bootstraps each peer connection — polls ${signalBasePath}/${roomCode}/signals and reads the roster from ${signalBasePath}/${roomCode}. Once the channels are open the component plays no part (game data is pure P2P), but it can't form the network without that endpoint to relay the initial handshake. The bundled poker room server (/api/poker/room) is the default; set signalBasePath to use your own.
  • Determinism without traffic. createGameRng(seed, round) lets every peer compute the same sequence from a shared secret, so games can keep secret state (a deck, a spawn table) off the wire entirely and send only intent.
  • The transport is injectable. P2PNetworkSystem defaults to the real WebRTC createMeshConnection but accepts a connectionFactory option — the seam the tests use to drive every message and membership branch with a fake transport, and a clean place to swap in a different signaler.
  • The browser seam is guarded, not assumed. All of the WebRTC and fetch code is gated behind browserHasWebRTC(), so the component is safe to instantiate in any environment; it simply doesn't open a network where one can't exist.

More like this

PokerMultiplayer

Was this page helpful?

We read every note — tell us what's working and what isn't.

↑↓ NavigateEnter SelectEsc CloseCtrl+K Open Search