| Header | Link |
|---|---|
| Purpose | Purpose |
| Use Cases | Use Cases |
| Shape | Shape |
| Create | Create |
| Flat Batch | Flat Batch |
| Tree | Tree |
| Multiple Trees | Multiple Trees |
| Child Collections | Child Collections |
| Top-Level Collections | Top-Level Collections |
| Scene Refs | Scene Refs |
| Scene Children | Scene Children |
| Scene And Collections | Scene And Collections |
| Multi Scene Tree | Multi Scene Tree |
| Cross Domain | Cross Domain |
| Real Node Data | Real Node Data |
| Return Order | Return Order |
A NodeCollection is an in-code scene tree: you describe nodes as Rust data with node_collection! and spawn them live with create_nodes!. It is how a script builds things that were never placed in a .scn file β enemy waves, projectiles, generated UI, spawned prefabs, and debug overlays. Collections nest, splice into one another, and can embed .scn scenes, so a single spawn call can create a whole subtree. Use .scn files for editor-authored scenes and node collections for anything a script generates.
- Spawn an enemy wave or a burst of pickups at runtime: build a flat
node_collection![ ... ]and instantiate it withcreate_nodes!(ctx.run, wave, parent_id). - Fire a projectile or one-shot effect as a small prefab: a
node_collection!subtree with ascript = ...entry, spawned on each shot. - Generate UI on the fly (a scoreboard row per player, an inventory grid): a nested
node_collection!ofUi*nodes parented under a panel. - Compose a spawn from an authored scene plus extra nodes: mix
scene = "res://..."entries andcollection = ...splices inside one collection. - Build runtime-only debug or tool overlays: parent the collection under
NodeID::nil()to keep it a root, or underctx.idto scope it to the caller.
A collection owns a batch of nodes and optional tree relationships outside the main scene tree. Use it when one system creates, updates, and removes many similar runtime nodes. Keep ordinary authored nodes in a scene when they need editor-visible individual wiring. Store the collection ID in the owning script state; pass member IDs out only when another system truly needs a stable target.
Each entry uses plain fields:
{
name = "node_name",
tags = tags!["tag_a", "tag_b"],
node = Node2D::new(),
children = [
{ name = "child", node = Node2D::new() },
],
}Fields:
node = ...required.scene = ...loads a.scnscene.name = ...optional.tags = ...optional.script = ...optional single script resource.children = [...]optional.collection = ...splices another collection.
node = ... is real Rust node data.
Use a type name for default node data:
{ name = "button", node = UiButton }Use a type body to set only the fields you care about:
{
name = "title",
node = UiLabel {
text: {"Paused".into()},
font_size: 32.0,
},
}Missing fields use Default::default().
Nested type bodies work the same way:
{
name = "actor",
node = Node2D {
transform: Transform2D {
position: Vector2::new(5.0, 7.0),
},
},
}Use { expr } for arbitrary Rust expressions.
Use node = { expr } to keep the old escape hatch for full custom values.
scene = ... uses any scene path value accepted by the scene API.
name and tags are scene graph metadata.
Use scene object form to patch the loaded scene root:
{
name = "player",
scene = {
path = res_path!("res://scenes/player.scn"),
patch = Node2D {
transform: Transform2D {
position: Vector2::new(10.0, 0.0),
},
},
},
script = res_path!("res://scripts/player.rs"),
}patch applies only if the loaded scene root type matches.
Use patch lists when a root needs more than one typed patch:
scene = {
path = res_path!("res://scenes/player.scn"),
patch = [
Node2D {
transform: spawn_xform,
},
],
}Use script config form to inject vars before script init:
{
node = Node2D,
script = {
path = res_path!("res://scripts/player.rs"),
vars = {
hp: 100_i32,
title: {"Player".to_string()},
},
},
}script still means one script per node.
Use @key in script vars to pass a spawned node id:
node_collection![
player: { node = Node2D },
camera: {
node = Camera2D,
script = {
path = res_path!("res://scripts/follow.rs"),
vars = { target: @player },
},
},
]@key vars resolve during spawn.
Keys are local macro labels.
Names are runtime strings.
Use create_nodes!.
let nodes = node_collection! {
{
name = "root",
node = Node2D::new(),
children = [
{ name = "sprite", node = Sprite2D::new() },
],
}
};
let ids = create_nodes!(ctx.run, nodes, ctx.id);
let root = ids[0];Result:
ctx.id
root
sprite
IDs return in preorder.
Top-level collection nodes become children of the parent passed to create_nodes!.
If parent is NodeID::nil(), top-level nodes stay roots.
Use array form when no nesting is needed.
let wave = node_collection![
{ name = "enemy_a", tags = tags!["enemy"], node = Node2D::new() },
{ name = "enemy_b", tags = tags!["enemy"], node = Node2D::new() },
{ name = "enemy_c", tags = tags!["enemy"], node = Node2D::new() },
];
let ids = create_nodes!(ctx.run, wave, ctx.id);Result:
ctx.id
enemy_a
enemy_b
enemy_c
Use keyed entries when a flat list needs parent refs.
let actor = node_collection![
root: { node = Node2D },
sprite: { parent = @root, node = Sprite2D },
camera: { parent = @root, node = Camera2D },
];Keys are compile-time macro refs.
If name is omitted, keyed entries use the key text as node name.
Use name = ... to override it.
parent = @key is only for flat entries.
Inside children = [...], parent is implicit.
Keys inside children are allowed as name shorthand, but cannot be referenced.
Use root = @key when a collection splice should return a non-first root:
let actor = node_collection![
shell: { node = Node2D },
body: { node = Node2D },
root = @body,
];Use object form for one tree.
let actor = node_collection! {
{
name = "actor",
tags = tags!["player"],
node = Node2D::new(),
children = [
{ name = "sprite", node = Sprite2D::new() },
{ name = "camera", node = Camera2D::new() },
],
}
};
let ids = create_nodes!(ctx.run, actor, ctx.id);Result:
ctx.id
actor
sprite
camera
Use array form with nested entries.
let pack = node_collection![
{
name = "hud",
node = UiPanel::new(),
children = [
{ name = "score", node = UiLabel::new() },
],
},
{
name = "actor",
node = Node2D::new(),
children = [
{ name = "sprite", node = Sprite2D::new() },
],
},
{ name = "camera_anchor", node = Node2D::new() },
];
let ids = create_nodes!(ctx.run, pack, ctx.id);Result:
ctx.id
hud
score
actor
sprite
camera_anchor
Use collection = expr to splice reusable parts.
fn toolbar() -> NodeCollection {
node_collection![
{ name = "inventory", node = UiButton::new() },
{ name = "map", node = UiButton::new() },
]
}
fn hud() -> NodeCollection {
node_collection! {
{
name = "hud",
node = UiPanel::new(),
children = [
{ collection = toolbar() },
{
name = "hp",
node = UiLabel {
text: "HP".into(),
..UiLabel::new()
},
},
],
}
}
}
let ids = create_nodes!(ctx.run, hud(), ctx.id);Result:
ctx.id
hud
inventory
map
hp
Collections can be spliced at the top level too.
let scene_bits = node_collection![
{ collection = hud() },
{ collection = actor_debug() },
{ name = "marker", node = Node2D::new() },
];
let ids = create_nodes!(ctx.run, scene_bits, ctx.id);Result:
ctx.id
hud
...
actor
...
marker
Use scene = ... to splice a .scn scene into a collection.
let pack = node_collection![
{
name = "player",
tags = tags!["actor"],
scene = res_path!("res://scenes/player.scn"),
},
{
name = "hud",
scene = "res://ui/hud.scn",
},
];
let ids = create_nodes!(ctx.run, pack, ctx.id);Result:
ctx.id
player
...scene nodes
hud
...scene nodes
Scenes can have code children.
let actor = node_collection! {
{
name = "ship",
scene = res_path!("res://scenes/ship.scn"),
children = [
{ name = "debug_anchor", node = Node3D::new() },
{
name = "nameplate",
node = UiLabel {
text: "Ship".into(),
..UiLabel::new()
},
},
],
}
};
let ids = create_nodes!(ctx.run, actor, ctx.id);Result:
ctx.id
ship
...scene nodes
debug_anchor
nameplate
Scenes can contain collection children.
Collections can contain scene children.
fn loadout_ui() -> NodeCollection {
node_collection![
{ name = "weapon_slot", node = UiButton::new() },
{ name = "item_slot", node = UiButton::new() },
]
}
let squad = node_collection![
{
name = "leader",
scene = res_path!("res://scenes/player.scn"),
children = [
{ collection = loadout_ui() },
],
},
{
name = "followers",
node = Node2D::new(),
children = [
{ scene = res_path!("res://scenes/follower.scn") },
{ scene = res_path!("res://scenes/follower.scn") },
],
},
];
let ids = create_nodes!(ctx.run, squad, ctx.id);Result:
ctx.id
leader
...player scene nodes
weapon_slot
item_slot
followers
...follower scene nodes
...follower scene nodes
Use many scene refs, nested scenes, and code children in one collection.
let multi = node_collection![
{
name = "ship_a",
tags = tags!["ship", "player"],
scene = res_path!("res://scenes/ship.scn"),
children = [
{
name = "ship_a_debug",
node = Node3D::new(),
children = [
{
name = "ship_a_nested_hud",
scene = res_path!("res://ui/ship_hud.scn"),
children = [
{ name = "fps_label", node = UiLabel::new() },
],
},
],
},
],
},
{
name = "squad_root",
node = Node2D::new(),
children = [
{
name = "ship_b",
scene = res_path!("res://scenes/ship.scn"),
},
{
name = "ship_c",
scene = res_path!("res://scenes/ship.scn"),
children = [
{ name = "marker", node = Sprite2D::new() },
],
},
],
},
];
let ids = create_nodes!(ctx.run, multi, ctx.id);ID order:
ids[0] ship_a
ids[1] ship_a_debug
ids[2] ship_a_nested_hud
ids[3] fps_label
ids[4] squad_root
ids[5] ship_b
ids[6] ship_c
ids[7] marker
Live tree:
ctx.id
ship_a
...ship.scn nodes
ship_a_debug
ship_a_nested_hud
...ship_hud.scn nodes
fps_label
squad_root
ship_b
...ship.scn nodes
ship_c
...ship.scn nodes
marker
2D, 3D, and UI nodes can live in one collection.
Parenting is graph-level.
let mixed = node_collection! {
{
name = "node_2d_root",
node = Node2D::new(),
children = [
{
name = "node_3d_child",
node = Node3D::new(),
children = [
{
name = "ui_under_3d",
node = UiPanel::new(),
children = [
{ name = "node_2d_under_ui", node = Node2D::new() },
],
},
],
},
{
name = "ui_sibling",
node = UiLabel {
text: "Mixed".into(),
..UiLabel::new()
},
},
],
}
};
let ids = create_nodes!(ctx.run, mixed, ctx.id);Result:
ctx.id
node_2d_root
node_3d_child
ui_under_3d
node_2d_under_ui
ui_sibling
Use struct update syntax when setting fields.
let menu = node_collection! {
{
name = "pause_menu",
tags = tags!["ui", "menu"],
node = UiPanel {
base: UiNode {
clip_children: true,
..UiNode::new()
},
..UiPanel::new()
},
children = [
{
name = "title",
node = UiLabel {
text: "Paused".into(),
font_size: 32.0,
..UiLabel::new()
},
},
{ name = "resume", node = UiButton::new() },
{ name = "quit", node = UiButton::new() },
],
}
};
let ids = create_nodes!(ctx.run, menu, ctx.id);Result:
ctx.id
pause_menu
title
resume
quit
IDs return in preorder.
0 root
1 first child
2 first grandchild
3 next child
For one-root collections:
let root = ids[0];For multi-root collections:
let roots = [ids[0], ids[3], ids[7]];Keep indices local to the collection shape.
Use names/tags to find nodes when tree shape can change.