Physics API

Rigid-body physics for any node on the canvas.
title: "Bouncing ball"
description: "A dynamic ball falls under gravity and bounces off a static floor"
nodes:
  - type: ELLIPSE, name: "Ball", x: 200, y: 80, w: 50, h: 50, fill: "#E74C3C"
  - type: RECT, name: "Floor", x: 100, y: 350, w: 300, h: 20, fill: "#2C3E50"
modules:
  - node: "Ball", module: "physics", config: { bodyType: "dynamic", restitution: 0.7 }
  - node: "Floor", module: "physics", config: { bodyType: "static" }
tools_visible: [select, play]
ui_visible: [properties, modules]

Adding physics

Open the Properties panel for any node, click Add Module, and choose Physics. The node gets a rigid body, a collider shape auto-detected from its geometry, and it starts simulating when you press Play.

Body types

TypeBehaviorTypical use
dynamicAffected by gravity and forces. Collides with everything.Player, projectile, crate
staticNever moves. Infinite mass.Floor, wall, platform
kinematicMoves only through code (formulas). Collides with dynamic bodies but is not pushed by them.Moving platform, elevator, door

Set the body type in the Properties panel or from a formula:

this.physics.bodyType = "kinematic"

Body properties

PropertyTypeDefaultDescription
densitynumber1.0Mass per unit area. Higher = heavier.
frictionratio 0-10.3Surface friction. 0 = ice, 1 = rubber.
restitutionratio 0-10Bounciness. 0 = no bounce, 1 = perfectly elastic.
gravityScalenumber1Multiplier on gravity for this body. 0 = floats. Negative = falls upward.
fixedRotationbooleanfalsePrevents the body from rotating.
fixXbooleanfalseLocks horizontal position. Body can only move vertically.
fixYbooleanfalseLocks vertical position. Body can only move horizontally.

Colliders

A collider defines the physical shape used for collision detection. It does not have to match the visual shape exactly.

Auto colliders

When collider type is set to auto, the shape is inferred from the node type:

Node typeCollider shapeNotes
RECT, FRAMEBoxAxis-aligned rectangle
ELLIPSECircleTrue circle if width equals height, otherwise 8-vertex polygon approximation
POLYGONPolygonConvex hull, max 8 vertices
STARPolygon / Box fallbackConvex hull of the outline, falls back to bounding box
VECTORConvex hullSamples the path, computes convex hull, simplified to max 8 vertices

Multi-collider

A single node can have multiple colliders. Each collider is a ColliderEntry:

FieldTypeDefaultDescription
namestring"Collider"Identifier. Shows up in collision events.
typeenum"auto"auto, circle, box, or capsule
scalenumber (%)100Percentage scale of the collider shape
offsetXnumber (px)0Horizontal offset from node center
offsetYnumber (px)0Vertical offset from node center

Add colliders in the Properties panel under the Physics module section. Each entry gets a name like "Collider", "Collider2", etc.

Coordinate conversion

The engine uses PIXELS_PER_METER = 50. All positions on the canvas are in pixels; the physics engine works in meters internally. This means a 100px wide box is 2 meters in the physics world.

You never need to convert manually -- the API accepts and returns pixel values. But if you are debugging raw physics values or doing advanced math, know that:

meters = pixels / 50
pixels = meters * 50

Formula API

When a node has a physics module, you get a this.physics proxy in formulas. All values are in pixels and degrees -- no unit conversion needed.

Velocity

// Read current velocity (pixels per second)
let vx = this.physics.velocity.x
let vy = this.physics.velocity.y

// Set velocity directly
this.physics.velocity.x = 200   // move right at 200 px/s
this.physics.velocity.y = -300  // move up at 300 px/s

// Set both components at once
this.physics.velocity = { x: 100, y: -50 }

Angular velocity

// Read (radians per second)
let spin = this.physics.angularVelocity

// Set
this.physics.angularVelocity = 3.14  // ~half turn per second

