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

TypeScript
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.

TypeScript
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)
Note

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.

Note

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.

TypeScript
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 ones

duration / 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.

TypeScript
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()
Note

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).

Note

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.

TypeScript
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): this

setParent 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).

Note

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.

TypeScript
node.worldPosition                    // Vec2 — composes all ancestors
node.worldToLocal(p: Vec2Like): Vec2  // world point → this node's local space
node.localToWorld(p: Vec2Like): Vec2  // and back

Move 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.

Note

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

TypeScript
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

TypeScript
node.destroy(): void

Frees 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

TypeScript
// ✗ 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