Skip to content

Latest commit

Β 

History

History
733 lines (593 loc) Β· 15 KB

File metadata and controls

733 lines (593 loc) Β· 15 KB

Node Collections

Page Map

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

Purpose

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.

Use Cases

  • Spawn a wave by instancing an authored enemy .scn prefab per member.
  • Fire an authored projectile or one-shot-effect .scn prefab 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 .scn root.
  • Build runtime-only debug or tool overlays: parent the collection under NodeID::nil() to keep it a root, or under ctx.id to scope it to the caller.

Ownership And Choice

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.

Shape

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 .scn scene.
  • 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.

Create

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.

Flat Batch

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,
];

Tree

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

Multiple Trees

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

Child Collections

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

Top-Level Collections

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

Scene Refs

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

Scene Children

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

Scene And Collections

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

Multi Scene Tree

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

Cross Domain

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

Real Node Data

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

Return Order

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.