Skip to content

Scenes & prefabs

A scene is a JSON document (.vscn.json) describing a tree of entities plus environment settings. The Zod schemas in packages/engine/src/scene/schema/ are the single source of truth for the format — see the generated Scene Schema reference.

loadScene(url, ctx) fetches and parses a scene (a failed fetch — 404, 500 — throws with the URL and status rather than surfacing as a cryptic JSON parse error); loadSceneFromData(data, ctx) skips the fetch. Either way the loader runs three passes (packages/engine/src/scene/scene-loader.ts):

Three passes: 1 spawn entities and build id map, 2 create joints, 3 init components
The loader's three passes.
  1. Spawn. spawnEntity recurses into each entity’s children and registers every id (including nested ones) in an id → Object3D map.
  2. Joints. createSceneJoints links physics bodies together using that id map (a joint references its targets by id).
  3. Init. componentManager.initAll() runs every component’s init() — now that all entities and joints exist, so an init() can safely reference other entities.

loadScene returns a SceneInstance with an update(dt) to tick per frame and a dispose() to tear everything down.

packages/engine/src/scene/defaults.ts is the single source of default values shared by the editor and the runtime, so what you preview in the editor is what the game renders.

A prefab (.prefab.json) is a reusable entity template: { version, name, root }. A scene entity references one via a prefab field. When an entity has both a prefab and its own fields, mergePrefabInstance takes the prefab definition first, then applies the per-instance overrides on top. The merge is per-field: transform is replaced wholesale when the instance sets it (never blended field-by-field); other sections (mesh, material, physics, shadow, animation, audio, particles, spline, render, …) are shallow-merged — the instance’s fields override the prefab’s same-named fields, with a few nested sub-objects (e.g. physics.collider, material.clearcoat) also deep-merged so a partial override doesn’t drop the prefab’s sibling fields; components are deep-merged by name; joints are replaced wholesale; and children are appended — so one prefab can have many customized instances. The loader caches fetched prefab and material files.

Helper queries walk the live tree:

  • queryByTag(root, tag)
  • queryByName(root, name)
  • queryByComponent(root, componentName)

Metadata (tags, components, the entity definition) is stored in each object’s userData.