Skip to content

Prefabs ​

A prefab packages a fully configured entity — its children, components, materials, physics, animations, and scripts — into a reusable .prefab asset. Configure something once (a third-person character, a pickup, an enemy), save it as a prefab, then reuse it everywhere: drag copies into the scene, or spawn them from scripts at runtime.

File anatomy ​

A .prefab file is JSON:

jsonc
{
  "version": 1,
  "name": "Enemy",
  "snapshot": {
    "entities": [1, 2, 3],          // ids renumbered 1..N, root first
    "components": [                  // same columnar shape as a world snapshot
      { "component": "PrefabNode", "entries": [ /* stable per-node uuids */ ] },
      { "component": "TransformComponent", "entries": [ /* … */ ] }
      // …every other serialized component
    ]
  }
}
  • Every entity carries a PrefabNode { nodeId } — a stable uuid used to match instance entities back to the file across sessions and machines.
  • The root's ParentId is always null; entity references inside the subtree (script EntityInput/ComponentInput fields, instanced-mesh slots) are remapped on instantiation. References that pointed outside the subtree are nulled when the prefab is created.
  • Scene singletons (Environment, Postprocessing) never serialize into a prefab. Cameras and image targets cannot be turned into prefabs.

Creating and editing (editor) ​

  • Create: drag an entity from the Hierarchy into the Asset Browser. The dragged entity automatically becomes the first linked instance.
  • Edit: double-click the .prefab tile — the editor enters Prefab Mode, showing only the prefab's contents with the full inspector. Saving writes the file; closing returns to the scene and applies the changes to every instance.
  • Instantiate: drag the .prefab from the Asset Browser into the viewport or onto a Hierarchy row.

Instances and per-property overrides ​

Scene instances stay fully editable. Any property you change on an instance (a material color, a script field, a child's transform) becomes an override: when the prefab asset is edited later, overridden properties keep their per-instance values while everything else syncs from the file.

Always per-instance (never synced): the instance root's position / rotation / scale, name, and place in the scene hierarchy.

Rules of thumb:

  • Components or children you add to an instance are yours — prefab updates never remove them.
  • Components or children you delete from an instance come back on the next prefab apply (deletion tracking is not supported).
  • "Unpack" in the inspector detaches ONE level — the objects lose their link but any nested instances inside survive as standalone instances.

Nested prefabs ​

Prefabs can contain other prefab instances: drag a prefab into an open prefab (Prefab Mode), or create a prefab from a subtree that contains whole instances. Node identity is scoped per placement, so a prefab may nest two copies of the same child prefab and each keeps independent overrides.

  • Editing a child prefab cascades: every prefab file that nests it is patched (overrides preserved), and every scene instance updates when you close the editor — transitively through any depth of nesting.
  • A prefab can never contain itself, directly or through another prefab (the editor rejects the drop).
  • Only whole instances nest — a partial slice of an instance can't become a prefab.
  • In the scene, content can't be re-parented across a nested instance boundary (its identity would change); open the host prefab to restructure.
  • Runtime spawning is unaffected: files are fully materialized, so spawnPrefab needs no resolution regardless of nesting depth.

Spawning at runtime ​

Scripts spawn prefabs with this.spawnPrefab(source, options?). Declare an AssetInput field and drag the .prefab onto it in the editor:

ts
import { Behaviour } from "@relu-interactives/spatial-ecs";

export default class EnemySpawner extends Behaviour {
  data = {
    // AssetInput — drag Enemy.prefab onto this field in the inspector
    enemyPrefab: { path: "", type: "prefab" },
    spawned: [] as number[],
  };

  private async spawnWave() {
    for (let i = 0; i < 3; i++) {
      const rootId = await this.spawnPrefab(this.data.enemyPrefab, {
        position: { x: i * 2 - 2, y: 0, z: -6 },
      });
      this.data.spawned.push(rootId);
    }
  }

  private despawnWave() {
    for (const id of this.data.spawned) {
      this.despawn(id); // removes the root and all of its children
    }
    this.data.spawned = [];
  }
}

Details:

  • spawnPrefab resolves once the entities exist. Heavy assets inside (models, textures, audio) stream in reactively — repeated spawns of the same prefab share geometry through the model cache, so waves are cheap.
  • source can be an AssetInput ({ path, type }), a URL string, or a pre-loaded PrefabAsset.
  • Options: { parentId, position, rotation, scale } — the transform applies to the spawned root in its parent's local space.
  • world.spawnPrefab(...) / world.despawnPrefab(id) expose the same calls outside of behaviours.

Preloading and caching ​

Each prefab URL is fetched and parsed once per session (loadPrefab caches the parsed file; failed loads retry). Published experiences preload prefab files referenced by script AssetInput fields — plus the models and textures inside them — before the experience starts, so the first spawn does not hitch.

Custom hosts ​

Prefab files store editor-space asset URLs. Published and preview builds register a resolver that maps them onto the deployed assets/ folder:

ts
import { setPrefabAssetUrlResolver } from "@relu-interactives/spatial-ecs";

declare function rewriteToMyCdn(url: string): string;

setPrefabAssetUrlResolver((url) => rewriteToMyCdn(url));

The standard project templates already do this — you only need it when embedding the runtime in a custom host.