Skip to content
hikari
RenderingEditorAIToolchainDocumentation
Download

© 2026 Flying Rat Studio.
All rights reserved.

Explore the engineDocumentationContributorsDownloadLicenseBack to top
Documentation / Systems
Browse docs
Overview
Tutorials17
OverviewPlay, Edit, and scenesFirst UI documentFirst game projectFirst entityComponent properties and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides25
OverviewWorking in the editorDevelopment guideGameplay APIActor and component lifecycleThreads, jobs, events, and routinesGameplay AI tasksGame AI: perception and movement playgroundBrain assetsBrain execution for swarm populationsLocalizationGame cursorsChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringChoosing and scaling shadowsTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / Unreal
Systems32
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorPipeline compilationSkeletal animationGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitUI documentsAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)Document schemasUI and editorEditor asset hot reloadEditor Project Selector
Language reference3
OverviewAkari GPU ABIAkari language referenceKawa language reference
Engine overview
Start exploring
  • No matching sections. Try fewer words or another topic.
NavigateEnter Openesc Close
Overview
Tutorials17
OverviewPlay, Edit, and scenesFirst UI documentFirst game projectFirst entityComponent properties and the inspectorFirst mesh and materialFirst physics body and triggerFirst character controllerFirst session servicesFirst UIFirst input actionFirst messagesFirst runtime spawnFirst motionFirst skeletal animationAssets in Play (soft refs and hot reload)Dynamic editor recompile
Guides25
OverviewWorking in the editorDevelopment guideGameplay APIActor and component lifecycleThreads, jobs, events, and routinesGameplay AI tasksGame AI: perception and movement playgroundBrain assetsBrain execution for swarm populationsLocalizationGame cursorsChoosing component storageBuild and packagingProject filePluginsHikari Plugin APIData-driven content, JSON, and pathsScripting with KawaShader authoringChoosing and scaling shadowsTime of dayUser interfaceUI layoutUI widgetsMigration from Unity / Unreal
Systems32
OverviewArchitectureApplication lifecycleFrontends and driversPlatforms and supportSession services and cross-scene stateScenes and gameplayActor communication (hi.actors)Game-facing refsSave / replication wire versionCoordinate space and camera conventionsRenderingRenderer architecture mapFrame governorPipeline compilationSkeletal animationGPU particlesVisual ZonesVolumetric mediaInputAudioPhysicsMotion KitTemporal KitUI documentsAssets and ShinraPrefabsAsset residencyAsset formats (Shinra pipeline)Document schemasUI and editorEditor asset hot reloadEditor Project Selector
Language reference3
OverviewAkari GPU ABIAkari language referenceKawa language reference
Engine overview
Systems17 min read

Prefabs

On this page
On this pageSummaryCreate and reuse a prefabAuthoring formatVariants (base)Shared expandNested prefabsLoaderHydrate / dehydrateOverride modelApplyRevertRefresh (live document only)UnpackEditor UXPlane ops (MCP scene_edit wire names)ChromeOpen Prefab document modeRuntime SDKBulk spawn jobfindPrefabMemberSampleInvariants Back to top
FieldValue
StatusNested instances + variants; Shinra .shinprefab cook; the runtime loads cooked prefabs only.
AudienceLevel designers, game developers, engine contributors
RelatedScenes and gameplay, Assets and Shinra, Gameplay API

Summary

A prefab is a reusable actor subtree asset. A scene references an instance and records what differs from the template; the engine expands it at load (prefab/hydrate.zig). Expanded actors exist in memory only — each carries a prefab link stamp for identity, and every consumer downstream (editor hierarchy, physics, selection, undo, MCP) sees the same flat actor list it always did.

Scenes resolve their prefab templates when opened. Changes made outside the editor appear on the next scene open; an already-open scene uses the explicit refresh paths described below.

Overrides are per field, addressed as render.material, light.intensity, transform.position, components.<name>. Overriding a material therefore leaves casts_shadow tracking the template — the failure mode of block-granular overrides, where touching one field silently freezes every field beside it.

Create and reuse a prefab

These steps use the editor; no code or JSON editing is required. Start in Edit mode with a saved scene.

  1. Select the root of the object you want to reuse in the Hierarchy. Its children become part of the prefab too.
  2. Open the root's context menu and choose Make Prefab…. Give it a unique name. The editor creates the asset and links the selected instance to it.
  3. In the Asset Browser, select the prefab and use Place Prefab to add another instance. Move, rotate, or scale its root to position the whole object.
  4. Change a property on one instance. That change becomes an override: other instances continue to use the template's value.
  5. To change the template itself, choose Open Prefab, edit it, and choose Save Prefab. Use Close Prefab to return to your scene, then save the scene.
