Skip to content

The tri-world capstone — one clock, three native renderers

tutorial lesson Intermediate ⏱ 12 min vgai 0.1.0

Prerequisites: Build a multi-world game · Add a React world

What you'll learn

  • Declare a three-root game (threejs + pixijs + react) in vgai.game.json
  • Share live clock state from a Three.js GameComponent with PixiJS and React
  • Author readable hierarchy names with native renderer and accessibility semantics

What you'll build: A minimalist abstract clock whose Three.js point field, PixiJS dial, and React annotations all read one live state source.

Walkthrough — The tri-world capstone — one clock, three native renderers · generated by site/media/lessons/learn-triworld.lesson.ts (5 steps)

This is the capstone of the multi-root arc: one game, all three built-in adapters. Three.js owns a point-field sculpture and the clock state. PixiJS reads that state to draw a precise dial. React reads the same state to render sparse annotations. They remain native renderer trees, but the host mounts them as one Game and the editor presents them through one hierarchy.

The Three.js scene attaches SignalField to TemporalClockField. The component owns the clock values as well as the point sculpture:

src/scripts/components/signal-field.ts (excerpt)
export class SignalField extends GameComponent {
hours = 0;
minutes = 0;
seconds = 0;
milliseconds = 0;
timeLabel = '--:--:--';
update(dt: number, ctx: GameContext): void {
const now = new Date();
this.hours = now.getHours();
this.minutes = now.getMinutes();
this.seconds = now.getSeconds();
this.milliseconds = now.getMilliseconds();
// Update the native Three.js point field here too.
}
}

Both scene formats resolve behavior from their native registries:

src/scripts/registry.ts
export const componentRegistry = { SignalField } satisfies ComponentRegistry;
export const pixiComponentRegistry = { TacticalMap } satisfies Component2DRegistry;

PixiJS receives the shared Game in its world context and queries the authoritative component. There is no mirrored clock model:

src/scripts/components/tactical-map.ts (excerpt)
const field = ctx.game?.queryByComponent(SignalField, { worldId: 'main' })[0];
if (!field) return;
const secondAngle =
((field.seconds + field.milliseconds / 1000) / 60) * Math.PI * 2 - Math.PI / 2;

React uses the same query through useGameState, which subscribes to selected-value changes without a polling timer:

hud.tsx (excerpt)
const state = useGameState((game) => {
const field = game.queryByComponent(SignalField, { worldId: 'main' })[0];
return {
timeLabel: field?.timeLabel ?? '--:--:--',
signalStrength: field?.signalStrength ?? 0,
};
}, shallow);

Hierarchy identity comes from each renderer’s ordinary conventions:

  • Three.js: Object3D.name
  • PixiJS: Container.label / display-object labels
  • React: accessible HTML — aria-labelledby, aria-label, headings, direct text, alt, title, or id, with the tag name only as a fallback

For example, the HUD’s section is regular semantic HTML:

<section aria-labelledby="projection-state-title">
<h2 id="projection-state-title">PROJECTION STATE</h2>
<p>PixiJS reads the live component directly—no mirrored model.</p>
</section>

The hierarchy calls that node PROJECTION STATE. No vgai-only naming attribute is required. An explicit data-vgai-name remains supported only as an escape hatch when a native tree genuinely has no useful semantic identity.

  1. Press Play. One Game mounts the Three.js canvas, the transparent PixiJS canvas, and the React DOM root. Together they form one restrained, geometric clock.

    The abstract tri-world clock running in the editor
    Three native renderers, one composition.
  2. Watch the clock move. The Pixi hands and React metrics change with the real local time because both query the SignalField living in the Three.js root.

    The live clock hands and React temporal annotations
    Cross-world state through the shared Game root, with no mirror or polling loop.
  3. Read the hierarchy. Expand Three.js, PixiJS, and React. You see domain names such as TemporalPointBloom, ClockDialConstruction, and PROJECTION STATE, not raw renderer primitives such as Graphics or section.

    The tri-world hierarchy with semantic clock-domain names
    Native naming and accessibility semantics produce an intelligible tree.
  4. Select TemporalProjection. The same hierarchy and inspector now route to the PixiJS root and expose its 2D transform.

  5. Edit Position-Y, then Stop and Play. The edit is visible immediately while you inspect the running game. A new Play session reconstructs the root from authored source, so the value returns to 0. Play mode is an ephemeral preview; durable edits belong in Edit mode or the source scene.

    TemporalProjection selected in the PixiJS inspector
    The inspector is shared; Play-mode mutation is intentionally temporary.

Recap

New functionality

  • Declared a three-root game in vgai.game.json
  • Shared one live clock component with PixiJS and React
  • Inspected all three native trees through semantic hierarchy names

New concepts & skills

  • one Game mounts every adapter root and stacks surfaces by zOrder
  • queryByComponent spans roots without a mirrored state model
  • native object names and accessible HTML are the hierarchy contract
  • Play-mode edits are ephemeral previews

Next lesson → Multi-world games & play control (manual)

On Your Own!

Extend what you built:

  • Add a date ring to the PixiJS projection using the same SignalField
  • Create a second React world for an interactive clock settings panel
  • Expose a time-zone property on SignalField and add it to Storybook design states