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
| Type | Behavior | Typical use |
|---|---|---|
dynamic | Affected by gravity and forces. Collides with everything. | Player, projectile, crate |
static | Never moves. Infinite mass. | Floor, wall, platform |
kinematic | Moves 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
| Property | Type | Default | Description |
|---|---|---|---|
density | number | 1.0 | Mass per unit area. Higher = heavier. |
friction | ratio 0-1 | 0.3 | Surface friction. 0 = ice, 1 = rubber. |
restitution | ratio 0-1 | 0 | Bounciness. 0 = no bounce, 1 = perfectly elastic. |
gravityScale | number | 1 | Multiplier on gravity for this body. 0 = floats. Negative = falls upward. |
fixedRotation | boolean | false | Prevents the body from rotating. |
fixX | boolean | false | Locks horizontal position. Body can only move vertically. |
fixY | boolean | false | Locks 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 type | Collider shape | Notes |
|---|---|---|
RECT, FRAME | Box | Axis-aligned rectangle |
ELLIPSE | Circle | True circle if width equals height, otherwise 8-vertex polygon approximation |
POLYGON | Polygon | Convex hull, max 8 vertices |
STAR | Polygon / Box fallback | Convex hull of the outline, falls back to bounding box |
VECTOR | Convex hull | Samples the path, computes convex hull, simplified to max 8 vertices |
Multi-collider
A single node can have multiple colliders. Each collider is a ColliderEntry:
| Field | Type | Default | Description |
|---|---|---|---|
name | string | "Collider" | Identifier. Shows up in collision events. |
type | enum | "auto" | auto, circle, box, or capsule |
scale | number (%) | 100 | Percentage scale of the collider shape |
offsetX | number (px) | 0 | Horizontal offset from node center |
offsetY | number (px) | 0 | Vertical 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 / 50pixels = 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 secondForces 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 = falseWhen 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:
| Property | Type | Description |
|---|---|---|
targetVelocity | number | Desired speed (rad/s for revolute, px/s for prismatic) |
targetPosition | number | Desired angle or translation |
maxForce | number | Maximum 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:
- Sync from nodes -- push node positions into the physics world (handles kinematic movement, teleports, parent changes)
- Step -- advance the simulation by the accumulated time
- Sync to nodes -- write dynamic body positions back to nodes
- Axis locking -- clamp fixX/fixY bodies and zero their velocity on locked axes
- Drain collisions -- collect collision events and dispatch them to the event system
Events API
Nodes
Formulas API