Skip to main content

Scenes and nodes

Nodes are entities​

A node is an ECS entity with Name, Parent, Children and GlobalTransform.

  • transform is a component like any other. A node without one sits at its parent.
  • Node paths ("A/B/C"), transform propagation and recursive free are core systems.
  • Plugins add their own components to the same entities.
  • Scripts reach the tree through 30 node operations: get_node, parent, children, add_child, set_parent, queue_free, global_position, translate, attach_script, and the component operations. set_parent keeps the world pose.
  • Paths accept ..: ../Target is a sibling. The full list is in the script modules reference.
  • queue_free and other structural changes apply at the end of the frame.

The node tree of examples/hello, with the Platform node's components in the inspector

Scene files​

A flat list of [[nodes]] entries with name, an optional parent by id, script, and one key per component.

[[nodes]]
id = "n_ball"
name = "Ball"
parent = "n_world"
script = { source = "scripts/ball.rn" }
body3d = { kind = "dynamic" }
collider3d = { kind = "sphere", radius = 0.5 }
shape3d = { kind = "sphere", radius = 0.5 }

[nodes.transform]
position = [0, 6, 0]
rotation_euler = [0, 1.5708, 0] # radians
scale = [1, 1, 1]
  • Node keys: id, name, parent, tags, script, instance, overrides, visible, tint, z_index and z_as_relative. Every other key is a component.
  • A component is written inline (shape3d = { ... }) or as its own table ([nodes.transform]).
  • Scene keys apply in plugin registration order.
  • At run time, scene::instantiate places a scene and node.add_child makes a node.

Prefabs​

A prefab is a scene file. instance on a node places one, and the file's roots become that node's children.

[[nodes]]
id = "n_crate_b"
name = "CrateB"
instance = "scenes/crate.toml"
transform = { position = [-0.5, 5, 1.5] }

[nodes.overrides."Crate/Lid".shape3d]
color = [0.9, 0.3, 0.25, 1]
  • The node keeps its own name, transform and components. Moving it moves the whole prefab.
  • overrides is keyed by path from the instance node and holds scene keys: components, transform, and a script whose props merge over the prefab's.
  • An override patches one property and leaves the rest of the component alone.
  • Ids inside an instance are prefixed by the instance's own (n_crate_b/n_lid).
  • A prefab that contains itself is an error naming the cycle.

In the editor:

  • A prefab is placed from the command palette, one command per scene file.
  • An instance's nodes appear in the tree one shade quieter.
  • Editing one writes an override. Setting it back to the prefab's value removes the override.
  • Adding or deleting a node inside an instance is refused, with the prefab offered to open instead.

Components​

Plugins declare named, schema-described components. A property's type is one of float, bool, string, enum, vec2, vec3, color or asset, with a default, enum options and a readonly marker. A malformed schema fails at boot naming the component and property.

One registration gives:

The scene-file keycollider3d = { kind = "sphere", radius = 0.5 }
The runtime APIEvery property as a field on the node's handle (node.collider3d.density = 15.0), plus node.set_component, patch_component, get_component, has_component, remove_component, component_names, scene::component_types() and scene::component_schema(name)
Editor supportThe Add component palette and the property inspectors, generated from the registry
ComponentPluginPurpose
transformcorePosition, rotation and scale in the parent's space
body3d, collider3dphysics3D rigid body and collider (rapier3d)
body2d, collider2dphysics2D rigid body and collider (rapier2d)
shape3d, shape2drender3D / 2D primitive with a color
sprite, tilemap, tile_collision, particlesrender, physicsTextured 2D quad; a tile grid over a tileset, autotiled and collided as one shape; a particle emitter
meshrenderAn OBJ or glTF mesh, optionally skinned to a skeleton
boolean3d, boolean2drenderThe node's children joined, cut out of one another, or overlapped
clonerrenderThe node's subtree drawn many times, one draw call per mesh
polygonrenderA textured 2D polygon, GPU-skinned to a rig
camerarenderThe 2D or 3D camera
bone2d, bone3dcoreA rest pose; a node with one is a bone
modifier2danimlook_at or two_bone_ik on a rig, run after the clip
animationanimA clip library (by reference or inline) and an autoplay clip
soundaudioA sound file with volume and looping
widgetuiHUD element (label / button / panel) anchored to the screen
  • No class hierarchy. A node is whatever its components make it.
  • A tagged-union property is always kind (kind = "sphere"). type always means the datatype.
  • The generated component reference lists every component with every property, default and option.

Objects​

Every shape the engine draws is a mesh, built without a GPU. A collider fits it, a ray picks it, and a headless test measures it.

Every object, one row per category

