Skip docs navigation
Public Examples
Tutorial
3D scenes, cameras, lighting, and spatial interactions hosted inside TPG surfaces.

Three.js Game Tutorial

Build Three Signal Orbit from an empty Vite project into a playable registered TPG game. This tutorial includes the complete implementation using the renderer-neutral @tpgames/game-kit package and Three.js.

What You Build

Three Signal Orbit has two browser surfaces:

  • a host-display surface in examples/three-game-repo/src/host.ts
  • a controller surface in examples/three-game-repo/src/controller.ts

The host-display owns the Three.js scene, camera, renderer, animation frame, and shared orbit state. Controllers send pulse direction and power as player state. Keep Three.js scene objects local to the host-display surface, and synchronize only serializable facts through api.setSharedState() and api.setPlayerState().

Create the Project

Start with the canonical TP Games project and add Three.js. This preserves the tested scripts, CI workflow, validation, and bundled AI development skill:

If the author CLI is already installed, tpgames init three-signal-orbit replaces the first scaffold command.

This tutorial intentionally replaces the starter surface layout with the checked-in Three.js example. Copy the example's root host.html and controller.html in place of host.html and controller.html, then remove vite.config.mjs before creating the vite.config.ts shown below. Do not keep both Vite config files.

Keep host and controller as separate entries so the registry manifest can launch each TPG surface independently:

Configure the Author Tools

Add the device, bundle, and registry commands to package.json:

Configure Vite to expose both surfaces and the TPG workbench:

Create tpgames.json:

Model Shared and Player State

Put serializable game state in src/game.ts. Shared state belongs to the room. Player state belongs to one controller.

Do not store meshes, materials, vectors, cameras, or renderer handles in TPG state. They are local runtime objects and cannot be replayed safely across host, controller, spectator, or future dedicated logic roles.

Boot the Runtime Bridge

Both surfaces use bootIframeGame() from @tpgames/game-kit. The kit creates the default postMessage bridge internally, infers registry iframe context from the URL hash when the shell launches the game, and still lets local examples provide fallback context for standalone development.

Each iframe runs its own game definition. The host treats surfacesReady as a repeatable readiness notification, initializes shared state idempotently, and lets subscription echoes drive the Three.js scene.

The host uses a host-display context. Controllers use a controller context with a participantId fallback for local previews.

Write the Host Runtime

The host connects once, subscribes to lifecycle, participants, shared state, and player state, then seeds the first orbit when all required surfaces are ready or when play starts.

The exact source line api.setSharedState(createOrbitState(1)) is the synchronization point. It publishes the first play step once, then every mounted surface receives the shared-state snapshot through the runtime bridge.

Create the Three.js Scene

Create the renderer and scene after the DOM root exists:

Use local Three.js objects for presentation. In the example, the beacon mesh, orbit ring, marker, hemisphere light, and directional light are all host-display resources. The runtime state tells the host which play step and target angle to render; the scene graph decides how that looks.

Resize and Render

Bind the canvas dimensions to the surface container, not the full browser window. Iframes, shell chrome, and future host-display layouts can change independently.

Keep the animation loop local and derive visual motion from current runtime snapshots:

This keeps frame-by-frame animation out of shared state. Shared state should change when room facts change, not on every rendered frame.

Clean Up WebGL Resources

Three.js examples should show cleanup explicitly because leaked animation frames, resize observers, geometries, and materials are hard to diagnose inside iframe surfaces.

Use this same pattern when returning to the lobby, hot reloading during development, or navigating away from a registry-hosted game surface.

Write the Controller Surface

Controllers publish one player's pulse state. The slider value remains local UI state until the player presses a pulse button.

The host subscribes to player-state updates and uses the latest pulse facts to influence the render loop. The controller can also call runtimeApi.openSettings() and runtimeApi.returnToLobby() so shell-owned lifecycle controls remain available while the game surface is active.

Add the Manifest

Create bundle/manifest.template.json with one required host-display surface and one required controller surface:

Set display to required because Three Signal Orbit needs the host-display WebGL scene. The controller cannot render the shared 3D view on its own.

Test on Multiple Devices Without Uploading

The example's @tpgames/sdk-dev-kit Vite plugin exposes the Three.js display and controller to the normal TPG room flow. Install cloudflared, then run:

The command starts Vite and a temporary Cloudflare Quick Tunnel, then opens the host room on https://play.tp.games. Scan the room QR with two or more phones to test real controller identity, transport, and input against your current source. No registry upload, API key, or bundle build is required.

To use a tunnel you already manage, keep its origin port aligned with the CLI:

A Quick Tunnel exposes the local Vite server and source modules publicly. Treat the generated host link as private because it contains the temporary development-manifest descriptor; share only the room QR or controller invite. Keep .env files, credentials, and private assets outside Vite's served root, and stop the command and tunnel when finished. Use --no-open when you only want the private host link printed.

Bundle the Game

The example uses the public tpgames bundle command to render the manifest and zip the Vite build from tpgames.json.

The output should include:

Register and Publish

For local interactive testing, start the registry and browser approval shell from a TPG repository checkout in two terminals:

Then register from the Three.js example repo:

The registration script follows the same service path external automation should use:

  1. upload the zip to /creator/uploads
  2. submit the draft version
  3. publish it through /creator/games/:gameId/versions/:version/publish
  4. fetch the published manifest to verify launch metadata

Use the default upload mode to stop after upload, --mode submit to stop after review submission, or --mode publish --yes for the full publish path.

Registration delivers a built game version; it does not create a developer account. Browser login stores a session only for the exact registry origin. Use TPG_API_KEY only for CI or another unattended environment, keep it in the environment rather than arguments or project configuration, and grant only the scopes required by the selected mode.

You can also upload the zip through the Creator Portal, submit the draft, and publish the version through the portal. Reviewer approval remains available for manual review flows.

Test Locally

Use npm run dev for the multi-surface workbench while iterating, then use npm run dev:devices for a real room without uploading. After registration, open the local shell, start a room, choose the registered Three Signal Orbit game, and verify:

  • the host-display iframe renders a WebGL canvas
  • controller buttons can be reached with the keyboard
  • Pulse left and Pulse right update the controller's live status
  • the host pulse count changes after controller input
  • returning to the lobby cancels the active game surface and allows a clean restart

TPG repository contributors can mirror the source-example checks from the monorepo root: