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.