Phaser Game Tutorial
Build Phaser Rocket Tap from an empty Vite project into a playable registered TPG game. This tutorial includes the complete implementation: Phaser renders the host display, a DOM controller sends player input, and TPG owns synchronized room facts.
What You Build
Phaser Rocket Tap has two surfaces:
- a host-display surface in
examples/phaser-game-repo/src/host.ts - a controller surface in
examples/phaser-game-repo/src/controller.ts
The host display runs the Phaser scene and owns shared round state. Controllers publish each player's altitude as player state. The scene treats sprites, stars, camera position, and animation timing as disposable view state; api.setSharedState() and api.setPlayerState() remain the source of truth for synchronized game data.
Create the Project
Start with the canonical TP Games project and add Phaser. This preserves the tested scripts, CI workflow, validation, and bundled AI development skill:
If the author CLI is already installed, tpgames init phaser-rocket-tap
replaces the first scaffold command.
This tutorial intentionally replaces the starter surface layout with the checked-in
Phaser 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 entries separate so the registry manifest can mount each TPG surface directly:
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 should be compact and deterministic. Phaser objects, textures, timers, and input handlers do not belong in TPG state.
Create helpers for round and player updates so the host and controller do not duplicate arithmetic:
Runtime state is untrusted input when it comes back through the bridge. Keep focused type guards next to these helpers and ignore snapshots that do not match the expected shape.
Boot the TPG Runtime
Both surfaces use bootIframeGame() from @tpgames/game-kit. The kit owns the default iframe postMessage bridge setup, origin handling, runtime bootstrap, and public SDK/runtime type re-exports. Phaser remains only the renderer.
Each iframe runs its own game definition. Keep lifecycle and
surfacesReady work idempotent, restrict initialization to the authority
surface, and render confirmed state from subscriptions after the shell echo.
Each surface calls bootIframeGame() with a defineSimpleGame() implementation. The host uses a host-display context; controllers use a controller context.
Write the Host Surface
The host connects once, subscribes to runtime changes, and seeds the first shared round. Keep runtime connection state outside the Phaser scene so scene restarts do not attach duplicate bridge subscriptions.
Seed shared state when the shell reports that players are ready or play has started:
The exact source line api.setSharedState(createRoundState(1)) is the synchronization contract: the host publishes round facts once, and every mounted surface reacts to the resulting shared-state snapshot.
Mount the Phaser Scene
Mount Phaser inside a stable host-display container. The source example uses Phaser.Scale.FIT so the 800 by 450 board keeps its aspect ratio inside the iframe without forcing horizontal scrolling.
In the scene, subscribe to the host store during create() and redraw only the dynamic layer when TPG state changes. Keep animation-only work in update() so transport snapshots do not drive every frame.
When you add assets, preload them in the scene and bundle them through Vite so the registry zip contains hashed files under dist/assets/:
The scene should also handle resize inside Phaser, not by remounting the iframe:
Write the Controller Surface
The controller can stay DOM-based. It reads shared round state, keeps button feedback local, and publishes player-specific altitude through TPG.
Subscribe only to the current controller's player state. This keeps one phone from rendering another player's altitude after reconnects or host transfer.
The source controller also reports a privacy-safe gameplay milestone when a player taps thrust. Use analytics for aggregate creator reporting, not for raw player-identifying data.
Add the Manifest
Create bundle/manifest.template.json with one host-display surface and one controller surface:
Keep manifest entries aligned with files emitted into dist/. The registry serves those entries directly when the shell launches the published game.
Test on Multiple Devices Without Uploading
The example's @tpgames/sdk-dev-kit Vite plugin exposes the Phaser display and
DOM 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
Use 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 Phaser example repo:
The registration script follows the same service path automation should use:
- upload the zip to
/creator/uploads - submit the draft version through
/creator/games/:gameId/versions/:version/submit - publish it through
/creator/games/:gameId/versions/:version/publish - 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, inspect the draft, submit it for review, 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.
Before registering, run:
After registering, launch the published game from the TPG shell. Verify that the host iframe renders the Phaser canvas, at least two controllers can tap thrust, altitude appears on the host display, refresh does not duplicate runtime subscriptions, and Return to lobby cleanly tears down the game surface.