Node2D
The base 2D scene node — a transform that draws nothing. Sprite (an image) and
Tilemap extend it, so everything here works on them too. A bare Node2D is a
grouping parent, a marker or spawn point, or an invisible physics / trigger object.
- move, rotate or scale a sprite
- draw a node in front of or behind others, or hide it
- group sprites so they move together (parent and children)
- convert a point between a node's local space and the world
- tween a node to a new position, rotation or scale
- move a node that has a physics body without fighting the physics
- react to taps, collisions and finished animations on a node
- remove a node and its children for good
Platforms: iOS, Android, desktop, the web player, the le.codes viewer app; not on the web (Web
Lite, lecodes web), where new Node2D() throws. Headless tests move nodes but draw nothing.
At a glance
async function main() {
const scene = new Scene2D()
const wheel = await Texture2D.load(asset('./wheel.png'))
const door = new Node2D()
.aspect(Shape2D, { box: [16, 32] })
.aspect(Trigger2D)
door.position = [320, 64]
door.addEventListener('enter', other => console.log('entered by', other.name))
const cart = new Node2D()
const wheelA = new Sprite({ texture: wheel, position: [-10, 0] })
const wheelB = new Sprite({ texture: wheel, position: [10, 0] })
cart.add(wheelA, wheelB) // children move with the cart
cart.x += 40
scene.add(door, cart, wheelA, wheelB) // the draw set is per node — children too
scene.open()
}
main()Move, rotate and scale a node: position, rotation, scale, layer
Position, rotation and scale are local to the parent, Y-up; 1 unit = 1 logical px at camera
zoom 1. layer / z set the draw order, visible hides the node. See
conventions.
node.x, node.y // single-axis setters
node.position = [120, 64] // Vec2Like; getter returns a fresh Vec2
node.rotation = 45 // degrees, CCW
node.scale = 2 // number (uniform) or Vec2Like
node.layer = 1 // draw layer (int); higher layers render on top
node.z = 5 // intra-layer depth on non-Y-sorted layers (higher = on top)
node.visible = false
node.name = 'door' // optional plain-JS label (scene files stamp their node keys here)
node.id // native entity id (readonly; 0 once destroyed)position and scale getters return fresh copies. The direct spelling
node.position.x = 3 is compiled to node.x = 3 and works; a copy you stored first is still a copy
(const p = node.position; p.x = 3 is a silent no-op). See
math value semantics.
an explicit Shape2D (box, circle, …) ignores scale — a scaled-up sprite keeps
the collider you declared. Only the auto shape Shape2D {} measures size × scale, once, when
the collider is built. See 2D physics.
Tween a sprite or node to a new position, rotation or scale: animateTo {#animating-the-transform}
Slide, spin or grow a sprite (or any node) over time: animateTo tweens from the current values,
animateFrom from the given ones back to the current ones.
node.animateTo({ x?, y?, position?, rotation?, scale?, duration?, delay?, easing?, loop?, clock? }): Animation
node.animateFrom({ ...the same }): Animation // from the given values to the current onesduration / delay are milliseconds (default 225 ms, ease-in-out); rotation is degrees; an
array of values is a keyframe list (for position / scale, an array of vectors). It returns the
Animation handle (pause, resume, cancel,
finished, …); a Sprite adds opacity and color — see
what each target animates.
async function main() {
const scene = new Scene2D()
const tex = await Texture2D.load(asset('./coin.png'))
const coin = new Sprite({ texture: tex, position: [0, -100] })
scene.add(coin)
scene.open()
// rises to the center while spinning once; await .finished to chain the next step
await coin.animateTo({ y: 0, rotation: 360, duration: 600, easing: 'outBack' }).finished
coin.animateTo({ scale: 1.5, duration: 200 })
}
main()node tweens default to the 'game' clock, but that clock follows Time.scale /
Time.paused only once a 3D Scene exists — in a 2D-only app a node tween keeps running while
the game is paused (see clocks).
a tween writes the drawn transform only — it never reaches a physics body or a
character. A tweened node with a body leaves its collider behind; place bodies with position.
Group sprites so they move together: parent and children, add / setParent {#hierarchy}
Put sprites under a parent node and they move, turn and scale with it: a child's transform is relative to its parent.
node.parent // Node2D | null (settable — sugar for setParent)
node.children // readonly Node2D[]
node.add(...children: Node2D[]): this // attach (each keeps its world transform)
node.remove(child: Node2D): this // detach a direct child → root, keeping world transform
node.setParent(parent: Node2D | null, keepWorld = true): thissetParent with keepWorld: true (the default) recomputes the local transform so nothing moves
on screen; pass false to keep the local values and snap to the new parent's frame. Self and
descendant parents are rejected (no cycles).
parenting does not add a child to the scene: a child is drawn once it is
in the draw set itself. And parent reads null while the parent is a loose
root (not yet in a scene or under another node).
A child's place in the world: worldPosition, worldToLocal, localToWorld
A child's position is relative to its parent; these give its place in the world and convert a
point between the world and the node's own space. A screen point (a tap) goes through the camera:
screenToWorld.
node.worldPosition // Vec2 — composes all ancestors
node.worldToLocal(p: Vec2Like): Vec2 // world point → this node's local space
node.localToWorld(p: Vec2Like): Vec2 // and backMove a node that has a physics body
Once a node has a dynamic or kinematic Physics2D body — or a
CharacterController2D — the native step owns its transform.
Reading node.position / worldPosition is always correct: the node re-syncs from native
automatically. Do not write node.position per frame; drive a dynamic body with velocity /
applyImpulse and a character with move().
A position or rotation write is routed to whatever owns the node — a body, a sensor or a
character — so it teleports the collider with it. That is how a kinematic body is placed, and it is
what replaced the old physics.moveTo / trigger.moveTo.
only a DIRECT write is routed. A body or sensor parented to a node that moves does not follow it — keep them on root nodes and write their own transform.
Taps, collisions and clip events on a node: addEventListener
node.addEventListener(channel, callback)
node.removeEventListener(channel, callback)| Channel | Fires when | Callback arguments |
|---|---|---|
click |
pointer-up over this node's physics shape | ClickEvent |
touchstart |
pointer-down on its shape — ev.track() for drags |
TouchStartEvent |
enter / exit |
a physics contact / sensor overlap began / ended | the other Node2D, a contact { point, normal, speed } |
loopReached |
a looping SpriteAnimation clip wrapped around |
clip name (string) |
completed |
a non-looping clip finished | clip name (string) |
click / touchstart require the node to be hittable: a Shape2D plus a Physics2D,
Trigger2D or CharacterController2D. See pointer events & gestures and
2D physics picking; the contact argument is described under
enter / exit events.
Remove a node and its children for good: destroy
node.destroy(): voidFrees the native entity and its whole subtree — children go with the parent, as in the 3D and
UI trees (detach a child first with setParent(null) to keep it). Every aspect in the subtree
detaches first, so bodies are released. A node you never added to a scene and stop referencing is
freed by itself. Afterwards node.id reads 0 and writes no longer reach the engine.
Pitfalls
// ✗ mutating a STORED transform copy
const p = node.position; p.x = 3
// ✓ single-axis setter, or assign a whole value (node.position.x = 3 compiles to node.x = 3)
node.x = 3
// ✗ fighting the physics step — a dynamic body's transform written every frame
crate.y += 2 // in an update(dt)
// ✓ drive the body instead
crate.physics.velocity = [0, 120]See also
- Scene2D — adding nodes, layers, Y-sort
- Sprite — the textured node
- 2D physics —
Shape2D, bodies, triggers,enter/exit - Pointer events & gestures —
click,touchstart, drags - Conventions — units, value semantics, lifecycle