Skip docs navigation
Public Examples
Tutorial
A lightweight canvas-first path for simple action, timing, and physics toys.

Kaplay Game Tutorial

Build Kaplay Bounce Rally from an empty Vite project when you want a lightweight canvas-first game loop while TPG owns room lifecycle, player identity, state sync, and publishing.

What You Build

Kaplay Bounce Rally has one animated host display and one accessible controller surface:

  • the host display creates a Kaplay context with kaplay({ global: false, root })
  • TPG shared state owns the current match, target lane, and total bounce count
  • TPG player state owns each controller lane and bounce count
  • transient bounce effects travel through api.broadcast("kaplay:bounce", ...)
  • the Kaplay loop reads the latest TPG snapshot without mutating authoritative state
  • the bundle is built with Vite and registered through the registry API

The tutorial below includes the complete host, controller, manifest, and registration source.

Create the Project

Start with the canonical TP Games project so the game inherits the tested scripts, CI workflow, validation, and bundled AI development skill:

If the author CLI is already installed, tpgames init kaplay-bounce-rally replaces the first scaffold command.

This tutorial intentionally replaces the starter surface layout with the checked-in Kaplay 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.

Use the checked-in example as the reference layout:

Install the public game kit plus Kaplay:

Configure the Author Tools

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

Configure vite.config.ts with the two browser surfaces and workbench:

Create tpgames.json:

Model Shared and Player State

Keep Kaplay objects local to the host display. Synchronized game facts belong in TPG shared state, and controller-owned facts belong in TPG player state.

The example uses shared state for match facts:

Each controller owns its current lane and bounce count:

The host display treats both as render input. Do not put Kaplay entity references, vectors, sprites, or timers into shared state.

Boot the Runtime

Both surfaces use bootIframeGame and defineSimpleGame from @tpgames/game-kit. The game kit owns the iframe postMessage bridge, allowed-origin defaults, runtime settings, and surface context bootstrap. The host initializes shared state when the room is all-ready or started:

Each iframe runs its own definition. Treat surfacesReady as a repeatable readiness notification, keep authority-side initialization idempotent, and update Kaplay from the later shared-state subscription echo.

connectRuntime(api) subscribes to lifecycle, participants, shared state, player state, and runtime messages. Store those snapshots in a focused module-local store, then let the Kaplay loop read that store every frame.

Mount Kaplay

Create the Kaplay context after the host surface DOM exists. Pass a concrete root element and set global: false so helpers do not leak onto window from inside the iframe:

The example also marks the generated canvas as decorative because the controller surface owns interactive input:

Render from TPG Snapshots

Use Kaplay for the high-frequency render loop and TPG for synchronized facts. The host display creates lane panels, a moving target, and per-player avatars once, then updates positions from the latest snapshot:

That boundary keeps the canvas smooth without turning Kaplay into the authority for room state.

Send Controller Actions

Controllers stay DOM-first so labels, focus, keyboard input, and screen-reader status remain accessible. Moving lanes writes durable player state:

Bounce actions update player state, emit a transient host effect, and report a creator analytics milestone:

Use player state for facts that should survive reconnect. Use runtime messages for effects that are safe to miss or replay only while the surface is mounted.

Clean Up the Loop

Kaplay owns an animation loop. Release it when the iframe is hidden or unloaded:

If your game can replace or remount the canvas root without a full page unload, keep the cleanup function near the code that creates the Kaplay context and call it before mounting a replacement.

Test on Multiple Devices Without Uploading

The example's @tpgames/sdk-dev-kit Vite plugin exposes the Kaplay 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 and Register

Render the bundle:

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

Then sign in and register from the Kaplay example repo:

The registration script uploads the archive to /creator/uploads, submits the version, creator-publishes it with the selected credential, and fetches /games/{gameId}/manifest for verification.

Registration delivers a built game version; it does not create a developer account. Its default upload mode stops after upload, --mode submit requests review, and --mode publish --yes deliberately completes creator publication. 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.

Launch Verification

After publishing, open the web shell, select Kaplay Bounce Rally, join from at least two controllers, and start the game. Verify:

  • the host iframe shows the Kaplay lane canvas and bounce feed
  • controller buttons and keyboard arrows move lanes
  • Bounce updates the controller status and host feed
  • Settings and Lobby controls still route through the shell
  • refreshing a controller preserves the player seat and can continue play
  • leaving the room stops the surface without leaving a runaway Kaplay loop

Current Limitations

  • The example uses procedural shapes instead of a sprite or audio asset pipeline.
  • Shared state does not advance rounds automatically yet; the tutorial keeps round progression deliberately limited.
  • Spectator surfaces are not included.
  • Registry review and publish are API-driven for automation, so product creator/reviewer flows should still be tested separately for production submissions.