Forces and impulses

// Apply a continuous force (good for engines, thrusters)
// Call every frame -- force accumulates over the timestep
this.physics.applyForce(fx, fy)

// Apply an instant impulse (good for jumps, explosions)
// Call once -- immediately changes velocity
this.physics.applyImpulse(ix, iy)

Force vs impulse: A force is like pushing a shopping cart -- steady pressure over time. An impulse is like hitting a baseball -- one instant burst of energy.

Sensor mode

// Enable sensor -- detects overlaps but does not block movement
this.physics.sensor = true

// Disable sensor -- normal solid collisions
this.physics.sensor = false

When sensor is true, the node still fires collide events but does not physically push or block other bodies. Use this for trigger zones, pickups, or damage areas.

Collision bypass

// Disable collisions between this node and another specific node
this.physics.disableCollisions(otherNode)

// Re-enable them later
this.physics.enableCollisions(otherNode)
title: "Platformer character"
description: "Arrow keys move a kinematic platform while a ball bounces on it"
nodes:
  - type: RECT, name: "Platform", x: 150, y: 300, w: 150, h: 15, fill: "#3498DB"
  - type: ELLIPSE, name: "Ball", x: 200, y: 50, w: 40, h: 40, fill: "#E74C3C"
  - type: RECT, name: "Wall L", x: 20, y: 100, w: 15, h: 300, fill: "#2C3E50"
  - type: RECT, name: "Wall R", x: 380, y: 100, w: 15, h: 300, fill: "#2C3E50"
modules:
  - node: "Platform", module: "physics", config: { bodyType: "kinematic" }
  - node: "Ball", module: "physics", config: { bodyType: "dynamic", restitution: 0.5 }
  - node: "Wall L", module: "physics", config: { bodyType: "static" }
  - node: "Wall R", module: "physics", config: { bodyType: "static" }
formulas:
  - node: "Platform", event: "frame", value: |
      if (keys.ArrowLeft) this.physics.velocity.x = -200
      else if (keys.ArrowRight) this.physics.velocity.x = 200
      else this.physics.velocity.x = 0
tools_visible: [select, play]
ui_visible: [properties, modules, formulas]

Joints

Joints connect two physics bodies with a constraint. Create them through the formula API.

Revolute joint

A hinge. Bodies rotate around a shared anchor point.

let joint = this.physics.addJoint("revolute", otherNode, {
  anchorX: 0,          // anchor relative to this node's center (px)
  anchorY: 0,
  lowerAngle: -1.57,   // optional angle limits (radians)
  upperAngle: 1.57,
  enableLimit: true,
  enableMotor: true,    // optional motor
  motorSpeed: 3.0,      // target angular velocity (rad/s)
  maxMotorTorque: 100,
})

Prismatic joint

A slider. Bodies move along a fixed axis.

let joint = this.physics.addJoint("prismatic", otherNode, {
  anchorX: 0,
  anchorY: 0,
  axisX: 1,             // slide direction (normalized)
  axisY: 0,
  lowerTranslation: -100,  // limits in pixels
  upperTranslation: 100,
  enableLimit: true,
  enableMotor: true,
  motorSpeed: 50,        // target speed (px/s)
  maxMotorForce: 200,
})

Distance joint

Keeps two bodies at a fixed distance apart.

let joint = this.physics.addJoint("distance", otherNode, {
  anchorAX: 0, anchorAY: 0,   // anchor on this node (px)
  anchorBX: 0, anchorBY: 0,   // anchor on other node (px)
  length: 100,                 // rest length (px)
  stiffness: 4.0,              // spring stiffness (Hz)
  damping: 0.5,                // damping ratio 0-1
})

Weld joint

Glues two bodies together rigidly.

let joint = this.physics.addJoint("weld", otherNode, {
  anchorX: 0,
  anchorY: 0,
})

Rope joint

Maximum distance constraint -- bodies can get closer but not farther.

