FOLLOW THE COACH / COMPLETE TUTORIAL

From headset
to feedback.

This is the whole loop of the project. Read one chapter, change one value, and use the result to explain what the game is doing.

01The pipelineWhat each layer is responsible for 02Tracking and coordinatesHow Quest data reaches the scene 03Targets and scoringHow a pose becomes a test 04ChoreographyHow custom poses are recorded 05Sound and hapticsHow numbers become feedback 06MultiplayerWhat is shared and what stays local
01 / PIPELINE

One frame of the game

Every animation frame repeats a short loop. The headset and controllers provide input; the Coach code turns that input into a target comparison; the renderer and feedback systems show the result.

Questhardware tracking
→
WebXRposes in JavaScript
→
Coach logictargets, timing, score
→
Feedbackscene, sound, vibration

Start with model.animate(() => { ... }) in coach.js. That callback is the frame loop.

02 / TRACKING

From headset matrix to a body frame

WebXR supplies a 4×4 transform matrix for the headset. In this project, the translation is stored in entries 12–14. The experiment below lets you change the player's head position and a hand's local position, then calculates a simple world position.

Matrix translationheadM.slice(12, 15) → [0.00, 1.60, 0.40]

World target[−0.25, 1.90, 0.30]

top view

headP = headM.slice(12, 15) extracts the position. bodyFrame() then turns “left / up / forward from the player” into a world-space point.

03 / JUDGEMENT

A target is a point with tolerance

The pose table stores hand positions relative to the player's head. The scene converts them into two target spheres. A controller counts as correct when its distance from the target is at most TOL = 0.28 metres, and both hands must pass during the second beat.

target centreedge of tolerance
PERFECT RANGE

The hand is close enough to build a hit.

distance(hand, target) ≤ 0.28then hold it for about 0.12 seconds → hit

Try dragging past 0.28 m. This is the same kind of comparison used in coach.js; scoring then turns the hit into PERFECT or GOOD.

04 / CHOREOGRAPHY

Recording is just collecting pose objects

While the player holds the beam on the Coach, the scene converts both controller positions back into the player's body frame. Every pose change stores one object. Four objects become a custom routine; releasing early keeps at least two.

const routine = [];

The real project writes the same shape to coach.live, broadcasts it, and later reads it through poseAt(). This sandbox is deliberately small so the data structure is visible.

05 / FEEDBACK

Music is scheduled from the same clock

The game does not load a music file. It converts MIDI note numbers to frequencies and schedules oscillators, noise, and envelopes with WebAudio. The beat clock also drives controller ticks and pose changes.

Read the real synthesizer in coachMusic.js. Try 0.60 seconds: that is Dance mode's beat. The scene calls it from the shared clock.

06 / MULTIPLAYER

Share the clock, keep each score local

The first connected headset periodically broadcasts a beat-clock sample. Other clients interpolate between samples. Each player sends their own pose result, so one player's score cannot overwrite another's.

Headset A12.00 s
Headset B11.96 s

This is why a partner avatar can look slightly delayed: the remote state arrives as messages, then the second client renders the latest known pose. See server.synchronize('coach') and the partner-body code.