Action on an instanceResult
ApplyWrites that instance's changes into the shared prefab asset and refreshes other open instances while preserving their overrides
RevertRestores template values on the instance while keeping its root placement
UnpackKeeps the objects but removes their prefab link
Create Variant…Creates another prefab that inherits from this one and stores its own changes

Apply and Make Prefab write asset files that scene Undo cannot restore. Save or version your work before changing a shared template. Revert and Unpack are undoable scene edits. Base-owned actors in a variant appear locked; edit the base prefab to change them.

The remaining sections document file formats, expansion, and runtime behavior for programmers and engine contributors.

Authoring format

json
{
  "kind": "com.hikari.prefab",
  "version": 1,
  "id": "crate_stack",
  "name": "Crate Stack",
  "root": "root",
  "actors": [{ "id": "root", "archetype": "empty" }]
}
  • On disk: assets/prefabs/<stem>.prefab.json (authoring).
  • Cooked: .shinprefab (the document as a schema-neutral value tree in the cooked container, like .shinscene) at the same relative path as its source under cook/work — never next to authoring JSON. Shinra finds prefabs anywhere under --input by suffix or kind tag, like every other authored document; Kaji stages the cooked file to the same relative path under data/.
  • Runtime load (World.retainPrefab / AssetStore): prefer cooked .shinprefab (no JSON parse). Authoring .prefab.json only in Debug (mirror scene policy).
  • A scene references instances in a top-level instances array (never expanded actors):
json
{
  "instances": [{
    "id": "lamp_blue",
    "prefab": "prefabs/street_lamp",
    "transform": { "position": [-7, 0, 0] },
    "overrides": [
      { "local_id": "bulb", "path": "render.material", "value": "asset://./materials/blue" }
    ],
    "removed": ["banner"],
    "added": []
  }]
}

Members mint {id}__{local_id} at expansion, so anything referencing a member by scene id (joints, scripts) keeps working. The per-actor prefab stamp still appears inside a .prefab.json, where it marks a nested instance member — scene_json_validator.Schema is what keeps the two documents apart while they share one decoder.

Variants (base)

Optional on version 1:

json
{
  "kind": "com.hikari.prefab",
  "version": 1,
  "id": "enemy_red",
  "name": "Enemy Red",
  "base": "prefabs/enemy",
  "root": "root",
  "actors": [{ "id": "root", "archetype": "empty" }]
}
  • base: extension-free stem or asset://./… of the parent prefab.
  • Variant actors may be a full copy or sparse (only overridden locals + structure adds).
  • Resolution (scene/prefab/resolve.zig):
    1. Load/resolve base recursively (cycle detect + depth limit, default 8).
    2. Start from base’s fully resolved actor list.
    3. Matching local ids: merge authoring fields from variant onto base (Apply-style via diff.zig).
    4. Actors only in variant: append (structure add).
    5. Actors only in base: keep.
    6. Result is a fully resolved PrefabAsset with variant id/name/root and base == null.
  • Missing base → hard error on resolve.
  • Base chain cycle → PrefabCycle.
  • Editor Open Prefab edits the variant document as-is; base is preserved on save and editable via inspector Base prefab (set_prefab_base).
  • Make Prefab writes no base by default. Create Variant… writes a new prefab with base set (optional sparse overrides from a linked instance).

Shared expand

Module: src/hikari/src/scene/prefab/

  • expandToSceneActors(allocator, prefab, options) ![]SceneActorDesc
  • Options: instance root_id, optional root parent_id, root transform override, asset_uri for links, optional reserved host ids, optional id_probe, optional loader for nested re-hydrate, max_nest_depth (default 8).
  • Child scene ids: {root_id}__{local_id} (collision sanitize with -N suffix). Ids over expand.id_buf_len (512) fail with PrefabIdTooLong.
  • Root placement (parent and transform) is applied on the instance root whether it expands as a plain actor or as a re-hydrated nested root.
  • reserved is for small caller-owned sets (an open document). A host with a large live index passes id_probe instead — World.spawnPrefab does, so a spawn costs one lookup per prefab actor rather than a copy of the whole scene-id index per instance.
  • Pure logic — used by editor place/make and World.spawnPrefab.

