The two ways
You choose when you submit the game, and can switch in your studio (its card, under Where it plays). Either way players find it on its page here and play it in Player Two’s player.
| On Player Two (recommended) | On your own site | |
|---|---|---|
| How it gets here | Upload a zip, or push the folder with one command. Up to 5 GB. | Paste its address. |
| Its address | Its own, on playertwo.cc | Yours |
| Code changes | None. Player Two puts the SDK in its page. | One script tag, with Connect on. |
| Saves | Work as in any browser, and follow players to any device. | Your site’s, though a frame keeps its own: see Connect. |
| Selling it | Only its owners can play it. Nothing to build. | Your server checks who owns it. |
| Trophies, leaderboards, friends, invites | Built in: a few SDK calls. | With Connect, the same calls. |
| Serving it | Player Two's, from a CDN. | Yours. |
Code changes
Hosted here: none
Your export plays as it is. Its paths work as they do on any server, root-absolute ones too (/assets/app.js), and so do its own storage, workers, WebAssembly and audio. Player Two adds one script first in each of its pages, the game SDK in auto mode, which finds Player Two around the game and keeps its saves going:
<script src="https://playertwo.lol/sdk/v1.js" data-p2-auto data-game="your-game"></script>If your game already loads the SDK and calls PlayerTwo.init(), nothing changes: it gets the same answer. A query on a file’s address (app.js?v=3) makes no difference to which file is sent.
Your own servers
A game hosted here talks to your servers (an API, a multiplayer server) from its own origin, https://your-game.playertwo.cc. Add it wherever your servers say which origins may call them: your CORS, and your WebSocket server’s check of the Origin header.
// your API (Express with cors): the game calls it from its own address on Player Two
app.use(cors({ origin: ["https://yourgame.com", "https://your-game.playertwo.cc"] }));
// your WebSocket server: the same address, in its check of where a connection comes from
const ALLOWED = new Set(["https://yourgame.com", "https://your-game.playertwo.cc"]);
wss.on("connection", (socket, req) => {
if (!ALLOWED.has(req.headers.origin)) socket.close(1008, "Not from the game");
});To know who is playing, your servers check a signed identity from p2.identity(), as a game on its own site does: see the identity.
Trophies and leaderboards
A few calls, once you have made the trophies and leaderboards in your studio. The game plays on without them anywhere else.
PlayerTwo.init().then(async (p2) => {
await p2.trophies.unlock("first-win"); // the player sees the toast
await p2.scores.submit("time-attack", 83400); // a leaderboard you made in your studio
}).catch(() => { /* played outside Player Two: the game carries on without them */ });Saves
A game hosted here keeps its saves where it always does, in the browser’s localStorage and IndexedDB, at its own address. Player Two keeps a copy of both in each player’s cloud saves and puts it back before the game runs, on any device they sign in on. No code needed.
- When it goes up: a moment after the game last wrote, and as the player leaves or closes the game. Another device saving in between is answered by sending again on top of it: the newest write wins. At the start of a page load, the cloud’s copy wins.
- What stays on the device: keys starting
p2_ignoreorp2-ignore, the keys and databases you list in your studio, databases named for a cache (Unity’sUnityCache), and a database holding files (Blobs), which can’t be copied. - How much: up to 2 MB of localStorage and 2 MB of IndexedDB a player. The largest keys and databases are left out until the rest fits, and your studio lists them.
- Your own saves:
p2.saveskeeps named slots in the player’s cloud saves, from any game (the SDK). Turn the copy off in your studio if you would rather keep saves that way only.
Unity, Godot and SharedArrayBuffer
Unity
Export for WebGL and push the folder with index.html at its top. Compressed builds work as they are: a file ending .br or .gz is sent with its encoding, so Unity’s decompression fallback isn’t needed. PlayerPrefs and files under Application.persistentDataPath follow the player; Unity’s download cache stays on the device.
Godot
Export for the Web. Files under user:// follow the player. Godot 4 with threads needs SharedArrayBuffer (below); since Godot 4.3, a build exported without threads needs nothing.
SharedArrayBuffer
A page has SharedArrayBuffer (threads in WebAssembly) only when it is cross-origin isolated. Turn Cross-origin isolation on in your studio: your game’s pages then say so in their headers (COOP and COEP, and Document-Isolation-Policy, which isolates the game’s frame on its own). Files your game loads from other sites then need CORS or a Cross-Origin-Resource-Policy header; choose credentialless to let plain ones load, without cookies.
Chrome and Edge isolate the frame today. In other browsers SharedArrayBuffer isn’t there yet inside a frame, so export without threads where your engine can.
Pushing a build
The tool is one file, p2.mjs, with nothing to install. It sends only the files Player Two doesn’t have yet, eight at a time, picks up where it left off if it is stopped, and makes the build live once every file has arrived. Players get the new build on their next page load; a page already open keeps its own to the end.
# once: the tool (Node 20 or later), and an agent key from your studio
curl -O https://playertwo.lol/cli/p2.mjs
# then, each time: the folder your engine exported
P2_AGENT_KEY=p2_agent_... node p2.mjs push ./build --game your-gameHashing 2,914 files (412.6 MB)...
Build 6f1d0c9a2b7e4f3a8c5d1e0b: sending 38 files (21.4 MB); 2,876 files Player Two has already.
sent 38 of 38 files, 21.4 MB of 21.4 MB
Live: https://your-game.playertwo.cc/--entry: the page the game opens at, if it isn’tindex.html.- Left out:
.git,.DS_Storeand the like, and symbolic links (it says which). - The game must be on Player Two first: submit it with a zip, or with its address on your site, then push. The push makes it hosted here.
The three calls
The tool makes three calls, which your own scripts can make too, with your agent key. First the list of files, each with its size and SHA-256; the answer names the build and the files to send, each with where to send it:
POST https://playertwo.lol/api/v1/agent/games/your-game/web
Authorization: Bearer p2_agent_...
{ "entry": "index.html",
"files": [{ "path": "index.html", "size": 1841, "sha256": "9f2c...", "type": "text/html" }, ...] }
200 { "build": "6f1d0c9a2b7e4f3a8c5d1e0b",
"upload": [{ "sha256": "9f2c...", "method": "PUT", "url": "https://...", "headers": { ... } }, ...] }Then each of those files, as it is, to its url with its method and headers. The addresses last six hours; ask again with the same list for fresh ones (files already sent aren’t asked for again). Last, the build is checked and made live:
POST https://playertwo.lol/api/v1/agent/games/your-game/web/6f1d0c9a2b7e4f3a8c5d1e0b/done
200 { "live": true, "url": "https://your-game.playertwo.cc/" }
409 { "code": "missing", "missing": ["path", ...] } # some never arrived: ask again
422 { "code": "mismatch", "mismatched": ["path", ...] } # not what the list said: push againHow it's served
- One build a page load. The page is never cached; each file it asks for comes from that build’s own address (
/~<build>/...), cached for a year, since a build’s files never change. A new build is a new address, so no page load mixes two. - Played in Player Two’s player only. Only Player Two can frame the game, and a visit to its address goes to its page here, where saves, trophies and friends work. A free game can be played at its own address too: turn that on in your studio.
- Its files are its own. They are sent to the game’s own pages and to tools, never to another site. Ranges are answered, for audio and video.
Limits
| Up to | |
|---|---|
| A pushed folder | 5 GB, 20,000 files |
| One file in it | 512 MB |
| A zip (the submit form, or an agent's upload) | 100 MB; 300 MB and 3,000 files unpacked |
| One .html page | 10 MB |
| A player's copy of localStorage, and of IndexedDB | 2 MB each |
Paths are the folder’s own: none may go outside it. Builds a game no longer plays are removed two days after they are replaced, all but the last two.