let joint = this.physics.addJoint("rope", otherNode, {
  anchorAX: 0, anchorAY: -20,
  anchorBX: 0, anchorBY: 20,
  maxLength: 150,        // maximum distance (px)
})

Spring joint

Like a distance joint but explicitly spring-like. Bodies oscillate around the rest length.

let joint = this.physics.addJoint("spring", otherNode, {
  anchorAX: 0, anchorAY: 0,
  anchorBX: 0, anchorBY: 0,
  restLength: 80,
  stiffness: 2.0,        // Hz
  damping: 0.3,           // ratio 0-1
})

Motor properties

Revolute and prismatic joints support motors:

PropertyTypeDescription
targetVelocitynumberDesired speed (rad/s for revolute, px/s for prismatic)
targetPositionnumberDesired angle or translation
maxForcenumberMaximum force/torque the motor can apply

Gravity attractors

A gravity attractor pulls (or pushes) dynamic bodies toward a point.

this.physics.addAttractor({
  x: 200,           // world position (px)
  y: 200,
  strength: 500,    // positive = attract, negative = repel
})

The force follows an inverse-square law: it gets stronger the closer a body is to the attractor. A negative strength pushes bodies away.

Raycast

Cast a ray through the physics world to find the first body it hits.

let hit = this.physics.raycast(
  originX, originY,     // starting point (px)
  dirX, dirY,           // direction (does not need to be normalized)
  maxDist                // maximum distance (px)
)

if (hit) {
  hit.node      // the VNode that was hit
  hit.point     // { x, y } impact point in world pixels
  hit.normal    // { x, y } surface normal at impact
}

Raycasts ignore sensor bodies by default. Use them for line-of-sight checks, laser beams, or ground detection.

title: "Raycast ground check"
description: "A character uses a downward raycast to detect when it is standing on the floor"
nodes:
  - type: RECT, name: "Player", x: 180, y: 200, w: 40, h: 60, fill: "#9B59B6"
  - type: RECT, name: "Ground", x: 50, y: 320, w: 300, h: 20, fill: "#2C3E50"
modules:
  - node: "Player", module: "physics", config: { bodyType: "dynamic", fixedRotation: true }
  - node: "Ground", module: "physics", config: { bodyType: "static" }
formulas:
  - node: "Player", event: "frame", value: |
      let hit = this.physics.raycast(this.x + 20, this.y + 60, 0, 1, 10)
      let grounded = hit !== null
      if (grounded && keys.Space) {
        this.physics.applyImpulse(0, -400)
      }
tools_visible: [select, play]
ui_visible: [properties, modules, formulas]

Simulation details

Fixed timestep

The physics engine runs at a fixed 1/60s (60 Hz) timestep using an accumulator. The game loop feeds real elapsed time into the accumulator, and the engine steps in fixed increments until the accumulator is drained. This keeps the simulation stable regardless of frame rate.

Nested physics

Nodes inside frames or groups work correctly. The engine resolves parent transforms so a dynamic body inside a moving frame gets the right world position. If the parent moves, children track it. If a dynamic child is simulated, its local coordinates are written back relative to the parent.

Play / stop

When you press Play, the engine captures a full snapshot of every physics node's state (position, rotation, velocity, module config). When you press Stop, the snapshot is restored -- the scene goes back to exactly how it was before you hit Play. You can experiment freely without breaking your layout.

State during play

During play mode, the physics step runs once per frame in this order:

  1. Sync from nodes -- push node positions into the physics world (handles kinematic movement, teleports, parent changes)
  2. Step -- advance the simulation by the accumulated time
  3. Sync to nodes -- write dynamic body positions back to nodes
  4. Axis locking -- clamp fixX/fixY bodies and zero their velocity on locked axes
  5. Drain collisions -- collect collision events and dispatch them to the event system

Events API
Nodes
Formulas API
← НазадPropertiesДальше →Nodes