Nested prefabs

  1. A prefab asset may contain actors that are themselves prefab instance members (ScenePrefabLink pointing at another prefab).
  2. Validate allows nested links. Members sharing prefab.root_id must share the same prefab.asset (NestedGroupInconsistent otherwise). Parent-id cycles still rejected.
  3. Make Prefab allows selection that includes nested instances; nested links are stored (with root_id remapped when the nested root is in the subtree). Links for the selection instance (same prefab.root_id as the selection root) are cleared so co-members become outer-owned locals — only true nested groups (different root_id still in the subtree) keep leaf stamps.
  4. On expand of an outer prefab (when loader is set):
    • Remap all scene ids as usual.
    • For each distinct nested instance group (asset + template prefab.root_id):
      • Load nested asset via loader, resolve variants, expand under the remapped nested-root scene id.
      • Apply overrides from the outer template’s stored members (mergeRefresh vs nested source) onto re-expanded actors.
      • Nested members keep leaf prefab links (not the outer asset); root_id is the new nested root scene id.
    • Soft path if load fails: keep stored expanded members, remap ids/root_id, warn once — content is not hard-broken offline.
  5. Cycle detection walks the prefab asset stem graph during nested load; depth limit stops runaway nests.
  6. Apply / Revert / Unpack still work per leaf link on the instance. Unpack clears members matching that instance root_id (nested leaves unpack separately).

Loader

zig
pub const PrefabLoader = *const fn (ctx: *anyopaque, allocator: Allocator, stem: []const u8) anyerror!PrefabAsset;
// free via prefab.free after use

pub const Loader = struct { ctx: *anyopaque, load: PrefabLoader };

pub const ExpandOptions = struct {
    // …
    loader: ?Loader = null,
    max_nest_depth: u8 = 8,
};

World retainPrefab / spawnPrefab and editor place/make inject a loader that reads cooked .shinprefab (AssetStore) first; authoring .prefab.json only in Debug. Pure tests supply fixtures without World.

retainPrefab resolves variants before caching; expand re-hydrates nested instances via the same loader.

Hydrate / dehydrate

DirectionModuleWhen
instances → actorsscene/prefab/hydrate.zigEvery scene load, both runtime and editor
actors → instanceseditor/scene_document/dehydrate.zigDocument save

Hydrate runs inside the scene decoder, not in world.loadScene: scene_loader/preload.zig walks asset.actors to retain materials and meshes before the world loads, so a member appearing any later would render without its assets resident. Order per instance is fixed — resolve variants → expand (nested instances re-hydrate through the same loader) → drop removed → apply overrides → append added. Templates are decoded once per stem per load, so twelve lamps cost one decode. A broken instance warns and contributes nothing rather than failing the scene.

Dehydrate needs no bookkeeping during editing, which is why scene_document/{commands,edits_*,apply}.zig and undo were untouched by this model. Hydrate stamps each actor with its outer authored-instance ownership while retaining the leaf prefab link used by nested prefab operations. Save groups by that ownership stamp, treats an unstamped actor under a member as an added, treats an expected template local with no actor as a removed, and derives overrides from the diff.

The prefab loader is injected (prefab.Loader): runtime passes a store-backed one, the editor a path-backed one, both from scene/prefab_source.zig. A scene declaring instances with no loader fails with error.PrefabLoaderRequired — dropping them silently would open a scene that looks fine and is missing half its content.

Override model

Module: src/hikari/src/scene/prefab/patch.zig (pure; testable without editor UI).

ConceptRule
BaselinePrefab asset actors (local ids)
CurrentExpanded scene actor with matching prefab.local_id
OverrideOne differing leaf, addressed by path
Identityid, parent_id, prefab link are never addressable
Root TRSInstance-owned placement, recorded on the instance, never an override

Paths are derived from the struct definitions, not from a table. classify treats an optional struct as a component block (block.field), special-cases transform and components, skips identity, and calls everything else a top-level leaf — so a field added to SceneRenderDesc is overridable the day it lands. Values are JSON text encoded/decoded by std.json over the typed field, so the value format follows the schema for free too. A path that no longer resolves (the template dropped the field) warns and is skipped; it never half-applies.

Game and plugin components get the same granularity, addressed as components.<name>.<key>. The engine has no schema for those payloads and must not grow one, so the split happens over the JSON object itself — each top-level key of the payload is a leaf, merged back in on apply. A component overriding components.cube_logic.health therefore keeps following the template's max_health, exactly like render.material and render.casts_shadow. Three cases stay whole-payload because no key path can express them: the component being added, removed, or holding a non-object payload, and a key the instance dropped (JSON null is a legal value, so it cannot double as "absent").

