Playing inside the Hub#
The Hub shell#
The Hub page stays loaded while the player plays: it runs the game in a
full-screen frame from /<path>/game.html, and shows /<path>/ in the address bar.
Going back to the Hub, or to another game, never reloads the page, so the room
and its chat (Buds rooms) stay open. Opening /<path>/ directly (a reload, a
shared link) starts the Hub straight into that game. Only one tab of the browser runs the
Hub at a time: it holds the Web Lock uniplay:library, and another tab shows "Uniplay is
open in another tab" with Use here, which steals the lock; the tab that loses it closes
its game (running onExit), leaves Buds and waits. Query strings and # fragments on
/<path>/ are passed on to game.html.
The frame is same-origin and allows autoplay, fullscreen, gamepad and
microphone. A game finds the Hub on window.parent.uniplayShell (v: 1); the
SDK's shell module does this, and does nothing when there's no Hub (a dev
server, or game.html opened on its own).
Game side (uniplay.shell) |
Meaning |
|---|---|
exit() |
Back to the Hub. Outside it, game.html goes to /; anywhere else nothing happens. |
canExit |
Whether exit() does anything: show an "Exit" button only then. |
onExit(cb) |
Before the Hub closes the game. Save here; a returned promise is awaited, about a second at most. |
paused, onPause(cb), onResume(cb) |
The Buds overlay is over the game. Ignore input while paused. |
notify(notice) |
A notice in the Hub's alert corner; false outside the Hub. |
The Buds overlay (room, people, chat) opens with the key left of 1 (Backquote), or
on a controller with Home, or Back and Start together. Games must not use these. The
Hub also accepts Back alone while no game is running.
uniplay.shell.notify({ kicker, title, text?, icon? }) shows a notice in the Hub's
alert corner (top right), where Buds messages appear; it returns false outside the
Hub. The SDK's achievement popup goes there when the game runs in the Hub.
if (uniplay.shell.canExit) menu.add('Exit', () => uniplay.shell.exit());
uniplay.shell.onExit(() => uniplay.saves.write('main', state));
uniplay.shell.onPause(() => (inputEnabled = false));
uniplay.shell.onResume(() => (inputEnabled = true));
Buds rooms#
Every player has a room: its code is room in their profile
(uniplay:profile), 6 characters from
ABCDEFGHJKLMNPQRSTUVWXYZ23456789. Signed-in players keep codes out of sight: buds add each
other by nickname or email, and accepted buds get each other's code. The Hub opens the room
when it starts, so friends can join with the code at any time, and moves to a friend's room
when the player joins one. A page is in at most one room.
Rooms are Trystero (WebRTC) rooms with app id uniplay-v1, room id the code in
capitals and password the same code, over Trystero's Nostr strategy: relays only see
encrypted connection offers; everything else goes directly between browsers. Pages
exchange offers on the SDK's relay list (RELAYS in the net module: top.testrelay.top,
relay.sigit.io, social.amanah.eblessing.co, nostr.islandarea.net, nos.lol,
purplerelay.com), and two pages only meet if they share a relay, so a page that
passes its own relayUrls should keep some of these. Inside
the Hub there is one connection for the whole site, owned by the Hub page;
games use it through uniplay.net. A game outside the Hub opens its own when it's
given Trystero's joinRoom.
Every message is JSON on a named channel (a Trystero action, at most 32 bytes):
| Channel | Payload | Meaning |
|---|---|---|
u:sys |
{ "t": "hello", "v": 1, "profile", "name", "avatar", "platinum", "room", "game", "hosting", "voice", "muted", "sdk" } |
Sent to each peer on connecting, and to everyone when the page opens or closes a game (game is a game id or null), starts or stops hosting it, joins, leaves or mutes in the voice call, or the player changes their profile. hosting is optional, true only with a game, and only in the player's own room. avatar is optional: an avatar id (uniplay:profile) or null. platinum is optional (SDK 1.4): true shows the platinum version of that avatar, and only counts with an avatar. voice and muted are optional (SDK 1.3): muted only counts with voice. Nothing else from a peer counts until its hello. A hello carrying the receiver's own profile is ignored (the same player in another tab), and a hello whose profile is already in the room under another connection replaces that connection without a join or leave. |
u:chat |
{ "text" } |
A chat line: at most 200 characters after collapsing whitespace, at most 8 per 10 seconds per peer. Show it as text, never as HTML. |
g:<game id>:<name> |
anything | A game's own channel; name is letters, digits and -. |
The room's owner is the peer whose room is the room's code: in co-op games, the
owner hosts and everyone else joins as a guest. A game announces that it's hosting with
uniplay.net.startHosting(); Buds then shows "Hosting uniplay.net.onInvite(cb); the game joins the way its own Join does.
/<path>/#join=CODE links do the same. Leaving a game isn't leaving the room: a guest
whose host closes the game stays in the room, and only goes back to their own when
the owner has left too. Names and profile ids are whatever the
peer says: use them to show and recognise people, not to trust them.
import { joinRoom } from 'trystero';
// Outside the Hub, the game opens its own room with Trystero; inside, it uses the Hub's.
const uniplay = new Uniplay({ game: 'heated-heist', net: { joinRoom } });
const net = uniplay.net; // null when there's no room
const moves = net.channel<{ x: number; y: number }>('move');
moves.onMessage((move, peer) => moveGuest(peer.id, move));
net.onJoin((peer) => addGuest(peer));
net.onLeave((peer) => removeGuest(peer));
net.onInvite((code) => net.join(code));
hostButton.onclick = () => (net.home(), net.startHosting());
Each room has one voice call, which anyone in it can start or join. A peer in the
call says "voice": true in its hello and sends its microphone as a WebRTC audio
stream (a Trystero stream, not a channel) only to the peers whose hello also says
"voice": true, and stops sending to any that leave the call. Peers outside the call
never receive anyone's audio. Muting keeps the peer in the call with a silent track
and "muted": true. Moving to another room, or closing the page, leaves the call. In
the Hub the call belongs to the Hub page, so it carries on across games;
games don't take part in it.
