| 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 a runtime-only batch: describe nodes as Rust data with
node_collection! and spawn them live with create_nodes!. Use it for content
whose count or shape exists only at runtime, such as generated UI rows, debug
overlays, test fixtures, and transient leaf nodes. For projectiles, waves,
enemies, and other gameplay objects, instance an authored .scn prefab and
vary only the runtime count/placement. Keep reusable composition, fixed node
trees, editor wiring, and per-placement values in .scn files. Follow the
scene templates and scene docs
for authored composition, then load or instance that scene from script.
Do not use NodeCollection as a code replacement for an authored scene. If a
tree is known before play, put it in a .scn file so designers and tools can
inspect, override, reuse, and compose it. A collection may instance that .scn
at runtime and add truly dynamic children around it.
- Spawn a wave by instancing an authored enemy
.scnprefab per member. - Fire an authored projectile or one-shot-effect
.scnprefab per shot. - Generate UI rows on the fly when player/item count is unknown in the scene.
- Add transient leaf markers or test fixtures around an authored
.scnroot. - 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 .scn when they need
editor-visible wiring or composition. Store the collection ID in the owning
script state; pass member IDs out only when another system needs a stable target.
The nested forms below document the low-level API for generated/tooling data.
Do not use them to compose a reusable gameplay tree; put that topology in a
.scn and instance it.
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.