[[assets]]
id = "blade"
type = "mesh"
source = "models/blade.glb" # a model file...
kind = "torus" # ...or a primitive, with its own knobs...
kind = "text" # ...or a word, shaped from the project's fonts...
kind = "extrude" # ...or a path given thickness
positions = [[0, 0, 0]] # ...or the vertices themselves
  • 3D primitives: sphere, box, capsule, cylinder, cone, plane, torus, pyramid, prism, tube.
  • 2D primitives: circle, rectangle, capsule, ellipse, star, ngon.
  • size is the whole extent along each axis, as a collider's is. A capsule's height runs tip to tip.
  • shape3d and shape2d draw them. A mesh asset is for when a collider, a boolean or a script needs the triangles.

The primitives row

Paths​

[[assets]]
id = "arch"
type = "mesh"
kind = "extrude"
path = "#outline"
depth = 0.5
bevel = 0.08
KindWhat it makes
path2d, path3dA run of cubic control points with a closed flag
extrudeA closed outline given thickness, with an optional bevel
latheA profile revolved around y
sweepA cross-section carried along a rail

A shape2d polyline whose mesh names a path2d strokes it. Its join, cap, miter_limit and taper set how the stroke turns a corner, how it ends and how it narrows along its length.

An arch extruded, a goblet revolved, a rail swept

Booleans and cloners​

Union, difference and intersection of a cube and a ball

Ellipse, star, ngon and capsule; a rect and a circle joined, cut and intersected

  • boolean3d draws a node's children joined, cut out of one another, or only where they overlap: operation is union, difference or intersection, applied in declaration order. boolean2d is the same over filled outlines.
  • The operands stay in the tree, hidden and editable. Moving one recomputes the result.

A row, a ring and a scattered grid

  • cloner draws the node's subtree many times: linear with a count and a step, radial with a radius and an angle (zero closes the ring), or grid with counts.
  • seed and a random range scatter the copies.
  • Physics and scripts see one node. Each mesh is one draw call.

Vertex colours and morph targets​

type = "material"
shader = "shaders/painted.wesl"
features = { vertex_color = true }
  • A glTF's COLOR_0 and its blend shapes survive the import.
  • A morph weight is a component property, so a clip drives it as mesh/morph.<name>.
  • Vertex colours reach a shader through a material with vertex_color.

A word as a mesh, and a triangle coloured per vertex

examples/objects has all of it, one row per category.

Reacting without a script​

Three scene keys: [variables], states on a node, and bindings on a node.

[variables]
score = { type = "int", value = 0 }

[[nodes]]
name = "Door"

[nodes.states]
current = "shut"

[nodes.states.shut]
shape3d = { color = "#734f2b" }

[nodes.states.open]
shape3d = { color = "#4cbf66" }

[[nodes]]
name = "Ball"

[[nodes.bindings.rows]]
event = "pointer_click"
action = "add_variable"
target = "score"
value = 1

[[nodes.bindings.rows]]
event = "pointer_click"
when = "score >= 3"
action = "set_state"
target = "../Door"
value = "open"

The Bindings view: two binding rows on the ball, one counting the click and one opening the door when the score reaches three

examples/hello carries exactly this.

EventsPointer enter, exit, down, up, click, drag, drop; key down and up; a declared action; scroll; resize; a variable or a state changing; a collision starting or stopping. Each also reaches the node's script as on_<event>, bindings first
Actionsstate, set_variable, add_variable, play, sound, spawn, free, switch, open_url, emit, call, visible
whenA comparison over the scene's variables, joined with && and ||
StatesA table of component properties patched over the node's own. node.states.set_state("hover") does the same from a script, and on_state_changed(from, to) follows
  • The Bindings view writes these rows. Convert to script writes the Rune they mean, one function per hook.
  • A node may carry both. Bindings run first.
  • Pointer events need a window. Key, action, scroll and variable events fire headless.

Import settings​

art/hero.png.import.toml holds the import settings for art/hero.png.

# art/hero.png.import.toml
filter = "nearest" # pixel art, drawn crisp
srgb = true # false for a normal map or a mask

The same 32-pixel sprite at ten times its size, filtered linearly and with nearest

# project.toml: the default for every file of a kind
[import.texture]
filter = "nearest"
  • A sidecar overrides the project default key by key.
  • A file's kind comes from its extension: texture, audio, font, model.
  • recode is read by the exporter and overrides the project's export mode for one file.

The Import tab showing filter and srgb for a selected picture

  • Select a picture in the Assets dock. The Import tab shows one row per key and says whether the value is the file's own, the project's or the engine's default.
  • Clearing a row drops the key. A sidecar with nothing left is deleted.
  • Settings change pixels, never sizes. A sprite takes its extent from the image header. Downscaling art for a phone is an export key.