Skip to the content
The room

Documentation · Game SDK · Travel

Travel and avatars

Every player on Player Two has a critter, the same one everywhere. p2.avatar() gives it to your game, as pixels and as a 3D model, so your character can be theirs. p2.travel puts doors in your game: through one, the player (and their party) goes on to another game on Player Two, or back to the Plaza. Both are opt-in: a game that uses neither is as it was. Back to the game SDK.

  1. Your game opens a doorp2.travel.to("hoop-it"): its last saves go up, as they do when the player closes it.
  2. Player Two swaps the gameIn the same player, with no new page: the next game opens, given where the player came from.
  3. The party followsMembers playing with the leader are asked to follow, and the party is called to the game.
On this page

The avatar

p2.avatar() says who is playing, to dress your character with: their name and level, their critter, and what they wear. It never says their email or wallets, as nothing in the SDK does.

const me = await p2.avatar();
// {
//   id: "Xq3...",  handle: "amber", name: "Amber", level: 7, picture: null,
//   critter: { face: "amber", size: 18, tile: "#1c1a26",
//              pixels: [[6, 3, "#f2c14e"], [7, 3, "hsl(40, 85%, 62%)"], ...],
//              model: "https://playertwo.lol/api/v1/critters/amber.glb" },
//   worn: [],
// }
  • The critter is drawn from its face (their handle, unless they picked another): the same name always gives the same critter, on every page of Player Two and in every game. Its pixels sit in an 18 by 18 box, from its top left; colours are CSS.
  • worn is what they wear, earned or bought on Player Two: a hat, a trail, a badge, each a model and a picture. It is empty for now.
  • Ask once a session: the critter only changes when the player picks another face.

Drawing the pixels

A 2D game
// the critter on a canvas, crisp at any size: one rectangle a pixel
function drawCritter(ctx, critter, x, y, size) {
  const u = size / critter.size;
  for (const [px, py, colour] of critter.pixels) {
    ctx.fillStyle = colour;
    ctx.fillRect(Math.round(x + px * u), Math.round(y + py * u), Math.ceil(u), Math.ceil(u));
  }
}
drawCritter(ctx, me.critter, 24, 24, 72);

The 3D model

critter.model is the same critter as the Plaza draws it, in 3D: a glTF binary (.glb), each pixel a cube 7 cm a side, standing on the origin and facing +Z, coloured by its vertices, a few thousand triangles. Anyone may fetch it, from anywhere, and it never changes, so cache it as long as you like.

three.js
import { GLTFLoader } from "three/addons/loaders/GLTFLoader.js";

const { scene: critter } = await new GLTFLoader().loadAsync(me.critter.model);
critter.traverse((o) => { if (o.isMesh) o.material.vertexColors = true; });
critter.position.set(0, 0, 0);   // it stands on the origin, facing +Z, about 0.9 m tall
world.add(critter);

Travel

Through a door

p2.travel.to(slug, { params, place }) sends the player to another game by its address on Player Two (playertwo.lol/game/slug). p2.travel.home() sends them back to the Plaza. Each resolves as the player goes: Player Two’s player sends your game’s last saves up, closes it, and opens the next game in its place.

Your game
// a door in your lobby: the player goes on to Hoop It, and their party with them
doorButton.onclick = () => p2.travel.to("hoop-it", { params: { mode: "pickup" }, place: "court" });

// back to the Plaza, where every game on Player Two has its screen
homeButton.onclick = () => p2.travel.home();
  • The game gone to plays as it would from its page: a free game they don’t have yet asks to be added, and a paid one they don’t own offers its demo, or Buy.
  • params are invite params: up to 16 strings, 512 bytes in all, with from and place.
  • Refused: no-game (there is none at that address), not-out, not-playable (it isn’t played in Player Two’s player), same-game, bad-params, and no-travel where there is nowhere to send the player (below).

Arriving

The game reached gets your params as p2.params, with from, your game’s address, and place, if you named one. A game without the SDK finds them in its address’s query, as it finds a join link’s.

The game reached
const p2 = await PlayerTwo.init();
if (p2.params?.from) {
  // came through another game's door: p2.params.from is its address on Player Two
  toast(`Welcome from ${p2.params.from}`);
  if (p2.params.place === "court") goToCourt();
}

