Physics (2D)
Opal uses Rapier 2D for rigid-body simulation. Physics runs during Play (and editor preview when enabled), driven by two built-in components:
| Component | ID | Role |
|---|---|---|
| Rigid Body 2D | rigidBody2d | Mass, velocity, gravity, body type |
| Collider 2D | collider2d | Collision shape, friction, bounce, sensors |
| Joint 2D | joint2d | Hinge / weld / distance between two bodies |
Quick setup
Dynamic object (ball, crate, character)
- Attach Rigid Body 2D — set Body Type to
Dynamic. - Attach Collider 2D — shape matches the sprite (
Use Object Sizeis usually on). - Hit Play. Gravity pulls the object down; collisions fire flow events.
A rigid body without a collider simulates motion but never collides. The inspector shows a warning when this pairing is missing.
Static floor / wall
- Attach Collider 2D only.
- Opal creates an implicit fixed rigid body automatically.
No Rigid Body 2D is required for immovable geometry.
Body types
| Type | Behavior |
|---|---|
dynamic | Simulated by Rapier. Transform is written back each frame. |
fixed | Immovable. Thing transform drives the physics body (editor drag, motion, etc.). |
kinematicPosition | Moved by setting the Thing position; pushes dynamic bodies. |
kinematicVelocity | Moved by velocity; useful for moving platforms. |
Coordinates & gravity
- Scene positions use screen-style coordinates: +Y is down, same as the stage.
- Default gravity is
(0, 980)— tuned for pixel-scale scenes withlengthUnit: 100. - Collider sizes follow object width/height (and scale) when Use Object Size is enabled.
Collision events
Both components expose:
| Event | When |
|---|---|
collisionStart | Contact begins |
collisionEnd | Contact ends |
Payload: otherId, otherName, started, sensor.
Wire these in Object Flow like any other component event. If both Rigid Body 2D and Collider 2D are attached, each may emit for the same contact — prefer listening on Collider 2D unless you need body-specific logic.
Read-only fields update during play:
- Rigid Body 2D:
vx,vy,speed,sleeping - Collider 2D:
colliding,contactCount
Common actions
Rigid Body 2D
| Action | Use |
|---|---|
setVelocity | Launch or steer (x, y) |
applyImpulse | Instant kick |
teleport | Snap position and sync the physics body |
setBodyType | Switch dynamic / fixed / kinematic at runtime |
pinTo | Weld to another body (arrow sticking). Params: targetId, atContactPoint, breakForce (max anchor separation in px; 0 = unbreakable). Fires pinBroke when the weld snaps. |
wake / sleep | Control sleep state |
Collider 2D
| Action | Use |
|---|---|
setSensor | Pass-through trigger (no physical response) |
setEnabled | Toggle participation in simulation |
generateAutoCollider | Fit collider from sprite alpha (polygon, box, circle, tight) |
Joints (Joint 2D)
Connect two physics bodies with a hinge, weld, or rope/spring. Attach Joint 2D on one object and set Target to the other. Both objects need Rigid Body 2D (and usually Collider 2D).
| Type | Behavior |
|---|---|
revolute | Hinge. Limits are authored in degrees; motors use rad/s. |
fixed | Weld (no relative motion). Used by pinTo for arrow sticking. |
distance | Rope (stiffness 0) or spring (stiffness > 0). Rest Length 0 captures the current anchor separation when the joint is created. |
Anchors are body-local pixels (0,0 = center). Drag the cyan handles on the stage when a Joint 2D object is selected. Break Separation (field breakForce) is max anchor separation in px before the joint snaps — not a force (Rapier JS does not expose joint impulses). Authored joints fire joint2d/broke; pinTo welds fire rigidBody2d/pinBroke.
Multi-object ragdoll
Place the Ragdoll Humanoid prefab (Library → Prefabs): six sibling parts with revolute joints. Bodies start as kinematicPosition (pose holds). Go limp with setBodyType → dynamic on every part (Tutorials → Ragdoll Switch). See bundled/scenes/demo-joints.json for a pendulum, chain, and arrow→crate pinTo example.
Archery (stick + damage)
The Arrow prefab (builtin_prefab_arrow) is a CCD dynamic body with baked launch velocity. Its Object Flow listens for rigidBody2d/collisionStart (target group Ragdolls), then pinTo + health.damage. Spawn with spawnBlueprint from a Fire control. Acceptance scene: bundled/scenes/demo-archery.json (one hit → go limp + death punch). Tutorials → Archery (Stick + Damage). Combat Action stays on characters with Stats/Team — not on the projectile.
Skeleton ragdoll (skinned mesh)
Author Physics bone flags per bone in Skeleton Studio (stored as skeleton.ragdoll). Add the Ragdoll component; call Activate (or enable Activate on Death with Health). Bone capsules spawn at the current animated pose, jointed with authored angle limits. The skinned mesh follows via inverse-FK writeback — no renderer changes. Deactivate frees bodies and leaves the limp pose frozen.
Character Controllers + Collider 2D
The character-controller family — Platformer Controller, Top-Down Controller, and Side-Scroller Controller — shares one movement engine. Pick the node that matches your game; only the Platformer Controller adds gravity, jumping, and foot probes.
When Collide With World is enabled on any of them, movement uses Rapier shape casts against other Collider 2D objects.
The character's probe shape is derived from its own Collider 2D (box, circle, capsule, or polygon) including offset and auto-collider geometry. Without a collider, object bounds are used as a fallback.
Recommended stack for platformers / top-down characters:
- A character controller (e.g. Platformer Controller) —
collideWithWorld: true - Collider 2D — edit with the collider tools / auto-collider
- Rigid Body 2D (optional) — use
kinematicPositionorfixed, notdynamic
World geometry: Collider 2D on floors and walls.
Foot probes (platformer ground feel)
When Collide With World is on and mode is Platformer, gold foot probes control when the character is considered grounded:
| Field | Purpose |
|---|---|
| Show Foot Probes | Draw probes and draggable handles on canvas |
| Foot Spread | Half-width from collider center to each foot (0 = collider width) |
| Foot Inset | Pull probes inward from spread edge (0 = auto) |
| Left / Right Foot X | Explicit offsets from collider center (set by dragging handles) |
| Probe Depth | Downward ground-check distance |
| Probe Lift | Start probes above the foot row |
| Foot Row Y | Move the whole foot row up/down |
| Ground Check Mode | Both Feet, Leading Foot When Moving, or Either Foot |
| Leading Foot Speed | Horizontal speed before leading-foot mode kicks in |
Drag the gold handles on the selected character in Arrange to tune ledge hang, foot placement, and probe depth. During preview, active probes brighten based on movement direction.
Collider editing (Arrange workspace)
When an object has Collider 2D attached:
- A teal overlay shows the collision shape on the stage (amber dashed when Sensor is on).
- Use Edit Collider in the inspector (or press C) to enter collider edit mode.
- Drag handles to resize primitives, move the offset, or edit polygon vertices. Edge and corner drags anchor the opposite side by default; hold Shift to resize symmetrically from the collider center.
- Auto Collider generates a fit from sprite alpha: polygon trace, box fit, circle fit, or tight bounds.
Polygon colliders are convex only (Rapier limitation). Concave silhouettes are approximated via convex hull or box/circle fit.
Toggle Show all colliders in the Collider 2D inspector to faintly draw overlays on every physics object.
Parented objects
Physics uses world pose (position + rotation in scene space). Child objects with physics components simulate at their world transform.
v1 guidance: Avoid putting a dynamic rigid body on a child whose parent moves every frame (e.g. a dragged group). The sim may fight manual parent motion. Prefer:
- Physics on root objects, or
- Kinematic/fixed parents with dynamic children that are not parented to moving transforms.
Lifecycle
| Phase | What happens |
|---|---|
| Play start | preparePhysics() loads Rapier WASM and builds bodies from scene objects |
| Each frame | stepPhysics() syncs transforms → simulates → writes dynamic bodies back |
| Play end / editor | shutdownPhysics() disposes the world |
Only custom scene objects (getCustomObjects()) participate — not built-in layout template things.
Shapes
| Shape | Notes |
|---|---|
box | Default; uses width × height |
circle | Radius from smaller object dimension or Radius field |
capsule | Pill shape; Capsule Height + radius |
polygon | Convex polygon; edit vertices in collider edit mode |
Use Offset X/Y to shift the shape relative to the object pivot.
Troubleshooting
| Symptom | Check |
|---|---|
| Object falls through floor | Floor needs Collider 2D; dynamic object needs Collider 2D too |
| No collision events | Sensor colliders don't block; verify enabled on both components |
| Object doesn't move | Body type must be dynamic; check gravityScale (0 disables gravity) |
| Jitter when parent moves | See Parented objects above |
Related source
| Path | Role |
|---|---|
src/game/physics/runtime.js | Simulation loop, sync, events |
src/game/physics/backends/rapier.js | Rapier world wrapper |
src/game/components/built-in/rigid-body-2d.js | Rigid body component |
src/game/components/built-in/collider-2d.js | Collider component |
src/game/components/built-in/joint-2d.js | Joint component |
src/game/scene-editor/joint-editor.js | Anchor drag handles |
src/game/scene-editor/collider-editor.js | Canvas collider edit mode |
src/game/physics/auto-collider.js | Auto-collider generation |
src/game/physics/collider-overlay.js | Stage overlay drawing |
src/editor-play-session.js | preparePhysics when Play starts (editor) |
src/app/bootstrap-runtime.js / frame loop | Wires play session + per-frame stepPhysics |
src/player.js | Standalone player steps physics each frame |
See also Built-in reference — Physics.