Composed props building organic and complex objects from primitives in one prefab
entities array of project_create_prefab (project.prefab.create) may carry many entities, and parentLocalId on each one wires the hierarchy. That is the whole craft here: a tree is a trunk with branch children and leaf-cluster grandchildren; a bench is a frame with a seat slab, a backrest, and four legs; a rock cluster is oUse this for
Not for
Pairs with: Asset to instances model prefab instance authoring chain, Model authoring parametric primitives and CSG for custom meshes, Asset acquisition strategy, Scenery composition terrain-first workflow, sampling height, and visual verification
Composed props building organic and complex objects from primitives
The idea a prefab is a subtree
A prefab is not one entity. Its body is a whole entity SUBTREE: the entities array of project_create_prefab (project.prefab.create) may carry many entities, and parentLocalId on each one wires the hierarchy. That is the whole craft here: a tree is a trunk with branch children and leaf-cluster grandchildren; a bench is a frame with a seat slab, a backrest, and four legs; a rock cluster is one base boulder with satellite stones parented to it. You build the complex object ONCE, as one atomic prefab, then drop instances of it.
This is the same tool asset-to-instances teaches for the single-root case; this playbook is about using its FULL power, the multi-entity body.
The fields that carry the hierarchy
Each item in entities is a PrefabEntityDto. The load-bearing fields:
localId(required): a stable id unique WITHIN the prefab. Children point at it.key(required): the graph key (lowercase,/^[a-z0-9][a-z0-9._-]*$/).name(required): the human label shown in the outliner.parentLocalId(optional): thelocalIdof this part's parent. Omit or null on the root. A child'sTransformComponentis then LOCAL to its parent, so you pose branches relative to the trunk, not in world space.siblingOrder(optional number): deterministic order among siblings. Set it so the outliner and any ordered logic read the same every rebuild.components(default{}): the part's components, keyed by a free lowercase slot id with thetypeinside each value, exactly asproject_create_entitytakes them, e.g.{ transform: { type: "TransformComponent", position: {...} }, render: { type: "RenderableComponent", shape: "capsule", visible: true } }. Pickshapefrom the RenderableComponent shape enum (see docs/spec/generated/component-types.md). There is no cylinder or cone entity shape, so a trunk, a post or a leg is a capsule or a box; a true cylinder is a part inside a model (see model-authoring-parametric-csg).scripts(default[]),tags(default[]): as on any entity.prefabInstance(optional): make this part an instance of ANOTHER prefab (see "Nesting a prefab as a part" below).
Every entity also carries the standard content facets identity, editor, and runtime. They are all-defaulted, so pass {} for each to take defaults (an empty object is valid and fills the schema). The prefab as a whole needs a key, a name, and a rootLocalId that names the root entity's localId.
One note on the examples below: RenderableComponent.materialRef resolves to a material ASSET id (a uuid), not a symbolic name. The readable mat.bark / mat.foliage strings in the examples are stand-ins for those asset ids so the structure reads clearly; substitute a real material asset id, or omit materialRef to render the primitive with its default look, and skin the part afterward (see materials-and-looks).
Worked example one a tree (trunk, branches, leaf clusters)
Three depth levels in one call: a trunk root, two branch children, and leaf clusters as grandchildren. Local transforms position each part relative to its parent.
{
"key": "prefab.tree.oak_lowpoly",
"name": "Low-poly oak",
"rootLocalId": "trunk",
"entities": [
{
"localId": "trunk", "key": "tree.trunk", "name": "Trunk",
"siblingOrder": 0,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0, "z": 0 }, "scale": { "x": 0.4, "y": 3, "z": 0.4 } },
"render": { "type": "RenderableComponent", "shape": "capsule", "visible": true, "materialRef": "mat.bark" }
},
"identity": {}, "editor": {}, "runtime": {}
},
{
"localId": "branch_l", "key": "tree.branch_l", "name": "Branch left",
"parentLocalId": "trunk", "siblingOrder": 0,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": -0.6, "y": 2.2, "z": 0 }, "rotation": { "x": 0, "y": 0, "z": 40 }, "scale": { "x": 0.18, "y": 1.2, "z": 0.18 } },
"render": { "type": "RenderableComponent", "shape": "capsule", "visible": true, "materialRef": "mat.bark" }
},
"identity": {}, "editor": {}, "runtime": {}
},
{
"localId": "branch_r", "key": "tree.branch_r", "name": "Branch right",
"parentLocalId": "trunk", "siblingOrder": 1,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0.6, "y": 2.6, "z": 0.1 }, "rotation": { "x": 0, "y": 0, "z": -35 }, "scale": { "x": 0.16, "y": 1.0, "z": 0.16 } },
"render": { "type": "RenderableComponent", "shape": "capsule", "visible": true, "materialRef": "mat.bark" }
},
"identity": {}, "editor": {}, "runtime": {}
},
{
"localId": "leaves_l", "key": "tree.leaves_l", "name": "Leaf cluster left",
"parentLocalId": "branch_l", "siblingOrder": 0,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0, "y": 1.1, "z": 0 }, "scale": { "x": 1.4, "y": 1.2, "z": 1.4 } },
"render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.foliage" }
},
"identity": {}, "editor": {}, "runtime": {}
},
{
"localId": "leaves_r", "key": "tree.leaves_r", "name": "Leaf cluster right",
"parentLocalId": "branch_r", "siblingOrder": 0,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0.9, "z": 0 }, "scale": { "x": 1.2, "y": 1.1, "z": 1.2 } },
"render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.foliage" }
},
"identity": {}, "editor": {}, "runtime": {}
},
{
"localId": "crown", "key": "tree.crown", "name": "Top crown",
"parentLocalId": "trunk", "siblingOrder": 2,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0, "y": 3.2, "z": 0 }, "scale": { "x": 2.2, "y": 2.0, "z": 2.2 } },
"render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.foliage" }
},
"identity": {}, "editor": {}, "runtime": {}
}
]
}
The craft, not the JSON: a believable tree from primitives needs three moves. Break the silhouette (branches at DIFFERENT angles and lengths, never mirrored), overlap the foliage volumes so the canopy reads as one mass rather than separate balls, and let the crown sit slightly off-axis so it does not look stamped. The subtree makes each of these an independent, tweakable part.
Worked example two a rock cluster
One base boulder as the root, satellite stones parented to it so moving or scaling the cluster moves every stone together. Vary each stone's scale and rotation so no two read alike; that irregularity is what makes rock look like rock.
{
"key": "prefab.rock.granite_cluster",
"name": "Granite cluster",
"rootLocalId": "boulder",
"entities": [
{ "localId": "boulder", "key": "rock.boulder", "name": "Boulder",
"siblingOrder": 0,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0, "z": 0 }, "rotation": { "x": 10, "y": 30, "z": 5 }, "scale": { "x": 1.8, "y": 1.3, "z": 1.6 } },
"render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.granite" } },
"identity": {}, "editor": {}, "runtime": {} },
{ "localId": "stone_a", "key": "rock.stone_a", "name": "Stone A",
"parentLocalId": "boulder", "siblingOrder": 0,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 1.1, "y": -0.3, "z": 0.4 }, "rotation": { "x": 25, "y": 70, "z": 15 }, "scale": { "x": 0.7, "y": 0.6, "z": 0.8 } },
"render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.granite" } },
"identity": {}, "editor": {}, "runtime": {} },
{ "localId": "stone_b", "key": "rock.stone_b", "name": "Stone B",
"parentLocalId": "boulder", "siblingOrder": 1,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": -0.9, "y": -0.4, "z": -0.6 }, "rotation": { "x": 5, "y": 200, "z": 40 }, "scale": { "x": 0.5, "y": 0.45, "z": 0.55 } },
"render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.granite" } },
"identity": {}, "editor": {}, "runtime": {} }
]
}
Worked example three a bench
A frame root with a seat slab, a backrest, and four legs. Boxes only; the value is the ARRANGEMENT. Parent every part to the frame so the whole bench places, rotates, and scales as one object.
{
"key": "prefab.bench.park",
"name": "Park bench",
"rootLocalId": "frame",
"entities": [
{ "localId": "frame", "key": "bench.frame", "name": "Frame",
"siblingOrder": 0,
"components": { "transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0, "z": 0 } } },
"identity": {}, "editor": {}, "runtime": {} },
{ "localId": "seat", "key": "bench.seat", "name": "Seat",
"parentLocalId": "frame", "siblingOrder": 0,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0.45, "z": 0 }, "scale": { "x": 1.8, "y": 0.08, "z": 0.5 } },
"render": { "type": "RenderableComponent", "shape": "box", "visible": true, "materialRef": "mat.wood" } },
"identity": {}, "editor": {}, "runtime": {} },
{ "localId": "back", "key": "bench.back", "name": "Backrest",
"parentLocalId": "frame", "siblingOrder": 1,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0, "y": 0.85, "z": -0.22 }, "scale": { "x": 1.8, "y": 0.5, "z": 0.06 } },
"render": { "type": "RenderableComponent", "shape": "box", "visible": true, "materialRef": "mat.wood" } },
"identity": {}, "editor": {}, "runtime": {} },
{ "localId": "leg_fl", "key": "bench.leg_fl", "name": "Leg front-left",
"parentLocalId": "frame", "siblingOrder": 2,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": -0.8, "y": 0.22, "z": 0.2 }, "scale": { "x": 0.08, "y": 0.45, "z": 0.08 } },
"render": { "type": "RenderableComponent", "shape": "box", "visible": true, "materialRef": "mat.metal" } },
"identity": {}, "editor": {}, "runtime": {} }
]
}
Add the remaining three legs the same way (leg_fr, leg_bl, leg_br) with mirrored x/z. A collider on frame (a single box ColliderComponent sized to the seat) is usually enough; you rarely need a collider per plank.
Nesting a prefab as a part
A part can BE an instance of another prefab. Give the entity a prefabInstance instead of authoring its geometry inline:
{
"localId": "canopy", "key": "tree.canopy", "name": "Canopy",
"parentLocalId": "trunk", "siblingOrder": 3,
"prefabInstance": {
"prefabKey": "prefab.foliage.leaf_cluster",
"overrides": [
{ "entityLocalId": "cluster_root", "componentKey": "render", "propertyPath": "materialRef", "op": "set", "value": "mat.autumn_leaves" }
]
},
"identity": {}, "editor": {}, "runtime": {}
}
Now the leaf cluster is authored ONCE as its own prefab and reused across every tree: the part carries a reference to the other prefab (by prefabId or prefabKey, with its own per-instance overrides), and the referenced prefab is resolved and its overrides validated when the parent prefab is materialized (the create path checks the nested reference and folds its overrides). Prefer this when a sub-assembly repeats: author the leaf cluster, the wheel, or the lamp head once, then reference it from every prop that uses it, rather than pasting its geometry into each parent.
Variants re-skinning one base prefab
To ship a family (oak / pine / dead tree) without re-authoring the trunk-and- branch structure, set basePrefabKey and a variantOverrides array. When the prefab resolves, its base lineage is walked and each variant's override layer is folded down onto the base entities.
Know exactly which override ops the shared effective-entity resolver ACTUALLY applies, so you author overrides that take effect rather than silently no-op. The resolver (applyPrefabOverrideToEntity in runtime-shared/prefab.ts) implements:
set(a component field atpropertyPath, needscomponentKey),component_add/component_remove(needscomponentKey),name_set,scripts_set,tag_add/tag_remove,runtime_set,hierarchy_reorder_child(re-parent or reorder an EXISTING part: set itsparentLocalIdor itssiblingOrder).
Example: recolor the crown and re-parent a branch in a variant.
{
"key": "prefab.tree.oak_autumn",
"name": "Autumn oak",
"basePrefabKey": "prefab.tree.oak_lowpoly",
"rootLocalId": "trunk",
"entities": [ /* the base body, cloned */ ],
"variantOverrides": [
{ "entityLocalId": "crown", "componentKey": "render", "propertyPath": "materialRef", "op": "set", "value": "mat.autumn_foliage" },
{ "entityLocalId": "leaves_r", "propertyPath": "siblingOrder", "op": "hierarchy_reorder_child", "value": 2 }
]
}
ADDING a part in a variant is NOT done with an override. The hierarchy_add_child and hierarchy_remove_child ops exist in the PrefabOverride op enum and pass validation, but the shared effective-entity resolver does NOT apply them (they fall through as no-ops); treat them as a recorded gap, not a working add-a-child mechanism. To add a NEW part that the base lacks, include it as a fresh entity in the VARIANT'S OWN entities array with a parentLocalId into the base structure. Any variant entity whose localId is not in the base is appended to the resolved set:
{
"key": "prefab.tree.oak_winter",
"name": "Winter oak",
"basePrefabKey": "prefab.tree.oak_lowpoly",
"rootLocalId": "trunk",
"entities": [
{ "localId": "snow_cap", "key": "tree.snow_cap", "name": "Snow cap",
"parentLocalId": "crown", "siblingOrder": 0,
"components": {
"transform": { "type": "TransformComponent", "position": { "x": 0, "y": 1.0, "z": 0 }, "scale": { "x": 2.3, "y": 0.4, "z": 2.3 } },
"render": { "type": "RenderableComponent", "shape": "sphere", "visible": true, "materialRef": "mat.snow" } },
"identity": {}, "editor": {}, "runtime": {} }
],
"variantOverrides": [
{ "entityLocalId": "crown", "componentKey": "render", "propertyPath": "materialRef", "op": "set", "value": "mat.snow_foliage" }
]
}
The variant keeps the shared structure and layers only the differences (a re-skin override plus the one new snow-cap entity), so fixing the base trunk fixes every variant.
Instance the finished prop, do not re-author it
Once the prop is a prefab, place copies with project_create_entity (project.entity.create) passing prefabId (a uuid or the prefab key). Each call drops one prefab instance; componentOverrides on the call re-pose or re-scale that instance (a shorter tree, a rotated bench) without touching the prefab. This is the correct way to fill a scene with the same prop:
- Author the tree prefab ONCE.
- Place fifteen trees with fifteen
project_create_entitycalls, each with a differentTransformComponentposition and a small scale/rotation override so the copies do not look stamped.
Do NOT paste the whole entities subtree fifteen times. Re-authoring the body per copy bloats the graph, loses the single-edit point, and is the exact waste the prefab exists to prevent. For the runtime detail of how a prefab instance resolves (it places the prefab root as one instance entity, not an expanded graph subtree) and for placing HUNDREDS of copies of one SIMPLE mesh via an instanceSet instead of per-entity, see asset-to-instances (steps 3a and 3b).
Compose from primitives, or request generation
Composing a prop from a primitive subtree and minting a generated mesh are different tools for different shapes. Choose by where the fidelity lives:
- COMPOSE from primitives when the object reads from its GEOMETRY and arrangement: benches, crates, fences, lamp posts, simple carts, blocky or low-poly trees and rocks, anything you would model by stacking boxes, spheres and capsules. It is instant, costs no credits, stays fully editable part by part, and every piece is a real authored entity you can move or script.
- WELD with CSG (see model-authoring-parametric-csg) when the parts must fuse into ONE continuous surface, for example a keyhole cut through a plate or a socket bored into a block. CSG produces a single mesh; a prefab subtree keeps the parts distinct. Use a prefab when the parts should read or move separately; use CSG when they are one solid.
- REQUEST GENERATION (see asset-acquisition-strategy) when the value is SURFACE detail primitives cannot express: bark and leaf cards on a hero tree, a detailed creature, cloth folds, ornate carving. Spend the credits on the few hero objects the player studies up close, and compose primitives for the background fill.
A good instinct: reach for composition first for the many mid- and background props (it is free and immediate), and escalate to generation only for the hero objects where a primitive silhouette would break the illusion.
Pitfalls
rootLocalIdMUST name a reallocalIdinentities, and everyparentLocalIdmust reference alocalIdthat exists in the same array. A dangling parent id is a broken subtree.- Child transforms are LOCAL to the parent. If a branch flies off into space, you posed it in world coordinates by habit; halve the numbers and think relative to the trunk.
- Do not give every plank its own collider. One box
ColliderComponentsized to the prop is cheaper and usually indistinguishable in play. - A variant that re-authors the whole body instead of using
variantOverridesloses the shared-edit point; keep the base body canonical and layer only diffs. - Instancing a prefab is one instance entity per call; there is no one-call bulk fan-out of a multi-entity prefab on the authoring surface (asset-to-instances names this explicitly). Issue N
project_create_entitycalls, or use an instanceSet for repeated simple GEOMETRY.