diff.zig remains for actor-granular work — variant resolution (mergeApplyOwned), Apply, and the inspector's override queries.

Apply

  1. Resolve instance root from selection (member or root → prefab.root_id).
  2. Load current prefab authoring file from disk.
  3. Merge non-root-transform instance fields into template locals.
  4. Write asset (non-undoable file write); preserves variant base when present.
  5. Plane refresh_prefab (or internal refresh from apply_prefab) updates other instances of the same stem using old→new delta (undoable document txn). Source instance already holds applied values.

Editor entry: applyPrefabInstance(app, root_or_member_id).

Revert

  1. Load prefab asset.
  2. For each linked member: mergeRevertOwned (root keeps placement).
  3. SceneDocument.replaceActor per member in one undo transaction.
  4. Mutation sink despawn/spawn updates the live edit world.

Editor entry: revertPrefabInstance(app, id).

Refresh (live document only)

Since every load expands from the template, refresh exists purely to update the already-open document, whose actors were expanded before the template changed. Closing and reopening the scene would achieve the same thing.

Plane op refresh_prefab (asset_stem + old_utf8 + new_utf8 + optional skip_root_id), used after Prefab Save on the stacked parent scene and by Apply. Internally:

  • For each open-document instance of asset_stem (optionally skip the Apply source root):
    • For each member: if a field equaled the old template, copy from new; true overrides stay.
    • Instance root transform always preserved.

Unpack

Clear prefab on every actor in the instance (root_id match). Data kept. Undoable via replaceActor.

Editor entry: unpackPrefabInstance(app, id).

Editor UX

All prefab mutations enter the shared plane editor/authoring/scene_mutation.zig (bodies in editor/authoring/prefab_ops.zig). Chrome and MCP never re-implement document/file logic outside that path.

Plane ops (MCP scene_edit wire names)

OpArgsNotes
make_prefabid (root), nameWrite assets/prefabs/<stem>.prefab.json (non-undoable) + replace subtree with linked expand (undoable). Scene mode only. Refuses an existing stem (DuplicateName) — the write is not undoable, and sanitizeStem collapses "Crate Stack" / "crate stack" onto one file.
place_prefabasset, optional parent_id / positionResolve + expand + insert tree. Scene mode only.
apply_prefabid (root or member)Merge instance → asset file (non-undoable); refresh sibling instances (undoable).
revert_prefabidTemplate → instance members (root TRS kept). Undoable.
unpack_prefabidClear prefab links on instance. Undoable.
create_variantbase_asset, name, optional from_instance_idWrite variant with base; optional sparse overrides from instance. File only (non-undoable); refuses an existing stem like make_prefab.
set_prefab_basebase string or nullPrefab document mode only; updates prefab_edit_base + dirty.

Chrome

  • Make Prefab… (hierarchy context): when authored and not in prefab document mode → name prompt → plane.
  • Create Variant…: when actor is prefab-linked (base = link stem) or while editing a prefab document (base = open stem) → name prompt → plane → open new file.
  • Place Prefab: asset browser kind prefab → plane place_prefab. Disabled while editing a prefab document.
  • Open Prefab: asset browser double-click; hierarchy Open Prefab on a linked instance; inspector Open Prefab on the instance card; File → Close Prefab to return. Open is chrome document-stack (not a scene mutation).
  • Apply / Revert / Unpack: hierarchy context menu + inspector prefab card → plane.
  • Hierarchy context menu filters rows by actor (not hard-coded indices): Always Focus / Add Child / Rename / Copy / Paste / Duplicate / Delete; Make Prefab only in scene mode; Create Variant when linked or prefab doc; Open/Apply/Revert/Unpack only when actor.prefab != null.
  • Hierarchy badges: prefab = top-level instance root; nested = instance root under a different prefab instance; member = non-root linked member; layers icon for all linked.
  • Inspector override accents (rebuild only, selected actor): section title accent when a whole builtin/open component block differs from the resolved template; Active checkbox accent when differs; Transform section accent only for non-root members (root TRS is instance-owned).
  • Prefab document: inspector Base prefab text field + Clear → plane set_prefab_base. Shown only on the prefab's root actor (PrefabDocOpts.root_id) or on an empty selection — it is a document-level control, and offering "Clear base" beside a banner reads as a property of that banner when it re-parents the entire template.
  • Reparent of a linked member out of its instance root is rejected.

Open Prefab document mode

