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-kit entry point — the real import specifier is bodylink-platform/game-kit (the game-kit subpath of the installed bodylink-platform package):

import { createBodyLinkGame } from 'bodylink-platform/game-kit';
        

Your generated game-app.js already writes that import line — the examples below start from the returned game object. 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.runtracking.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.hands is not normalized. Derive hand positions from the converted landmarks (LEFT_WRIST 15, RIGHT_WRIST 16, LEFT_INDEX 19, RIGHT_INDEX 20) or read the converted copy at frame.normalizedHands — never mix raw frame.hands with 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
        

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