p2.travel.links() fills a menu of doors: the Plaza, then up to 12 games, in this order: where the party’s leader is, the games the player’s friends are in now, the ones they played lately, then the busiest. Only games played in Player Two’s player, out and listed, never yours, never one for adults unless the player said they are 18 or older, nor one with a label they hide.

const { home, links } = await p2.travel.links();
// home:  { title: "The Plaza", url }
// links: [{ slug, title, tagline, art, players, friends: [{ handle, name }], why, free, owned }, ...]
menu.add(home.title, () => p2.travel.home());
for (const d of links) menu.add(d.title, () => p2.travel.to(d.slug), { art: d.art, note: d.friends.length ? `${d.friends[0].name} is here` : `${d.players} playing` });

The party

A party moves as one, as it does in the Plaza. When its leader goes through a door, every member playing with them is asked, at once, in Player Two’s own bar over the game: “Amber went into Hoop It · Follow · Not now”. Following, they land where the leader is, with what the leader set friends to join them with there (p2.presence.set(text, { join })), and p2.params.from their own last game. Members elsewhere on Player Two are called to the game, as the leader’s “Join my game” calls them. Nothing in your game is needed for this.

Where it works

  • In Player Two’s player on the web, for a game Player Two hosts or serves, or one on its own site with Connect on.
  • In the Player Two app on a computer, and anywhere else: travel.to and travel.home reject with no-travel. Show your doors only when PlayerTwo.env is "p2", and hide them if one is refused.
  • On localhost, the stand-in says where it would have gone and stays.

Played with recently

A player’s profile lists who they played with in the last 14 days, to find them again. Your game adds to it by being played:

  • Rooms: players in the same room for a minute or more (p2.rooms). The SDK says who, once a minute; only players in the same game now count.
  • Parties: members of the same party in your game at the same time.
  • Your own server’s matches: report them (below).

A player who keeps their activity private is never shown on anyone’s list; blocked players are never shown to each other; guests are never counted.

Match reports

A game with its own servers reports a match when it ends, with its game key (your studio: Game keys). Name each player by their id for your game (the identity’s sub, as you name them for trophies: a player of your game in the last six hours), or by the identity their client sent you (p2.identity(), taken up to six hours past its time), or by their play session’s token. Each one’s list gets the others.

Your server
// your game's server, when a match ends: who was in it, by their ids for your game (the identity's sub)
await fetch("https://playertwo.lol/api/v1/sdk/server/match", {
  method: "POST",
  headers: { authorization: `Bearer ${process.env.P2_GAME_KEY}`, "content-type": "application/json" },
  body: JSON.stringify({
    id: match.id,                                  // counted once
    minutes: Math.ceil(match.ms / 60000),
    players: match.players.map((p) => ({ id: p.p2 })),   // or { token }: an identity a client sent you
  }),
});
// { counted: 6, players: [{ id, counted: true }, { id, counted: false, why: "guest" }, ...] }

Trophies across games

Two of Player Two’s own trophies are earned across games, and count toward the player’s level like every other. They show on a profile once earned, never before.

Player Two's trophies across games
TrophyHow
Through five portalsGo through a door (p2.travel.to), one of the Plaza's screens, or after the party's leader, into five different games.
Three games with one partyPlay three different games with the same party, at the same time as another of its members.

Reference

The calls and their requests
CallRequestAnswer
p2.avatar()GET /api/v1/sdk/avatar{ id, handle, name, level, picture, critter, worn }
p2.travel.to(slug, { params, place })POST /api/v1/sdk/travel { to, params, place }{ to, params, unlocked }
p2.travel.home()POST /api/v1/sdk/travel { home: true }{ to }
p2.travel.links()GET /api/v1/sdk/travel{ home, links }
p2.params.from, .placeIn the player's answer to the game's helloStrings, or none
A critter's modelGET /api/v1/critters/{name}.glbmodel/gltf-binary
Your server: report a matchPOST /api/v1/sdk/server/match { players, minutes, id }{ counted, players }

Every route is in the API spec.