Built-In Physics
Real simulation. Zero setup. Box2D at the core.
Formo ships a full rigid-body physics engine powered by planck.js — a ~40KB Box2D port. No approximations, no tweening hacks. Real forces, real collisions, real contact response.
One-Click Setup
Add the physics module to any node. Pick a body type. Press play. Gravity pulls at 9.8 m/s² and collisions just work.
3 Body Types
| Type | Behavior |
|---|---|
| Dynamic | Affected by forces, gravity, and collisions. The default for game objects. |
| Static | Immovable. Walls, floors, platforms. Zero mass, infinite inertia. |
| Kinematic | Moved by code only. Elevators, moving platforms, scripted obstacles. |
Switch between them with the TripleToggle control — one click, no dropdowns.
title: "Falling shapes"
description: "Dynamic shapes collide with a static platform — restitution controls bounce"
nodes:
- type: RECT, name: "Floor", x: 200, y: 400, w: 400, h: 30, fill: "#2C3E50"
- type: ELLIPSE, name: "Ball", x: 180, y: 80, w: 50, h: 50, fill: "#E74C3C"
- type: RECT, name: "Crate", x: 260, y: 40, w: 45, h: 45, fill: "#3498DB"
- type: POLYGON, name: "Gem", x: 140, y: 20, w: 40, h: 40, fill: "#2ECC71", sides: 5
modules:
- node: "Floor", module: "physics", config: { bodyType: "static" }
- node: "Ball", module: "physics", config: { bodyType: "dynamic", restitution: 0.8 }
- node: "Crate", module: "physics", config: { bodyType: "dynamic", restitution: 0.2 }
- node: "Gem", module: "physics", config: { bodyType: "dynamic", restitution: 0.5 }
tools_visible: [select, play]
ui_visible: [properties, modules]Automatic Colliders
The engine reads your shape type and generates the right collider — no manual setup.
| Shape | Collider | Notes |
|---|---|---|
| Rectangle | Box | Axis-aligned, matches width and height |
| Ellipse | Circle | Uses the larger radius |
| Polygon | Convex polygon | Up to 8 vertices, auto-decomposed |
Coordinate conversion uses PIXELS_PER_METER = 50. A 100px square becomes a 2m box in the physics world.
Multi-Collider Bodies
One node can hold multiple named colliders with independent type, scale, and offset. Build an L-shaped wall from two box colliders. Give a character a wide foot sensor and a narrow body hitbox. Add and remove colliders in the ColliderGroup panel.
Material Properties
Every collider exposes 7 tunable properties. Drag the SliderInput controls — pointer-lock gives you infinite drag range.
| Property | Range | Default | Effect |
|---|---|---|---|
| density | 0 – 100 | 1.0 | Mass per unit area |
| friction | 0 – 1 | 0.3 | Surface grip between bodies |
| restitution | 0 – 1 | 0.0 | Bounciness on impact |
| gravityScale | -10 – 10 | 1.0 | Per-body gravity multiplier |
| fixedRotation | bool | false | Prevents angular velocity |
| fixX | bool | false | Locks horizontal movement |
| fixY | bool | false | Locks vertical movement |
FixToggle buttons lock axes instantly. Position is captured when toggled — no drift.
Collision Events
The collide event fires on contact with full context.
// event properties
event.target // the other node
event.normal // contact normal vector
event.point // world-space contact point
event.collider // this node's collider name
event.targetCollider // other node's collider nameWrite game logic directly in the handler:
if (event.target.is("brick")) {
event.target.destroy()
Score.text = Tag.brick.length
}Sensor Colliders
Set this.sensor = true on any collider. It detects overlaps but generates no physical response — perfect for triggers, pickups, and damage zones.
Collision Bypass
Call this.disableCollisions(otherNode) to let two specific bodies pass through each other. Re-enable with this.enableCollisions(otherNode).
Formula Integration
Physics properties are first-class formula targets. Read velocity, apply forces, control movement — all from expressions.
this.velocityX // read horizontal velocity
this.velocityY // read vertical velocity
this.applyForce(x, y) // continuous force (newtons)
this.applyImpulse(x, y) // instant momentum changeThe VelocityWidget shows a direction dial and magnitude — drag to set initial velocity visually before play.
title: "Force-driven platformer"
description: "Arrow keys apply forces to a dynamic body on a static floor"
nodes:
- type: RECT, name: "Ground", x: 200, y: 380, w: 500, h: 30, fill: "#34495E"
- type: RECT, name: "Player", x: 200, y: 300, w: 40, h: 40, fill: "#E67E22"
- type: RECT, name: "Wall", x: 50, y: 300, w: 20, h: 200, fill: "#34495E"
modules:
- node: "Ground", module: "physics", config: { bodyType: "static" }
- node: "Player", module: "physics", config: { bodyType: "dynamic", friction: 0.6, fixedRotation: true }
- node: "Wall", module: "physics", config: { bodyType: "static" }
events:
- node: "Player", event: "keydown", value: |
if (key === 'ArrowRight') this.applyForce(200, 0)
if (key === 'ArrowLeft') this.applyForce(-200, 0)
if (key === 'ArrowUp') this.applyImpulse(0, -80)
tools_visible: [select, play]
ui_visible: [properties, modules, formulas]Nested Physics
Physics works inside transformed containers. The engine resolves parent rotation, scale, and translation before syncing positions — bodies inside a rotated group collide correctly.
Stable Simulation
The engine uses a fixed-timestep accumulator. Physics steps at a constant rate regardless of frame rate — 60fps or 144fps, the simulation produces identical results. No frame-dependent jitter, no tunneling at low FPS.
Non-Destructive Play
Press play — the engine snapshots every node's position, rotation, and velocity. Physics runs. Shapes fly, bounce, collide. Press stop — everything resets to the exact captured state. Iterate without fear.
title: "Breakout prototype"
description: "Ball bounces between walls and destroys tagged bricks on collision"
nodes:
- type: RECT, name: "Paddle", x: 200, y: 420, w: 100, h: 15, fill: "#ECF0F1"
- type: ELLIPSE, name: "Ball", x: 200, y: 390, w: 20, h: 20, fill: "#F39C12"
- type: RECT, name: "WallL", x: 20, y: 200, w: 10, h: 450, fill: "#2C3E50"
- type: RECT, name: "WallR", x: 380, y: 200, w: 10, h: 450, fill: "#2C3E50"
- type: RECT, name: "Ceiling", x: 200, y: 20, w: 400, h: 10, fill: "#2C3E50"
- type: RECT, name: "Brick1", x: 120, y: 100, w: 60, h: 20, fill: "#E74C3C", tag: "brick"
- type: RECT, name: "Brick2", x: 200, y: 100, w: 60, h: 20, fill: "#E74C3C", tag: "brick"
- type: RECT, name: "Brick3", x: 280, y: 100, w: 60, h: 20, fill: "#E74C3C", tag: "brick"
modules:
- node: "Paddle", module: "physics", config: { bodyType: "kinematic" }
- node: "Ball", module: "physics", config: { bodyType: "dynamic", restitution: 1.0, gravityScale: 0 }
- node: "WallL", module: "physics", config: { bodyType: "static" }
- node: "WallR", module: "physics", config: { bodyType: "static" }
- node: "Ceiling", module: "physics", config: { bodyType: "static" }
- node: "Brick1", module: "physics", config: { bodyType: "static" }
- node: "Brick2", module: "physics", config: { bodyType: "static" }
- node: "Brick3", module: "physics", config: { bodyType: "static" }
formulas:
- node: "Paddle", prop: "x", value: "mouse.x"
events:
- node: "Ball", event: "collide", value: |
if (event.target.is("brick")) event.target.destroy()
tools_visible: [select, play]
ui_visible: [properties, modules, formulas]Start building
Real physics. Visual controls. Formula-driven logic. Open Formo and drop a ball.