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.
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.
Start with model.animate(() => { ... }) in coach.js. That callback is the frame loop.
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]
headP = headM.slice(12, 15) extracts the position. bodyFrame() then turns “left / up / forward from the player” into a world-space point.
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.
The hand is close enough to build a hit.
distance(hand, target) ≤ 0.28then hold it for about 0.12 seconds → hitTry 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.
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.
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.
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.
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.