LeCodesdocs

Node2D

The base 2D scene node — an empty transform. Sprite and Tilemap extend it, so everything here (transform, hierarchy, events, aspects) works on them too. A bare Node2D is useful as a grouping parent, a world-space marker/spawn point, or a physics/trigger object (attach a Shape2D plus Physics2D or Trigger2D).

At a glance

TypeScript
const door = new Node2D()
  .aspect(Shape2D, { box: [16, 32] })
  .aspect(Trigger2D)
door.position = [320, 64]
door.addEventListener('enter', other => console.log('entered by', other.id))
scene.add(door)

const cart = new Node2D()
cart.add(wheelA, wheelB)        // children move with the cart
cart.x += 40

Transform

Local to the parent, Y-up; 1 unit = 1 logical px at camera zoom 1. 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 — draw scale only
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
Note

position and scale getters return fresh copiesnode.position.x = 3 is a silent no-op. Use node.x = 3, or mutate a local and assign back. See math value semantics.

Note

scale affects drawing only — physics shapes never read it. A scaled-up sprite keeps the collider you declared. See 2D physics.

Hierarchy

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

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

Physics-driven transforms

Once a node has a dynamic or kinematic Physics2D body, the native physics 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 the body with velocity / applyImpulse (kinematic: moveTo).

Events

TypeScript
node.addEventListener(channel, callback)
node.removeEventListener(channel, callback)
Channel Fires when Callback argument
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
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 or Trigger2D. See pointer events & gestures and 2D physics picking.

Destroying

TypeScript
node.destroy(): void

Frees the native entity and detaches from the parent. Children are reparented to the root, keeping their world transforms — they are not destroyed with the parent. Using a destroyed node afterwards is undefined behavior.

Pitfalls

TypeScript
// ✗ mutating a transform copy
node.position.x = 3
// ✓ single-axis setter, or assign a whole value
node.x = 3

// ✗ fighting the physics step on a dynamic body
setLoop(() => { node.y += 2 })
// ✓ drive the body instead
node.physics.velocity = [0, 120]

See also