Single SceneDocument chrome with a one-deep document stack (not a parallel document system):

  1. push — parent scene SceneDocument is stacked; main document is materialised from the prefab asset (local actor ids, path = authoring .prefab.json). Nested member links stay for authoring fidelity.
  2. Hierarchy/inspector edit the prefab actors as a mini-scene (live preview reloads session from scene-shaped UTF-8 of those actors).
  3. Save writes prefab format via prefab.writeUtf8 (not scene JSON), preserving base when the opened asset was a variant (or after set_prefab_base). On success, parent stack document instances of that stem are refreshed with old→new template delta (same as Apply sibling refresh).
  4. Close Prefab restores the stacked scene document and reloads the live world from it.

Make / Place remain disabled in prefab mode.

Two save modes. scene_document/io.zig: SaveOptions.prefab_members picks whether members fold into instances (.fold, the default, for persistence) or stay ordinary actors (.expand). The prefab document's live-preview payload takes .expand, because folding would ask the save path to resolve the very templates the document is authoring, which a prefab document has no assets root for.

Editing a variant: the base chain is visible

A variant file stores only what it changes, so on its own street_lamp_festive opens as three actors floating where a lamp should be — useless for authoring, because inheritance is exactly the thing you need to see. Opening a prefab with a base therefore resolves the whole chain, and the two consumers get different views of it:

holdswhy
Documentthis file's actors unmerged, plus base-only actors appended and lockedso save writes back exactly what this file owns
Live previewthe fully merged chainso the viewport shows the whole lamp

The split is load-bearing, not tidiness. resolve.mergeVariantActors merges fields: a variant actor that overrides light and inherits render comes back from the resolver carrying the base's render block. Put that in the document and the next save writes an inheritance the author never touched into the variant file — silently flattening the chain one actor at a time. So an id the variant owns is taken from the variant, verbatim; only ids it does not own come from the resolved chain, and those are locked.

documentFromPrefabAssetWithContext builds it; a chain that will not load warns and degrades to the raw file rather than refusing to open.

The chain, in the hierarchy. A variant is additive, so a flat merged list cannot answer who injects what. SceneDocument.origin_levels records the chain — furthest ancestor first, this file last, own_origin indexing this file — and actor_origin credits each actor to the deepest file that mentions it. Deepest, not first: if the base declares bulb and the variant overrides it, the variant is where you edit it, so that is where it belongs. That rule is what makes "which section is it in" and "can I edit it" the same question.

model.appendChainSections splits the prefab's layer into one hierarchy section per file, top to bottom in composition order, badged base / base 2 / … / editing. A chain file contributing nothing visible still gets a header row. prefab_document.walkChain builds the provenance by walking base a second time, because resolvePrefab returns only the merged result.

Locked actors are context: visible, selectable, inspectable, never edited, never saved. Locked is derived (origin != own_origin), not stored, so editability and the chain sections cannot drift apart.

  • scene_mutation.applyOne rejects any operation whose Operation.actorId() is locked with ErrorCode.read_only. It gates the subject, not the parent: create / insert / insert_tree under a locked actor stay legal, because adding a child adds to the variant.
  • prefabAssetFromDocument skips locked actors and sizes its output to the owned count (prefab.free releases asset.actors as a slice).
  • The gizmo never offers a locked actor: selection_flow.refreshOverlay clears the target for a locked primary and snapshotDragGroup skips locked ids in a multi-select, so a drag cannot open a transaction the plane rejects every frame.
  • Chrome hides destructive context-menu rows, the inspector builds read_only (the same mode Play uses), and the hierarchy row dims and takes prefab amber on its glyph and base badge.

Prefab mode chrome

editor_app/prefab_chrome.zig states the mode three ways: a 2pt amber frame inside the viewport rect, a band between the viewport sub-toolbar and the 3D view naming the template and the scene it returns to (with Close Prefab), and the toolbar Save button re-labelled Save Prefab. The hue is asset_browser.kindColor(.prefab). Save changes its word only, never its variant (the accent already means "unsaved authoring"). The chrome is derived from document_kind every frame, not latched by the open/close paths, so a discarded dirty prompt, a rolled-back open, or a project close all update it, and it re-bakes after a live theme switch.

Runtime SDK

