Your First Game with Vite
This is the complete happy path, from an empty folder to a submittable game zip. Copy every command exactly. After each step there is a What you should see so you always know whether it worked before moving on.
You do not need to know git, servers, or how bundling works. You need Node 20 or newer and a terminal you can paste into. Check your Node:
node --version
What you should see: a version line like v20.x.x or higher. If it says
v18 or "command not found", install Node 20+ from nodejs.org first, then come
back.
The 30-second mental model. A BodyLink game is a tiny web page that runs inside a sandboxed iframe on the BodyLink host. The host owns the camera and streams you body-pose "frames"; your page draws the game and sends back events. You build it with Vite, you talk to the host through the
game-kitSDK, and you ship a built zip — never raw source. That's the whole game.
Step 1 — Scaffold the project
npx downloads and runs the BodyLink scaffolder in one shot. Replace myslug
with a short, all-lowercase name (letters, numbers, dashes) — this becomes your
game's folder and its permanent id.
npx create-bodylink-game myslug
What you should see: a new folder called myslug/ with files already in it —
bodylink.game.json, index.html, game-app.js, vite.config.js, a
package.json, and a test/ folder. The scaffolder pins the Stable SDK
channel by default (see SDK Channels).
Want a nicer display name?
npx create-bodylink-game myslug --name "My Slug". The slug stays kebab-case; the name is what players read.
Step 2 — Install dependencies
Move into the new folder and install. This pulls the BodyLink SDK and Vite.
cd myslug
npm install
What you should see: npm prints an "added N packages" line and creates a
node_modules/ folder and a package-lock.json. No red ERR! lines. (If you
see EINTEGRITY, your SDK pin and lockfile disagree — see
Troubleshooting.)
Step 3 — Run the dev server
npm run dev
What you should see: Vite prints a local URL — for a fresh scaffold it is
➜ Local: http://127.0.0.1:7500/
Open http://127.0.0.1:7500/ in Chrome. You should see the scaffolded game's
home screen and a developer tour page (start-here.html) you can open from
there. npm run dev is standalone mode: no host, no camera required — the
game runs directly so you can iterate on gameplay. You can play it right now with
the mouse, touch, or arrow keys + Enter.
Leave this terminal running. It watches your files and reloads the browser on every save. Open a second terminal for the later build commands.
Step 4 — Edit one line and watch hot-reload
Open index.html in your editor. Find the tagline line:
<p class="tagline">Touch the rings with one hand to score.</p>
Change the text to anything, e.g.:
<p class="tagline">My very first BodyLink game.</p>
Save the file.
What you should see: the browser tab updates by itself within a second —
no manual refresh. That's Vite's hot-reload. This is your inner loop: edit,
save, look. (If nothing changed, confirm the npm run dev terminal is still
running and the URL is 127.0.0.1:7500.)
Step 5 — Open the Dev HUD and drive it from the keyboard
You do not need to edit URLs to turn on debug tools. With the game focused, press the backtick key — ` / ~, just under Escape.
What you should see: the Dev HUD slides in — a small on-screen panel of clickable toggles. Find the input source control and switch it to keyboard (keyboard-sim). Now the arrow keys move your on-screen pointer and Enter/Space acts as a "touch", so you can test the whole game with no camera.
The Dev HUD is the front door to every debug mode (perf overlay, skeleton view, input simulation, FTUE skip). The full toggle list — and the URL flags that do the same thing if you ever need them — is in Dev HUD & Debug Modes.
Step 6 — Build, validate, and export the zip
When the game plays the way you want, produce the shippable bundle. Run these in your second terminal, in the project folder, in this order:
npm run build
npm run validate
npm run export:zip
npm run buildcompiles your game into a self-containeddist/folder. (It callsnode tools/build.mjs, not a barevite build— that detail matters; see Vite Config & Build.)npm run validateruns the exact fail-closed gate the developer portal runs, againstdist/. It printsPASSor a named-check report.npm run export:zipre-builds and writes the distributable archive.
What you should see: after export:zip, a new file at
bodylink-bundles/myslug@1.0.0.zip
(myslug is your slug, 1.0.0 is the version from bodylink.game.json.) A
PASS from validate and a zip on disk means you have a submittable bundle.
Before you actually submit (and before every release after this one), run the scaffold's preflight from your game root:
npm run release:preflight
It dry-runs the submission requirements — version sync, the live-SDK SRI pin,
the upload size cap, released-version hygiene — and prints a ✓/✗ report
with an exact fix: line under every failure, ending in PREFLIGHT: PASS or
PREFLIGHT: FAIL. A ✗ means stop and apply the fix; only submit on PASS.
(- lines are skips — a check that doesn't apply to your setup yet — not
failures.) That zip is what you upload — the check-by-check breakdown and the
upload walkthrough are in the
Developer Portal Guide.
Golden rule: you submit the built zip, never the raw source folder. Opaque-origin iframes can't fetch modules at runtime, so unbuilt source is always rejected.
export:zipis the only correct way to produce a bundle.
The two files you will actually edit
Everything else the scaffold generates you can leave alone at first. These two are the ones you touch.
bodylink.game.json — the manifest (who your game is)
A trimmed, annotated version of what the scaffold writes. Every field here is explained in the Manifest Reference.
{
"id": "tv.bodylink.games.myslug", // permanent id — derived from your slug
"name": "My Slug", // display name players see
"version": "1.0.0", // bump this for every new release
"runtimeRange": "^1.0.0", // which Runtime v1 hosts can run this
"runtime": "bodylink-runtime-v1",
"entry": {
"kind": "iframe", // Runtime v1 bundles are iframe entries
"html": "index.html" // bundle-relative path to your entry page
},
"iframeSession": {
"channel": "postmessage",
"allowedParentOrigins": [ // hosts allowed to embed you (no "*")
"http://127.0.0.1:7500", // your local dev server
"https://bodylink.tv" // production host
],
"sandbox": ["allow-scripts"] // never add allow-same-origin
},
"capabilities": [
"iframe.bundle.v1", // required for an iframe entry
"sdk.proxy.v1", // required for an iframe entry
"tracking.frame", // you want pose frames
"input.remote", // you want remote/controller input
"diagnostics.runtime"
],
"launcher": { // how you appear in the catalog
"name": "My Slug",
"description": "Touch the rings with one hand to score.",
"color": "#38BDF8",
"gridSize": "2x2",
"tags": ["arcade", "hand-tracking"],
"howToPlay": ["Reach one hand to touch the glowing rings."],
"trackingFocus": "hand",
"trackingTier": "standard" // 640 + lite pose model — leave as-is
}
}
game-app.js — the entry file (what your game does)
The scaffold ships a full working game. Here is the minimal shape so you can see the moving parts — create the SDK, listen for frames, draw. The concrete API is in the game-kit API Reference.
// game-kit is the game-facing SDK entry point — the `game-kit` subpath of the
// installed bodylink-platform package. The scaffold already writes this import
// at the top of your generated game-app.js — you don't add it.
import { createBodyLinkGame } from 'bodylink-platform/game-kit';
const canvas = document.getElementById('gameCanvas');
const ctx = canvas.getContext('2d');
// Create the game. gameId must match your manifest slug. In a hosted session
// this performs the SDK handshake for you; standalone it just runs.
const game = createBodyLinkGame({ gameId: 'myslug' });
// Pose frames, already normalized to screen space [0,1]. `pointer` is a
// ready-made cursor (right wrist -> left wrist -> nose fallback chain).
game.onNormalizedTrackingFrame((event) => {
const { pointer } = event;
if (!pointer) return;
ctx.clearRect(0, 0, canvas.width, canvas.height);
ctx.beginPath();
ctx.arc(pointer.x * canvas.width, pointer.y * canvas.height, 18, 0, Math.PI * 2);
ctx.fillStyle = '#fff7a6';
ctx.fill();
}, { replaceFrame: true });
// Remote / controller actions (select, back, menu…), already normalized.
game.onRemoteInput((event) => {
if (event.action === 'back') { /* return to your menu */ }
});
Where to go next
- game-kit API Reference — the exact API you just used.
- Manifest Reference — every
bodylink.game.jsonfield. - Dev HUD & Debug Modes — every debug toggle.
- Vite Config & Build — why the build is special, and the one trap to avoid.
- Troubleshooting (Vite) — when a step above didn't do what it should.
- Developer Portal Guide — upload the zip and go live.