game-kit API Reference
game-kit is the game-facing BodyLink SDK: one high-level factory,
createBodyLinkGame(...), plus a set of pose helpers. It handles the SDK
handshake, the lifecycle, tracking-frame delivery, calibration, and the
camera-preview API so you don't hand-wire the bridge.
Where these come from. In your scaffolded game, everything on this page is exported from the SDK's
game-kitentry point — the real import specifier isbodylink-platform/game-kit(thegame-kitsubpath of the installedbodylink-platformpackage):import { createBodyLinkGame } from 'bodylink-platform/game-kit';Your generated
game-app.jsalready writes that import line — the examples below start from the returnedgameobject. A lower-level entry,bodylink-platform/iframe-sdk, is available when you need direct bridge control (see the end of this page). Import only these two subpaths; importing anything else is a boundary violation the submission gate rejects.
createBodyLinkGame(options)
Creates and wires the game. In a hosted session it performs the handshake; in a
standalone session (npm run dev) it just runs.
const game = createBodyLinkGame({
gameId: 'myslug', // REQUIRED: must match your manifest slug
autoHello: true, // send the hello handshake automatically (default true)
});
Options
| Option | Type | Default | Meaning |
|---|---|---|---|
gameId |
string | 'bodylink-game' |
Your game's slug. Must match the manifest. |
autoHello |
boolean | true |
Send the sdk.hello handshake on creation. Set false to send it yourself with game.hello(...) after you've set up listeners. |
hello |
object | {} |
Extra fields merged into the hello payload (e.g. { screen: 'home' }). May carry trackingReadiness. |
tracking |
object | {} |
Tracking defaults, e.g. { calibration: { profile: 'upperBody' } }. |
readyTimeoutMs |
number | 2500 |
How long to wait for sdk.ready before giving up (hosted only). |
windowObject |
Window | window |
Override the window (testing). |
logger |
object | console |
Logger for internal warnings. |
client / clientOptions |
— | — | Provide or configure the underlying iframe client. |
What it returns — the game object
| Member | Kind | What it does |
|---|---|---|
game.hosted |
boolean | true inside a BodyLink host iframe; false standalone. Branch on this. |
game.client |
object | The lower-level iframe client (raw bridge). |
game.session |
object | { sessionId, nonce, parentOrigin } for this session. |
game.on(kind, cb) |
fn | Subscribe to a raw bridge event (sdk.ready, lifecycle.*, tracking.frame, input.remote, input.pointer, sdk.error). |
game.onTrackingFrame(cb, opts) |
fn | Raw tracking frames (see below). |
game.onNormalizedTrackingFrame(cb, opts) |
fn | Tracking frames converted to screen-normalized [0,1]. Most games use this. |
game.onRemoteInput(cb) |
fn | Remote/controller actions, normalized (select, back, menu, …). |
game.onTrackingReady(cb) |
fn | Fires once when tracking is ready to consume. |
game.ensureRuntimeReady(opts) |
async | Run the host get-ready / calibration and resolve with the result. |
game.hello(payload) |
fn | Send (or re-send) the hello handshake. |
game.waitUntilReady() |
async | Resolve when the SDK handshake completes. |
game.cameraPreview |
object | .show(slot) / .update(patch) / .hide() — direct the host camera preview. See Camera Preview & Coordinates. |
game.cameraPhoto |
object | .capture(opts) — single-shot host photo. See Camera Photo. |
game.setCoordinateMode(mode, opts) |
fn | Bind 'full' / 'roi' (and optional framing). Also at game.tracking.setCoordinateMode. |
game.createStandaloneTracking(opts) |
fn | Standalone-only: run your own camera/video pose tracking (no host). |
game.setPhase(name, details) |
fn | Report a game phase (home, playing, …). |
game.markLifecycle(kind, details) |
fn | Report a lifecycle transition. |
game.diagnosticLog(event, ctx) |
fn | Emit a diagnostic to the host log. |
game.postVerification(kind, payload) |
fn | Emit a verification event (used by the runtime-boot probe). |
game.createTeardownSummary(summary) |
fn | Build the end-of-session summary payload. |
game.dispose() |
fn | Tear down listeners and the client. |
The handshake & lifecycle
The host requests every transition; your game acknowledges. With autoHello: true the kit sends sdk.hello for you; you just listen.
const game = createBodyLinkGame({ gameId: 'myslug', autoHello: false });
// 1. The host answers the handshake.
game.on('sdk.ready', ({ payload }) => {
// negotiated wire version + granted capabilities
console.log('ready', payload.capabilities);
});
// 2. The host drives the lifecycle: run <-> suspend/resume, then teardown.
game.on('lifecycle.run', () => { /* start your RAF loop */ });
game.on('lifecycle.suspend', () => { /* pause */ });
game.on('lifecycle.resume', () => { /* resume */ });
game.on('lifecycle.teardown', () => {
game.postVerification('summary', game.createTeardownSummary({ score }));
game.dispose();
});
// 3. Errors (version mismatches during negotiation are normal, ignore them).
game.on('sdk.error', ({ payload }) => console.warn(payload.code));
// 4. Now that listeners are wired, send hello.
game.hello({ gameId: 'myslug', screen: 'home' });
The wire order is: sdk.hello (game→host) → sdk.ready (host→game) → manifest
load → lifecycle.run → tracking.frame streaming → lifecycle.teardown. The
authoritative message-direction table is in the
Developer Guide §2.4.
The tracking-frame shape
Each tracking.frame carries a frame object. The fields you'll use:
| Field | Type | Meaning |
|---|---|---|
frame.landmarks |
array | Pose landmarks, each { x, y, z?, visibility? }. Index with LANDMARKS. |
frame.hands |
object | { left, right }, each with wrist / index / etc. Not converted by normalization — see the note below. |
frame.worldLandmarks |
array | null | Metric world-space landmarks when available. |
frame.coordinateSpace |
string | The stamped space (camera-fov-v1 for full, roi-centered-v2 for roi). |
frame.coordinateTransform |
object | null | { transformId, coordinateSpace } once calibration is installed. |
frame.tracks |
array | Per-player tracks for multi-player sessions. |
frame.timestampMs |
number | Frame timestamp. |
frame.source |
string | Which provider produced the frame. |
Landmark indices — LANDMARKS
LANDMARKS maps names to array indices (MediaPipe Pose ordering). The common
ones:
LANDMARKS.NOSE // 0
LANDMARKS.LEFT_SHOULDER // 11 RIGHT_SHOULDER // 12
LANDMARKS.LEFT_ELBOW // 13 RIGHT_ELBOW // 14
LANDMARKS.LEFT_WRIST // 15 RIGHT_WRIST // 16
LANDMARKS.LEFT_INDEX // 19 RIGHT_INDEX // 20
LANDMARKS.LEFT_HIP // 23 RIGHT_HIP // 24
Consuming frames
game.onNormalizedTrackingFrame(cb, opts) — recommended
Converts landmarks to one unified screen-normalized [0,1] space regardless
of the underlying coordinate mode, so you don't branch on coordinateSpace.
game.onNormalizedTrackingFrame((event) => {
const { frame, pointer } = event;
// With replaceFrame:true, frame.landmarks/body/tracks/pointer are all [0,1].
const rightWrist = frame.landmarks?.[LANDMARKS.RIGHT_WRIST];
if (rightWrist) drawCursor(rightWrist.x * W, rightWrist.y * H);
// `pointer` is a ready-made [0,1] cursor (fallback: right wrist -> left
// wrist -> nose).
if (pointer) aimAt(pointer.x, pointer.y);
}, { replaceFrame: true });
opts field |
Default | Meaning |
|---|---|---|
replaceFrame |
false |
When true, event.frame is the normalized frame. When false, you read normalized data off event.normalized. |
requireReady |
true |
Only deliver frames after tracking is ready (calibration settled). |
Sharp edge:
frame.handsis not normalized. Derive hand positions from the converted landmarks (LEFT_WRIST15,RIGHT_WRIST16,LEFT_INDEX19,RIGHT_INDEX20) or read the converted copy atframe.normalizedHands— never mix rawframe.handswith normalized landmarks. Full detail in Camera Preview & Coordinates §3.
game.onTrackingFrame(cb, opts) — raw
Same subscription, but gives you the raw frame plus a pointer resolved in the
frame's own coordinate space. Use it when you want to handle spaces yourself.
Input events
game.onRemoteInput(cb)
Remote / controller actions, already normalized to a small vocabulary:
game.onRemoteInput((event) => {
switch (event.action) {
case 'select': /* A / OK / Enter */ break;
case 'back': /* B / Esc */ break;
case 'menu': /* menu button */ break;
}
});
Normalized actions include select (aliases ok, enter), back (aliases
escape, esc), and menu. Unknown inputs normalize to 'unknown'.
Raw pointer/remote events are also available via game.on('input.pointer', …)
and game.on('input.remote', …) if you need the unnormalized payload.
Gestures
There is no "gesture event" pushed to you — you compute gestures from frames
using the pose helpers below (Body, HandIsolator, distances, the pointer
fallback). This keeps gesture semantics in your game, where they belong.
Tracking readiness & calibration
Hosted frames are gated: they aren't delivered to your requireReady
listeners until calibration settles. Two ways to work with that:
// Fire once, when tracking is ready.
game.onTrackingReady((snapshot) => {
startPlaying(snapshot); // snapshot.transformId, snapshot.coordinateSpace, …
});
// Explicitly run the host get-ready ceremony (returns calibration).
const result = await game.ensureRuntimeReady({
calibration: 'upperBody', // profile
cameraMode: 'video-background',
timeoutMs: 30000,
});
if (result.cancelled) {
// host calibration isn't coming — fall back gracefully
}
ensureRuntimeReady is cached per option set and de-duplicated — call it at
game entry, not at hello. game.trackingReady.getState() returns the live
readiness snapshot; game.trackingReady.onChange(cb) subscribes to it.
Coordinate mode & camera preview
These have dedicated pages; the entry points live on game:
game.tracking.setCoordinateMode('roi', { framing: 'upper-body' });
game.tracking.setCoordinateMode('full'); // camera-FOV [0,1]
game.cameraPreview.show({ rect: { x: 0, y: 0, w: 1, h: 1 }, mirror: true });
game.cameraPreview.hide();
const shot = await game.cameraPhoto.capture({ maxWidth: 720 });
if (shot.ok) useJpeg(shot.photo); // data URL
- Coordinate modes & framing → Camera Preview & Coordinates
- The camera photo capability → Camera Photo
Pose helpers (build gestures yourself)
game-kit also exports pure helpers for reading a pose. All operate on the
landmark arrays above.
| Export | What it is |
|---|---|
LANDMARKS |
Name → index map (above). |
resolveTrackingPointer(frame, opts?) |
Resolve a single [0,1] cursor from a frame (wrist/hand fallback chain). |
Body |
Wraps a landmark array: body.rightWrist, body.shoulderWidth, body.torsoCenter, body.hipCenter, body.toBodyFrame(point), world-space getters. Build with Body.from(landmarks). |
HandIsolator |
Locks onto one hand across frames with hysteresis (update({ leftHand, rightHand, dt })). |
OneEuroFilter |
1€ smoothing filter for jittery signals (filter(x, tMs)). |
getTorsoCenter, getBodyBounds, distance |
Small geometry helpers. |
normalizeRuntimeTrackingFrame(frame, opts?) |
The normalization onNormalizedTrackingFrame uses, callable directly. |
normalizeRemoteAction(input) |
The remote-action normalizer. |
The lower-level client (iframe-sdk)
Most games never need this. When you want direct control of the bridge, the
bodylink-platform/iframe-sdk entry exports createBodyLinkIframeClient(...), which
createBodyLinkGame builds on. It exposes client.on(kind, cb),
client.hello(payload), client.requestCalibration(...),
client.cameraPreviewSet(...), client.requestCameraPhoto(...),
client.waitUntilReady(), client.diagnosticLog(...), and client.dispose().
You reach the same instance via game.client.
Related pages
- Your First Game with Vite — see this API in a running game.
- Camera Preview & Coordinates — coordinate modes, framing, and the preview slot.
- Manifest Reference — the capabilities your game must declare to use these APIs.