zig
const ref = hi.PrefabRef.must("prefabs/crate_stack");
const handle = try hi.world().retainPrefab(ref);
defer hi.world().releasePrefab(handle);
const root = try hi.world().spawnPrefab(handle, .{
    .position = .{ 0, 1, 0 },
    .rotation_euler = .{ 0, 45, 0 },
    .scale = .{ 1, 1, 1 },
    .parent = .invalid,
    .active = true,
});
// Member lookup uses expand mint: "{root_scene_id}__{local_id}"
const lid = hi.world().findPrefabMember(root, "lid");
try hi.world().despawnPrefabInstance(root);
  • retainPrefab loads/decodes once (refcounted cache on World); resolves variants; nested links stay until expand. Cooked .shinprefab first; authoring JSON only in Debug.
  • spawnPrefab expands with unique root id + nested loader, batch-friendly spawn, parents attached after members exist.
  • Soft content refs on leaves match scene load (error mesh / _missing archetype).

Bulk spawn job

World-owned, multi-frame, one job at a time. Template is re-retained for the job duration so the game may release its handle early.

zig
const job = try hi.world().beginPrefabSpawnJob(handle, .{ .budget_ns = 2_000_000 });
try hi.world().prefabSpawnEnqueue(job, .{ .position = p0 });
try hi.world().prefabSpawnEnqueue(job, .{ .position = p1 });
// each frame:
_ = try hi.world().pumpPrefabSpawnJob(job); // true when complete
const roots = hi.world().prefabSpawnResults(job); // []ActorRef so far
// when done:
hi.world().endPrefabSpawnJob(job);
  • Each pump spawns full instances via existing spawnPrefab (one expand + batch entity allocate per instance) until budget_ns is exhausted (0 = one instance per pump).
  • Results accumulate until endPrefabSpawnJob. Result storage is sized at prefabSpawnEnqueue (it can never exceed the queue), so a slice from prefabSpawnResults stays valid across any number of pumps — a later enqueue or endPrefabSpawnJob invalidates it.

findPrefabMember

Runtime entities do not store ScenePrefabLink. Lookup uses the expand mint convention:

  • Root: ActorRef returned by spawnPrefab / job results.
  • Child: findPrefabMember(root, local_id) → findBySceneId("{root_scene_id}__{local_id}").
  • Collision suffixes (-N) are not searched.
  • Nested member locals are relative to the nested expand root; deep nested mints follow the same {parent_root}__{local} pattern.

Cook does not emit dependency edges for base or nested prefab.asset, so re-cooking a base does not invalidate variants. Product staging (StageCookedPrefabs) ships the whole prefabs/ tree.

Sample

hikari-games/example/scenes/prefab_showcase.json — a street of lamps and crates where every prop is an instance: plain instances, component and transform overrides, a variant (street_lamp_festive), and a nested instance (lamp_corner contains a street_lamp). Templates live in hikari-games/example/assets/prefabs/. The scene file stores unexpanded instances + overrides; the engine hydrates at load. Generator: hikari-games/example/tools/gen_prefab_showcase.py. The scene README walks through Open Prefab / Apply / Revert / Unpack / Create Variant on it.

Invariants

  • One expand + resolve + diff core under scene/prefab/ — never expand/diff-only logic solely under editor/.
  • Flat actor lists + parent_id only.
  • Nested expand is O(n) with maps; recursive with depth limit + asset-stem cycle detection; no per-frame work.
  • validateAlloc is O(n) (three-colour parent walk over an index table). No per-actor hashmap, and expand validates once per level — never twice at the same depth.
  • Prefab writer changes are guarded by comptime field-count tripwires in scene/prefab/asset.zig, the same contract editor/scene_document/serialization.zig carries. A new SceneActorDesc / render / physics / render-params field must be emitted there or the build fails.
  • Plane op bodies are !void with ordinary errdefer; only the thin public wrapper maps to plane.ErrorCode.
  • No per-frame prefab work; spawn / apply / revert are explicit.
  • Authoring structural/field edits go through document commands (replaceActor, mutation plane) so live sink + undo stay consistent.
  • Game modules see only PrefabRef / PrefabHandle / ActorRef via HostApi (no new SDK surface required for nested/variants).
  • Apply file write is intentionally non-undoable; document-side refresh/revert/unpack are undoable.
PreviousAssets and ShinraNext Asset residency

Documentation follows the current engine checkout.

Snapshot 86f12d68Source docs/systems/prefabs.md
On this pageSummaryCreate and reuse a prefabAuthoring formatVariants (base)Shared expandNested prefabsLoaderHydrate / dehydrateOverride modelApplyRevertRefresh (live document only)UnpackEditor UXPlane ops (MCP scene_edit wire names)ChromeOpen Prefab document modeRuntime SDKBulk spawn jobfindPrefabMemberSampleInvariants Back to top