Worlds and roots
A vgai game is a set of roots. There is no single “scene file” that is the game, and for a 3D world there is no scene format at all — the TSX you write is the document.
The manifest declares the roots
Section titled “The manifest declares the roots”vgai.project.json carries a roots array. It is required, it must be non-empty, and it is the
only place roots come from — nothing is discovered by scanning folders.
{ "manifestVersion": 2, "name": "My Game", "roots": [ { "id": "world", "adapter": "three", "entry": "src/world.tsx" }, { "id": "ui", "adapter": "dom", "entry": "src/ui/game.tsx", "zOrder": 100 } ]}Each root gets its own document, its own lifecycle, and its own render layer, z-ordered as siblings. That manifest is the actual starter: a 3D world and a React UI layer, running side by side.
The root’s adapter declaration is the sole discriminator. There is no separate kind
field and no implicit default root.
What each adapter means — and what it hands you
Section titled “What each adapter means — and what it hands you”The adapter name describes what the host provides, not which library you use.
adapter | The host gives you | The document is |
|---|---|---|
three | the one shared three instance | your TSX/R3F source |
canvas | a canvas element | the root’s own source |
dom | a DOM container | your JSX — JSX is truth |
{ module, surface } / { ingest, surface } | the declared surface plus adapter implementation | the game’s own source |
The surface name is the host’s promise, not a library allegiance. First-party Canvas roots use PixiJS; Phaser and Babylon currently enter through their own ingest adapters.
Source is the document (three roots)
Section titled “Source is the document (three roots)”For a three root the entry TSX file is not a description of the world that something else
loads. It is the world. The editor’s hierarchy, gizmo and inspector drive the live fiber scene
and write literal props back into that same file surgically — drag a mesh in the viewport and
its position={[…]} tuple updates in your source.
That has two consequences worth internalising:
- A prefab is a React component. Extract some JSX into a function and reuse it:
<Crate position={[2, 0, 0]} />. Sharing a material is sharing props or a module. There is no separate reusable-entity file format, because ordinary composition already does the job. - Behavior is a hook, not a class. A component calls
useFrame((state, dt) => …)and runs under the engine loop, so pausing freezes it. Its props are its config, typed from the JSX literal, so the inspector can edit them without a schema you had to declare separately.
The manifest is strict
Section titled “The manifest is strict”The source is the document on every first-party surface — there is no scene file format. The manifest schema rejects an unrecognized key loudly rather than quietly ignoring it: Zod’s default behaviour would otherwise strip the unknown key and leave you debugging an empty world.
Querying the live world
Section titled “Querying the live world”Metadata lives in each object’s userData, and helpers walk the live tree:
queryByTag(root, tag)queryByName(root, name)
These work on the running Three.js graph rather than a serialized scene document.
See also
Section titled “See also”- Entities & components — the node is the entity.
- Source is truth — how the editor writes your source back.
- The starter world — what a new project actually opens to.