Skip to content

Physics

Physics lives in its own repository, ChxisB/orblit-physics, as three packages.

  • orblit_physics is the solver. It is C++ behind a build hook, driven from Dart, and it depends on nothing else in Orblit. A body is named by whatever number you already use for the thing it belongs to.
  • orblit_physics_scene connects it to scene files. It simulates every entity that has a body component and answers each step with a SceneDiff, so whatever draws a document draws the simulation too.
  • orblit_physics_terrain lays a terrain in the world as ground, the regions near the camera, and lays them again when the terrain is edited.
pubspec.yaml
dependencies:
orblit_physics:
git:
url: https://github.com/ChxisB/orblit-physics.git
path: packages/orblit_physics
orblit_physics_scene:
git:
url: https://github.com/ChxisB/orblit-physics.git
path: packages/orblit_physics_scene
orblit_physics_terrain:
git:
url: https://github.com/ChxisB/orblit-physics.git
path: packages/orblit_physics_terrain

Because the solver is compiled C++, it builds wherever a native toolchain does: macOS, iOS, Android, Linux and Windows. It does not run on the web.

import 'package:orblit_physics/orblit_physics.dart';
const ground = 1;
const ball = 2;
void main() {
final physics = Physics();
physics.add(
ground,
shape: const Shape.plane(0, 1, 0),
motion: PhysicsMotion.fixed,
);
physics.add(
ball,
shape: const Shape.sphere(0.5),
at: [0, 3, 0],
restitution: 0.6,
);
for (var tick = 0; tick < 180; tick++) {
physics.step(1 / 60);
for (final event in physics.events) {
if (event.kind == PhysicsEventKind.touchBegan) {
print('bounce, ${event.force.toStringAsFixed(2)} N·s');
}
}
}
// Seven numbers: where it is, then how it is turned, as a quaternion.
final pose = physics.transformOf(ball)!;
print('resting at ${pose[1].toStringAsFixed(2)} m');
physics.dispose();
}

Everything you ask for waits in a queue until the next step, which applies it in order. Reading goes the same way: readInto fills a buffer for every body you name in one go. So a frame costs two trips across the boundary into C++, however many bodies there are.

There are three kinds of motion.

  • Fixed never moves. Floors and walls.
  • Driven goes exactly where it is sent, and pushes whatever is in the way without being pushed back. Lifts, doors and moving platforms. Move one with drive.
  • Free falls, gets pushed, and pushes back.

A body can change from one to another after it is made. See Switching motion.

Anything that walks is a character instead, which is none of the three.

There are six shapes: a ball, a box, a capsule, a cylinder, a convex hull and endless ground. It doesn’t offer a shape it would only approximate, because a body that acts like a box while the file says cylinder is a bug people learn to work around instead of reporting. A hull is the one place it trims: past 255 corners it keeps the ones that stand out. See Cylinders and hulls.

push hits a body with an impulse, and place moves it somewhere else, forgetting how it was moving. Bodies that stop moving go to sleep one at a time, and anything that touches one wakes it up. Bodies held by joints sleep and wake together.

A cylinder is flat at both ends and stands along the body’s own y. A hull is the smallest convex solid round a list of points: a rock, a wedge, a pipe fitting. A can stands on its end or rolls on its side. A wedge settles on one of its faces.

import 'package:orblit_physics/orblit_physics.dart';
const ground = 1;
const can = 2;
const wedge = 3;
const wedgeHull = 1;
void main() {
final physics = Physics();
physics.add(
ground,
shape: const Shape.plane(0, 1, 0),
motion: PhysicsMotion.fixed,
);
// 0.3 m across and 0.8 m tall: half the height is the second number.
physics.add(can, shape: const Shape.cylinder(0.15, 0.4), at: [0, 1, 0]);
// A triangle, one metre on each short side, pushed out one metre. Six
// corners, written x, y, z one after another.
final laid = physics.layHull(
wedgeHull,
points: const [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1],
);
if (!laid) throw StateError('the wedge encloses no volume');
physics.add(wedge, shape: const Shape.hull(wedgeHull), at: [2, 1, 0]);
for (var tick = 0; tick < 180; tick++) {
physics.step(1 / 60);
}
print(physics.transformOf(can));
print(physics.transformOf(wedge));
physics.dispose();
}

Shape.cylinder(radius, halfHeight) is 2 * halfHeight tall. A capsule with the same two numbers is taller by two radii and has no flat end to stand on.

layHull cooks the points into a hull and keeps it under an id you choose. Shape.hull(id) names it, for a body or for a cast. Cook at load and not in a tick, because cooking is the slow part.

  • Points are flat. Three numbers each, x0, y0, z0, x1, y1, z1, ....
  • A hull is shared. A hundred crates cut from one mesh name one hull.
  • A hull never changes once laid. Lay another under a new id and make the bodies again. dropHull takes one away, and answers false while a body still uses it.
  • Some points lay nothing. layHull answers false for an id of zero or one already used, fewer than four points or more than 100,000, a number that isn’t finite, a length that isn’t a multiple of three, and points that all lie in one plane or on one line.
  • Past 255 corners it keeps the 255 that stand out furthest. The solid is a little smaller than the points and never larger.
  • A body is placed by the origin of the frame its points were given in. It weighs and turns about the middle of the solid, wherever that falls. A mesh whose origin is at its feet is placed by its feet and tips about its balance point, with nothing moved.
  • A body that names a hull nobody laid makes nothing. It meets nothing either.
  • A hull is convex. A bowl becomes a lid and a doorway becomes a wall. There is no shape for a surface with holes in it yet. Build one from several bodies.

Every pair of shapes meets, and a cylinder or a hull meets ground and terrain too. Characters walk over them and are stopped by them. Casts and overlap queries take either as the shape. On a height field, one that has sunk into a cliff comes out along the nearest face, which is not always the way it came in.

A cylinder is "shape": "cylinder" with a radius and a height, and the height runs from one flat end to the other. So Shape.cylinder(0.15, 0.4) is "height": 0.8.

A hull is "shape": "hull" and a flat hull list of points.

"body": {
"shape": "hull",
"hull": [0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 1, 0, 1, 0, 1, 1],
"centre": [0.0, 0.0, 0.0],
"motion": "free"
}
  • The points are in the entity’s own units, measured from centre. Each axis is stretched by that axis of the entity’s scale.
  • Bodies cut from the same points at the same size share one hull. It is laid when a body first needs it and dropped when the last one goes.
  • Points that enclose nothing make a body that does nothing. Four on one plane is one. So is a cylinder with no radius or no height. The file keeps it, and fixing the points makes it a body at once.
  • A scene numbers its hulls from one, as it numbers bodies. A hull you lay straight into scene.physics takes a negative number, as a body does, so the two never clash.
  • A snapshot keeps which hull each body is made of. restore brings the hulls back with it.

A body is a component like any other.

{
"id": "crate",
"name": "Crate",
"components": {
"transform": { "position": [0.0, 3.0, 0.0] },
"mesh": { "asset": "models/crate.glb" },
"body": {
"shape": "box",
"size": [1.0, 1.0, 1.0],
"centre": [0.0, 0.5, 0.0],
"motion": "free",
"mass": 20.0,
"friction": 0.6,
"restitution": 0.1
}
}
}
Field What it means If left out
shape box, sphere, capsule, cylinder, hull or plane box
size A box, edge to edge 1 m each way
radius A ball, a capsule or a cylinder 0.5 m
height A capsule, from tip to tip, or a cylinder, from end to end 2 m
hull A hull, as a flat list of points, x, y, z one after another. See In a scene file No points
centre Where the shape sits relative to the entity The entity’s origin
motion fixed, driven or free free
mass Kilograms. Only a free body is moved by its weight 1
friction From 0 (ice) up; 0.5 is ordinary 0.5
restitution Bounce, from 0 (a sandbag) to 1 (a perfect ball) 0
linearDamping, angularDamping How fast it slows and stops spinning by itself, per second 0.05
layers Which layers it is in, one bit each 1
cares Which layers it wants to meet All of them
asleep Starts at rest, waiting to be touched false
trigger A place instead of a solid. Nothing collides with it, and it reports what enters and leaves. Ignored on a free body false
stay Reports every step it goes on touching or holding something, not only the first and the last false
surface How fast the surface moves in the world, in metres a second. A belt. See Belts Still
locks The ways it may not move, any of moveX, moveY, moveZ, turnX, turnY and turnZ, in the world’s axes None
gravityScale How much of the world’s gravity it feels. 0 floats and a negative number rises 1
maxSpeed, maxSpin Caps on speed in metres a second and on spin in radians a second. 0 is no cap 0
centreOfMass Where its weight is, from the middle of the shape, in the entity’s own units The middle
inertia How hard it is to turn about each of its own axes, in kilogram square metres. Used only when all three are above 0 The shape’s own

The last five are body controls.

A few things are worth knowing.

  • Sizes are in the entity’s own units, so a body scales with its entity. A crate scaled to two has a body twice the size. A ball takes the largest of the three scales, because a ball cannot be stretched into an egg. A capsule or a cylinder takes its larger sideways scale for the radius and its upright one for the height. A hull is stretched by each of its three.
  • centre is for when the entity’s origin is not the middle of the thing. A character stands on its feet, and its capsule belongs round its waist.
  • A capsule no taller than twice its radius is all ends and no middle, so it is simulated as a ball.
  • plane is endless ground. It faces the entity’s own up and passes through centre. It never moves, whatever motion says.
  • Two bodies meet when either one cares about the other, not only when both do. A bullet that cares about walls hits a wall that cares about nothing.
  • Every field is written every time, even the ones the shape doesn’t use. So a box switched to a ball and back comes back the size it was.

ScenePhysics builds a world from a document. Then, each frame, advance steps it and returns a diff with the entities that moved. Hand that diff to the OrblitDocumentView that draws the document.

import 'package:flutter/scheduler.dart';
import 'package:flutter/widgets.dart';
import 'package:orblit_filament/orblit_filament.dart';
import 'package:orblit_physics_scene/orblit_physics_scene.dart';
import 'package:orblit_scene/orblit_scene.dart';
import 'package:orblit_stage/orblit_stage.dart';
class Falling extends StatefulWidget {
const Falling({super.key, required this.document});
final SceneDocument document;
@override
State<Falling> createState() => _FallingState();
}
class _FallingState extends State<Falling>
with SingleTickerProviderStateMixin {
late final OrblitDocumentView _view = OrblitDocumentView(widget.document);
late final ScenePhysics _physics = ScenePhysics(widget.document);
late final Ticker _ticker;
Duration _last = Duration.zero;
@override
void initState() {
super.initState();
_ticker = createTicker((elapsed) {
final seconds = (elapsed - _last).inMicroseconds / 1e6;
_last = elapsed;
final moved = _physics.advance(seconds);
if (!moved.isEmpty) setState(() => _view.apply(moved));
})..start();
}
@override
void dispose() {
_ticker.dispose();
_physics.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) => OrblitView(scene: _view.scene);
}

The world always steps by exactly one sixtieth of a second, however unevenly frames arrive. A solver given a wobbling step gives a wobbling simulation. A fast frame may take no step at all, and a slow one several. A frame that arrives very late takes at most eight steps and lets the rest go. That way the simulation slows down instead of seizing up while a debugger is paused. Pass step and maxSteps to change either number. A frame that takes no step leaves the bodies where they were, unless you ask for smooth display.

The diff moves each entity that moved, and anything under one that did, parents first. It is already in _physics.document, so the two documents stay the same.

An edit goes to both: to the view, so it is drawn, and to apply, so the world matches it.

import 'package:orblit_physics_scene/orblit_physics_scene.dart';
import 'package:orblit_scene/orblit_scene.dart';
import 'package:orblit_stage/orblit_stage.dart';
import 'package:vector_math/vector_math_64.dart';
// Picks an entity up and drops it from five metres.
void drop(OrblitDocumentView view, ScenePhysics physics, String id) {
final document = physics.document;
final entity = document[id];
final transform = entity?[SceneComponents.transform];
if (entity == null || transform is! TransformComponent) return;
final next = document.withEntity(
id,
entity.withComponent(
SceneComponents.transform,
TransformComponent(
position: Vector3(transform.position.x, 5, transform.position.z),
rotation: transform.rotation,
scale: transform.scale,
),
),
);
final diff = SceneDiff.between(document, next);
physics.apply(diff);
view.apply(diff);
}

apply rebuilds every body the edit touched, and every body under one of those, where the document now puts it. A rebuilt body forgets how it was moving. So a crate dragged in mid-fall stops falling, and one dropped from five metres starts from rest. Moving, resizing, adding and deleting all work this way. Renaming or hiding an entity doesn’t touch its body.

The world steps at 60 Hz. A 144 Hz screen draws 144 frames a second, so a step lands on five frames in twelve. On the other seven a body is where it was on the frame before, and anything fast stutters.

Pass smooth: true and each body is shown blended between where it was after the last two steps, by the part of a step that has built up since. A body now moves on every frame. A call to advance that takes no step still returns a diff.

import 'package:orblit_physics_scene/orblit_physics_scene.dart';
import 'package:orblit_scene/orblit_scene.dart';
import 'package:orblit_stage/orblit_stage.dart';
ScenePhysics smoothWorld(SceneDocument document) =>
ScenePhysics(document, smooth: true);
// One frame, whatever the screen's rate.
void frame(ScenePhysics physics, OrblitDocumentView view, double seconds) {
view.apply(physics.advance(seconds));
}

Translation blends in a straight line and rotation along the short arc. A body at rest is shown exactly where it is, so a sleeping crate adds nothing to the diff.

The cost is one step of lag. The document shows a body up to a sixtieth of a second behind the world. physics keeps the truth, so casts, queries, events and contacts are about where bodies are now. A ray can hit a crate that is drawn a hair short of where it was hit. Nothing is extrapolated.

An edit through apply is a teleport already: the body is shown at its new place with nothing to blend from. A move made straight on the world is not. The body would be drawn crossing the gap between the two places. Say so with resetSmoothing, which takes an entity and everything under it to where the world has them now.

stopSmoothing does that and keeps the subtree shown as the world has it, until startSmoothing. Use it for a body the camera follows, or one you place yourself every frame. Each of the three answers false for an entity the document does not have.

import 'package:orblit_physics_scene/orblit_physics_scene.dart';
// Puts an entity at a place at once, with no streak on the way.
void jumpTo(ScenePhysics scene, String id, List<double> at) {
final body = scene.bodyOf(id);
if (body == null) return;
scene.physics.place(body, at: at);
scene.resetSmoothing(id);
}
// The camera's target is drawn where it is, and the rest is blended.
void followWithoutLag(ScenePhysics scene) {
scene.stopSmoothing('player');
}

The world itself is physics, for everything a document can’t say. A document names entities with strings and the world names bodies with numbers. bodyOf and entityOf convert between the two, and an entity keeps its number for as long as it has a body. A number is never given to a second entity, so an event can’t name the wrong thing.

import 'package:orblit_physics/orblit_physics.dart';
import 'package:orblit_physics_scene/orblit_physics_scene.dart';
// Shoves an entity sideways from above its middle, so it tips as it goes.
void shove(ScenePhysics scene, String id) {
final body = scene.bodyOf(id);
final pose = body == null ? null : scene.physics.transformOf(body);
if (body == null || pose == null) return;
scene.physics.push(
body,
impulse: [40, 0, 0],
at: [pose[0], pose[1] + 0.4, pose[2]],
);
}
// What started touching what during the last advance, by entity id.
Iterable<(String, String)> touches(ScenePhysics scene) sync* {
for (final event in scene.events) {
if (event.kind != PhysicsEventKind.touchBegan) continue;
final a = scene.entityOf(event.a);
final b = scene.entityOf(event.b);
if (a != null && b != null) yield (a, b);
}
}
// The entity under a character's feet, if one is within a metre and a half.
String? standingOn(ScenePhysics scene, String character, List<double> feet) {
final hit = scene.physics.cast(
from: feet,
direction: const [0, -1, 0],
distance: 1.5,
shape: const Shape.sphere(0.3),
ignore: scene.bodyOf(character) ?? 0,
);
return hit == null ? null : scene.entityOf(hit.body);
}

Read events from scene.events, not scene.physics.events. The world only keeps its last step. scene.events has everything from all the steps the last advance took, and nothing when it took none. A touch names the pair smaller number first, so you can key on it without sorting. A trigger’s events name the trigger first instead. See Triggers.

cast fires a shape along a line and returns the first body it meets. With no shape it fires a point, which is a ray cast. A cast that starts inside a body says so with started, instead of reporting a distance of zero and leaving you to guess. A character who begins inside a wall wants pushing out, not stopping where they are.

A body you add straight to scene.physics needs a negative number. The numbers from one upwards belong to the document. It is simulated, but never written back.

Five questions, one call each.

Ask Call Answer
What does this run into first? cast One hit, or null
What does it run into, in order? castAll Hits, nearest first
Does it run into anything? castAny true or false
What is inside this shape? overlap with a shape Body numbers
What is at this point? overlap with no shape Body numbers

A hit names the body, the point at, the normal of the surface where it faces the cast, and the distance along it. A cast with no shape is a ray. One that starts inside a body sets started.

import 'package:orblit_physics/orblit_physics.dart';
// Which way each surface faces along a shot, nearest first.
List<List<double>> normalsAlong(Physics physics) => [
for (final hit in physics.castAll(
from: const [0, 1.5, 0],
direction: const [0, 0, -1],
distance: 30,
limit: 4,
))
hit.normal,
];
// Whether a crate-sized shape can be pushed two metres along x.
bool isClear(Physics physics, int mover) => !physics.castAny(
from: const [0, 1, 0],
direction: const [1, 0, 0],
distance: 2,
shape: const Shape.box(0.5, 0.5, 0.5),
ignore: mover,
);
// What is standing in a doorway, and what holds a point in the wall.
List<int> inDoorway(Physics physics) => physics.overlap(
at: const [4, 1, 0],
shape: const Shape.box(0.5, 1, 0.1),
);
List<int> holding(Physics physics) => physics.overlap(at: const [4, 1, 0]);

Every call takes the same filters and has its own way to stop early.

  • layers and ignore. The question is a body on those layers, so it meets what it cares about or what cares about it, as two bodies do. ignore leaves one body out, so a character’s own cast doesn’t find them.
  • triggers. Left false, a question sees solid bodies and never a trigger, which is what a bullet or a camera wants. Set true, it sees triggers and nothing solid, which is how a game asks which zone a ray runs through.
  • castAny stops at the first body it finds and never works out which was nearest, so it is the cheap way to ask whether a wall is in the way.
  • castAll and overlap take a limit, 32 by default. A full castAll keeps the nearest, not the first it found. overlap gives bodies in no order.
  • A plane can’t be cast or overlapped. A point question does find the plane it is under, because a plane contains everything below its surface.
  • Asking moves nothing and wakes nothing.

A free body can be held to a plane, capped in speed and given a gravity of its own. setControls sets all of it for one body in one call.

import 'package:orblit_physics/orblit_physics.dart';
// A cart on a flat track: it stays in the x and y plane and cannot tip out of
// it, feels half the gravity and is never faster than eight metres a second.
void layTrack(Physics physics, int cart) {
physics.add(cart, shape: const Shape.box(0.5, 0.25, 0.25), at: [0, 2, 0]);
physics.setControls(
cart,
const PhysicsControls(
locks: {PhysicsLock.moveZ, PhysicsLock.turnX, PhysicsLock.turnY},
gravityScale: 0.5,
maxSpeed: 8,
),
);
}
  • locks hold any of six ways a body can go: moveX, moveY, moveZ, turnX, turnY and turnZ, in the world’s axes. A locked move drops that part of a velocity, of a push and of every contact. The solver holds them, so a body locked to a plane still rests on a floor that is tilted across it, and a joint keeps its length.
  • gravityScale is how much of the world’s gravity, and of a zone’s, the body feels. One is ordinary, zero floats and a negative number rises.
  • maxSpeed and maxSpin cap the speed in metres a second and the spin in radians a second. Zero is no cap. A cap is applied once a step, after the contacts, so a body can pass it inside one step but never leaves a step above it.
  • centre is where the weight is, in the body’s own frame. In a scene file it is centreOfMass, because centre there is where the shape sits. Zero is the middle of the shape. The body turns about this point, and a push through it doesn’t spin the body. The transform still reports the origin the body was placed by. A push with no at goes through the centre of mass.
  • inertia is three numbers, about each of the body’s own axes through the centre of mass. It is used as given, and only when all three are above zero. Otherwise the body turns as its shape would, moved to the centre of mass.

All of it is set together, so changing one field sends the others again. setControls returns false, changing nothing, for a body that isn’t there, ground, a number that isn’t finite and a negative cap or inertia. A fixed or driven body keeps its controls for when it is made free. quiet: true wakes nothing, for a body that has just been added and should stay asleep.

setGravity changes the gravity of the whole world, in metres per second squared, and wakes every body that can move. Zones and each body’s gravityScale still apply on top of it.

setMotion makes a body fixed, driven or free. A body made fixed or driven keeps where it is and stops feeling forces. Fixed stops it, and driven keeps the velocity it had. A body made free has the mass it was created with and the controls it was given, then falls and is pushed. What rested on it or was resting on it wakes.

import 'package:orblit_physics/orblit_physics.dart';
// A level on the moon, and a door that a cutscene shuts and later frees.
void goToMoon(Physics physics) {
physics.setGravity(const [0, -1.62, 0]);
}
void shut(Physics physics, int door) {
physics.setMotion(door, PhysicsMotion.fixed);
}
void release(Physics physics, int door) {
physics.setMotion(door, PhysicsMotion.free);
}

It does nothing for a trigger, a character, ground, a body that isn’t there and a body that already is what it is asked to be.

A rule with ignore makes a pair pass through each other, with no contact and no touch event, whatever their layers say. Both bodies wake when it is set or taken away. ScenePhysics does the same for two entities.

import 'package:orblit_physics_scene/orblit_physics_scene.dart';
// A ghost that walks through a stack of crates, and then stops doing so.
void haunt(ScenePhysics scene) {
scene.ignore('ghost', 'stack');
}
void solidify(ScenePhysics scene) {
scene.unignore('ghost', 'stack');
}
  • ignore returns false, changing nothing, for an entity that has no body and for an entity against itself. It replaces any rule the pair had.
  • It holds through the edits that rebuild either body. It ends when either entity loses its body or goes. The pair is kept by ScenePhysics and is not in the scene file, because it is a fact about two entities at run time.
  • A character doesn’t read it. A character reads layers, so to let one walk through a body, put the two on layers that don’t meet.

A material is friction and restitution set together. There are six, from the slipperiest.

Material Friction Restitution
Ice 0.05 0.05
Metal 0.4 0.15
Wood 0.5 0.2
Stone 0.7 0.1
Sandbag 0.9 0
Rubber 0.95 0.8
import 'package:orblit_scene/orblit_scene.dart';
BodyComponent onIce(BodyComponent body) =>
body.madeOf(BodyMaterial.presets.first);
// Null when the two numbers match no preset.
String? materialOf(BodyComponent body) => BodyMaterial.of(body)?.name;

The file keeps the two numbers and not the name. A body is a preset only while both numbers match exactly, so a preset added later never changes a scene that was saved.

A scene file names its layers with layerNames beside sky and ambient, for example "layerNames": ["Player", "Enemy", "Bullet"]. The first name is layer 1, which is the bit 1, and layer 3 is the bit 4. There can be thirty-two. A name is a label for people, and the solver only sees the bits. SceneSettings.nameOf gives the name of a layer, or null when it has none.

A trigger is a place. Set trigger on a body that stays where it is, fixed or driven, and nothing collides with it. It pushes nothing, and characters and solid questions walk through it. What it does is report what comes in and goes out.

import 'package:orblit_physics/orblit_physics.dart';
import 'package:orblit_physics_scene/orblit_physics_scene.dart';
// Which entity came into which trigger during the last advance.
Iterable<(String, String)> arrivals(ScenePhysics scene) sync* {
for (final event in scene.events) {
if (event.kind != PhysicsEventKind.entered) continue;
final place = scene.entityOf(event.a);
final visitor = scene.entityOf(event.b);
if (place != null && visitor != null) yield (place, visitor);
}
}
  • entered when a body comes in, and exited when it leaves or when either one is removed. a is the trigger and b is the body, whichever number is smaller. This is the one pair that is not named smaller number first. at and normal say where a body came in, and are zero on exit.
  • stay on the trigger or on the body adds inside, every step the body is still in. Either one asking is enough.
  • A body with stay also gets touchStay for each solid contact that goes on, so a game can burn a foot on hot ground without counting steps itself.
  • touchStay stops when the body goes to sleep, because a pair at rest has its touch ended, as touchEnded says elsewhere. A trigger looks at where bodies are, not at what they touch, so inside goes on for a body at rest.
  • A free body can’t be a trigger. The flag is ignored, because the solver has to move it. A trigger doesn’t see another trigger or a fixed body.

A zone changes how free bodies move inside a trigger: their gravity and their damping. Water is a weak gravity and a lot of drag. A lift shaft is a gravity that points up.

import 'package:orblit_physics/orblit_physics.dart';
void makePond(Physics physics, int pond) {
physics.add(
pond,
shape: const Shape.box(4, 1, 4),
motion: PhysicsMotion.fixed,
trigger: true,
);
physics.setZone(
pond,
const PhysicsZone(
gravity: [0, -1.5, 0],
linearDamping: 4,
angularDamping: 2,
priority: 1,
),
);
}
  • A field left out is left to the body. A zone with only linearDamping never touches gravity.
  • Where zones overlap, each field is decided on its own. It takes the answer of the zone that sets it with the highest priority, and at a tie the lower body number. So water can set the drag while a wind zone over the same place sets the pull.
  • A zone acts on free bodies, not on characters. A character asks for its own gravity in drive, and a fixed or driven body doesn’t fall.
  • setZone returns false, changing nothing, for a body that isn’t a trigger. Setting it again replaces the zone. removeZone takes it off and leaves the trigger. Both wake what is inside, so a body asleep in a pond hears about it.

In a scene file a zone is a component of its own, on an entity that has a body. It makes that body a trigger.

{
"id": "pond",
"components": {
"transform": { "position": [0.0, 0.0, 0.0] },
"body": { "shape": "box", "size": [8.0, 2.0, 8.0], "motion": "fixed" },
"zone": {
"gravity": [0.0, -1.5, 0.0],
"linearDamping": 4.0,
"priority": 1
}
}
}

gravity, linearDamping and angularDamping are each left out to leave them alone. priority is 0 if left out. Editing a zone rebuilds its body, like any other edit, so it is a teleport for whatever the body was doing.

setSurface makes a body’s surface move while the body stays where it is. What stands on it is dragged by friction towards the speed of the surface, and to that speed and no more.

import 'package:orblit_physics/orblit_physics.dart';
void layBelt(Physics physics, int belt, int crate) {
physics.add(
belt,
shape: const Shape.box(3, 0.1, 1),
motion: PhysicsMotion.fixed,
friction: 1,
);
physics.setSurface(belt, velocity: const [2, 0, 0]);
physics.add(
crate,
shape: const Shape.box(0.3, 0.3, 0.3),
at: [0, 0.4, 0],
friction: 1,
);
}
  • The velocity is in the world, in metres a second. Only the part along the surface it touches counts, so a velocity straight into the body does nothing. Turning the belt’s entity doesn’t turn its velocity.
  • It holds until set again, and zero is an ordinary surface. Bodies on it are woken, so a belt that starts carries the crate that had gone to sleep on it.
  • It is a property of the body, not of a pair. In a file it is surface on the body, three numbers.
  • A crate that a belt is carrying doesn’t go to sleep. It is still being moved.

setRule changes what is true whenever two particular bodies touch, and nowhere else.

import 'package:orblit_physics/orblit_physics.dart';
// A lift that carries a crate without the crate slowing it, and mud that
// grips a boot and lets nothing bounce.
void setRules(Physics physics, int lift, int crate, int mud, int boot) {
physics.setRule(lift, crate, const PhysicsRule(moveScaleA: 0));
physics.setRule(mud, boot, const PhysicsRule(friction: 2, restitution: 0));
}
  • friction and restitution replace what the pair would have used.
  • moveScaleA and moveScaleB say how much of this contact’s push each body takes. One leaves a body as it is, zero makes it immovable to the other, and two moves it as if it were half the mass. They follow the order you named the bodies in, whichever way the world keeps them.
  • It is data, not a callback. A Dart function called from inside the solver for every contact on every step would put a trip across the boundary in the hottest loop there is. A rule is sent once and read in C++. A game that wants to decide from what it sees reads events and sets a rule.
  • It holds until removeRule, or until either body goes. Laying ground again keeps rules on it. Both bodies are woken when a rule is set or taken away.
  • setRule returns false, changing nothing, for a body that isn’t there, a body against itself, a number that isn’t a number or a negative scale. A rule that changes nothing isn’t a rule, so at least one field is given.

A joint holds two bodies together, or one body to the world. The second body can move six ways relative to the first: along the three axes of the joint’s frame, and about them. A kind of joint is only which of those it holds, and how far it lets the others go. A hinge holds five and leaves the turn about x free, perhaps within a limit.

import 'dart:math';
import 'package:orblit_physics/orblit_physics.dart';
const hinge = 1;
const door = 2;
void main() {
final physics = Physics();
// A door a metre wide and two high, its hinged edge on the origin.
physics.add(door, shape: const Shape.box(0.5, 1, 0.025), at: [0.5, 1, 0]);
// The frame turned a quarter about z, so its x axis stands upright and the
// door turns about it. A motor opens it, and the limit stops it square.
physics.join(
hinge,
const Joint.hinge(limit: JointLimit(0, pi / 2), speed: 1, strength: 50),
a: 0,
b: door,
at: [0, 1, 0],
rotation: [0, 0, sin(pi / 4), cos(pi / 4)],
);
for (var step = 0; step < 120; step++) {
physics.step(1 / 60);
}
final state = physics.jointStateOf(hinge)!;
print('open ${(state.angles[0] * 180 / pi).toStringAsFixed(0)}°');
physics.dispose();
}

There are seven kinds.

  • fixed welds the two together.
  • point holds two points together and lets both bodies turn any way: a pendulum, or a ball and socket.
  • hinge turns about the frame’s x axis only. A door, a wheel, a knee.
  • slider moves along x only, never turning. A drawer, a piston.
  • distance keeps a point on each body a distance apart and lets both turn. With no limit it is a rod as long as it was made. With a limit it is a range, so JointLimit(0, 2) is a two-metre rope, slack until it is taut.
  • cone lets the second body’s x axis swing within swing of the first’s, in any direction, and twist within a limit. A shoulder or a hip.
  • sixAxis sets each of the six on its own: free with no limit, locked where a limit’s low is its high, and a range otherwise.

A hinge and a slider take a motor. It drives at speed and never pushes harder than strength. No strength is no motor, and a little strength with no speed is friction in the joint.

Limits are metres along an axis and radians about one. A joint is solved in the same pass as the contacts, so a door resting on a crate and the crate resting on the floor are one problem, not two that take turns being wrong.

A few things are worth knowing.

  • It holds how the bodies stood when it was made. Every limit is measured from there, so a hinge made with its door shut reads nought when the door is shut.
  • Every measure is the second body as the first sees it. So which body comes first decides the sense. With the world as a, a door’s angle is the door’s. With the world as b, it is the world’s angle as the door sees it, which turns the other way.
  • jointStateOf reads a joint back. It gives where the second body is and how far it has turned, and how hard the joint held on the last step. That force is the number to look at before choosing breakingForce or breakingTorque. Past either, the joint breaks, and a broke event names it: a is the joint and b is zero.
  • A range only pushes at its ends. A hinge swinging in the middle of its range is held by its five locked rows and nothing else.
  • Turning is a twist about x and then a swing. That is the angle it looks like to within a quarter turn either way, and it stops meaning anything close to a half turn. A joint that could bend that far wants a limit that stops it first.
  • Joined bodies sleep and wake together. Half a chain asleep would be half a chain that has stopped being held.
  • Removing a body removes its joints, without a broke event, because nothing broke. Whatever they held wakes up and falls.
  • The two bodies don’t collide with each other unless collide says so, because joined bodies nearly always overlap where they are joined.

Joint numbers are separate from body numbers, so a joint can share a number with a body.

In a document a joint is a joint component. Which two bodies it holds comes from where its entity sits in the tree, not from ids it names. It holds the nearest body at or above its entity to the nearest body above that one, or to the world when there is none.

[
{
"id": "door",
"name": "Door",
"components": {
"transform": { "position": [0.5, 1.0, 0.0] },
"body": { "shape": "box", "size": [1.0, 2.0, 0.05] }
}
},
{
"id": "hinge",
"name": "Hinge",
"parent": "door",
"components": {
"transform": {
"position": [-0.5, 0.0, 0.0],
"rotation": [0.0, 0.0, 90.0]
},
"joint": {
"kind": "hinge",
"limits": { "aboutX": [0.0, 90.0] },
"speed": 60.0,
"strength": 50.0
}
}
}
]

The hinge is an entity of its own under the door, placed at the door’s edge and turned so its x axis stands upright. It has no body, so the body it holds is the door’s. Nothing above the door has a body, so the door is held to the world. A forearm’s elbow goes on the forearm, or on an entity under it, with the forearm under the upper arm.

Field What it means If left out
kind fixed, point, hinge, slider, distance, cone or sixAxis hinge
limits A low and a high for any of alongX, alongY, alongZ, aboutX, aboutY and aboutZ, in metres and degrees. An axis left out is free None
swing How far a cone swings, in degrees 45
speed, strength A hinge’s or a slider’s motor, in degrees or metres a second and newton-metres or newtons 0
breakingForce, breakingTorque Where it breaks. Nought never breaks 0
collide Whether its two bodies still collide with each other false

A hinge and a cone read aboutX, and a slider and a distance joint read alongX. Only sixAxis reads the rest. Every field is written every time, so a hinge switched to a slider and back keeps its range.

  • Its point and frame are its entity’s, where the scene begins. Scale doesn’t count. A distance joint measures from that point to the middle of its body.
  • Angles are in degrees, as a transform’s are. ScenePhysics hands the world radians.
  • An edit makes it again. apply makes a joint again wherever either of its bodies is rebuilt, holding the bodies as they now stand.
  • A broken joint stays broken until its own entity is edited. Dragging the door somewhere else doesn’t mend the hinge it tore off. scene.broken lists the entities whose joints have broken.
  • It can’t close a loop. A chain tied at both ends needs a joint that names two bodies, and a tree has only one parent. Join those yourself on scene.physics, with a negative number.

jointOf and entityOfJoint turn an entity into its joint number and back, the way bodyOf and entityOf do for bodies.

import 'dart:math';
import 'package:orblit_physics/orblit_physics.dart';
import 'package:orblit_physics_scene/orblit_physics_scene.dart';
// The entities whose joints broke during the last advance.
Iterable<String> torn(ScenePhysics scene) sync* {
for (final event in scene.events) {
if (event.kind != PhysicsEventKind.broke) continue;
final entity = scene.entityOfJoint(event.a);
if (entity != null) yield entity;
}
}
// How far open the door hung on [hinge] is, in degrees.
double? opening(ScenePhysics scene, String hinge) {
final joint = scene.jointOf(hinge);
final state = joint == null ? null : scene.physics.jointStateOf(joint);
return state == null ? null : state.angles[0] * 180 / pi;
}

A free body is the wrong thing to walk around in. The solver would bounce it off walls, catch its feet on every step and slide it down every slope. A character is not solved at all. Each step it is swept through the world along the velocity it asked for, and it slides along whatever it meets.

import 'package:orblit_physics/orblit_physics.dart';
const floor = 1;
const player = 2;
// Two seconds of walking east, with one jump along the way.
void main() {
final physics = Physics();
physics.add(
floor,
shape: const Shape.plane(0, 1, 0),
motion: PhysicsMotion.fixed,
);
// The default capsule is 1.8 m tall, so its centre is 0.9 m above its feet,
// and it keeps a centimetre off everything.
physics.addCharacter(player, at: [0, 0.91, 0]);
const tick = 1 / 60;
for (var step = 0; step < 120; step++) {
final footing = physics.footingOf(player)!;
final jump = step == 30;
final up = footing.grounded && jump
? 4.0
: footing.velocity[1] - 9.81 * tick;
physics.drive(player, velocity: [1.5, up, 0]);
physics.step(tick);
}
final at = physics.transformOf(player)!;
print('${at[0].toStringAsFixed(2)} m east');
physics.dispose();
}

For a character, drive is a request, not an order: where it would like to go, in metres a second, relative to whatever it stands on. The world decides how much of that it gets.

  • It walks up steps as tall as stepHeight and stops at taller ones.
  • It stands on slopes up to steepest and slides down steeper ones.
  • It rides what it stands on. A lift carries it up and a turntable turns it round.
  • It pushes free bodies it walks into, no harder than strength, and nothing pushes it but a driven body. A rolling crate stops against a player. A closing door shoves them aside.
Parameter What it means If left out
shape What it is made of A capsule 0.6 m across and 1.8 m tall
stepHeight The tallest step it walks up 0.3 m
steepest The steepest slope it stands on, in radians π/4, which is 45°
skin The gap it keeps from everything, so it doesn’t snag on the seam between two floor tiles 0.01 m
strength The hardest it pushes a free body, in newtons 500

friction and layers mean what they do for any body.

Gravity is part of what a character asks for. That leaves the game to decide what a jump is, how long a player hangs at the top of one, and whether they can steer in the air. Leave it out and the character floats.

footingOf says what came of the last request.

  • velocity is what survived of it. A floor takes away the fall, and a wall takes away the part going into it. Build each request from this, not from the last one, and a character standing still won’t pile up a fall it isn’t taking and then drop through the next hole at full speed.
  • grounded is whether it is standing on something it may stand on. It is false in the air, on a slope too steep to climb, and on the way up a jump, so a held jump key launches it once.
  • ground is the body underneath, or 0 for none, and normal is which way that surface faces.
  • carried is how fast the ground moved it, and turning is how fast the ground turned it about up, in radians a second. These are kept apart from velocity, so a walk animation plays the walk it asked for and not the ride. A camera can add turning to turn with a turntable.

footingsOf reads a whole crowd in one trip, and gives null for anything that isn’t a character.

A clip with root motion says how far the walk would have gone, and the character decides how far it actually goes. Turn the step to face the way the character faces, divide it by the tick, and ask for that.

import 'package:orblit_motion/orblit_motion.dart';
import 'package:orblit_physics/orblit_physics.dart';
import 'package:vector_math/vector_math_64.dart';
// A character the walk carries, with the world deciding where it gets to.
class Walker {
Walker(this.physics, this.id, ClipDocument walk)
: player = ClipPlayer(
walk.copyWith(rootMotion: RootMotion(bone: 'hips', turns: true)),
whenDone: WhenDone.loop,
)..play();
final Physics physics;
final int id;
final ClipPlayer player;
Quaternion heading = Quaternion.identity();
// Once a tick, before the world steps by the same [seconds].
void step(double seconds, {double scale = 1}) {
final moved = player.advance(seconds).moved;
final walk = heading.asRotationMatrix().transformed(moved.position) *
(scale / seconds);
heading = (heading * moved.rotation)..normalize();
final fall = physics.footingOf(id)!.velocity[1] - 9.81 * seconds;
physics.drive(id, velocity: [walk.x, fall, walk.z]);
}
}

The character’s transform is its position now, so don’t add the step to a position as well. A clip that rises, such as a climb, carries its own up and down, so ask for walk.y in place of the fall while it plays.

ScenePhysics makes no characters, because a body component has no character motion yet. Add one to scene.physics with a negative number, and draw it from transformOf.

A character has to be driven before every step, or it keeps what survived of its last request and gains no more gravity. advance takes several steps or none, so don’t drive once a frame and then call advance with the frame’s time. Keep your own clock, and on each tick drive the character, then call advance with scene.step. That takes exactly one step.

import 'package:orblit_physics_scene/orblit_physics_scene.dart';
import 'package:orblit_scene/orblit_scene.dart';
const player = -1;
// One tick of a scene with a player in it: the player first, then the world.
SceneDiff tick(ScenePhysics scene, double x, double z, bool jump) {
final footing = scene.physics.footingOf(player)!;
final up = footing.grounded && jump
? 4.0
: footing.velocity[1] - 9.81 * scene.step;
scene.physics.drive(player, velocity: [x, up, z]);
return scene.advance(scene.step);
}

A plane is flat forever. Ground is heights on a grid, laid as one fixed body. Each square of four samples is two triangles, split from corner (c, r) to corner (c + 1, r + 1).

import 'dart:math';
import 'package:orblit_physics/orblit_physics.dart';
const hill = 1;
const ball = 2;
void main() {
final physics = Physics();
// 33 by 33 samples half a metre apart: a 16 m square round the origin.
const samples = 33;
final heights = [
for (var r = 0; r < samples; r++)
for (var c = 0; c < samples; c++)
2 * cos((c - 16) / 10) * cos((r - 16) / 10),
];
physics.layGround(
hill,
heights: heights,
columns: samples,
rows: samples,
spacing: 0.5,
at: [-8, 0, -8],
);
// Dropped beside the top, it rolls down the side.
physics.add(ball, shape: const Shape.sphere(0.3), at: [0.5, 4, 0]);
for (var step = 0; step < 300; step++) {
physics.step(1 / 60);
}
print(physics.transformOf(ball));
physics.dispose();
}

Sample (c, r) is heights[r * columns + c], at at plus (c × spacing, height, r × spacing).

  • Everything under the surface is ground. A crate that has sunk in comes up and out, never down through. That includes a crate asleep on ground that is raised under it.
  • A height that isn’t a number is a hole. Things fall through it and roll off its rim.
  • Laying it again replaces it. Call layGround with the same id and new heights, and everything over the old ground or the new wakes up. A crate on a hill that was lowered falls with it.
  • Taking it away wakes nothing, as with any body. A crate asleep on ground that has streamed out stays where it was until something wakes it, and is still there when the ground comes back.
  • The lines between triangles are seams, not edges. A ball rolls across flat ground without hopping at each one, and a box sits in a crease between two slopes.

Ground too big to lay at once is laid in pieces side by side, and the edge of a piece is a seam too. With margin: true, the outer ring of samples is the neighbouring pieces’ heights, which are never stood on. That’s how a ridge along the line between two pieces holds a ball up the way a single piece would. at is then the corner of the margin, one spacing out.

TerrainPhysics does all of that for a terrain. Each region is a piece, laid with a margin, and only the regions near a point are laid at all.

import 'package:orblit_physics/orblit_physics.dart';
import 'package:orblit_physics_terrain/orblit_physics_terrain.dart';
import 'package:orblit_terrain/orblit_terrain.dart';
void main() {
final physics = Physics();
final terrain = Terrain(regionSize: 64);
terrain.fillHeights(const RegionKey(0, 0), (x, z) => 0.1 * x);
final ground = TerrainPhysics(physics, terrain);
// Every frame, round the camera.
ground.sync(x: 32, z: 32, radius: 64);
}

sync lays the regions within radius metres, takes up the ones that have gone a region’s width further than that, and lays a region again when its heights or holes change, or its neighbours’ do. It answers how many it laid. The ground it lays is the ground the renderer draws, triangle for triangle, holes included, and a region that doesn’t exist is a hole too.

While a brush stroke is still being drawn, call sync with refresh: false. Nothing changed is laid again until the stroke is done and sync is called normally, so a brush doesn’t rebuild the ground under it every frame.

Its bodies are numbered far below zero, out of the way of entity handles and of the small negative numbers you give your own bodies. regionOf turns one of them, from an event or a cast, back into the region it is.

snapshot copies the world and restore puts it back. Use them for an undo, for a rollback that resimulates from a known frame, for a replay, and for a test that runs the same hundred steps twice.

import 'package:orblit_physics/orblit_physics.dart';
// Whether a hundred steps from here end in the same place both times.
bool replaysTheSame(Physics physics) {
List<double> run() {
for (var i = 0; i < 100; i++) {
physics.step(1 / 60);
}
return physics.transformOf(1)!.toList();
}
final snapshot = physics.snapshot();
final first = run();
physics.restore(snapshot);
final second = run();
snapshot.dispose();
for (var i = 0; i < first.length; i++) {
if (first[i] != second[i]) return false;
}
return true;
}

A restored world given the same commands and the same step sizes reaches the same bits. That holds on one build. It is not a promise across builds, compilers or machines. See what it does not do.

A snapshot holds what a step reads from the step before: bodies, joints, ground, zones, rules, characters, the settings and the gravity, the contacts of the last step and its events. Commands you queued and have not stepped are in the copy. A restore drops the ones queued since.

  • A snapshot is native memory. Call dispose when you are done with it. It owns its copy, so it outlives the world it came from and restores into another world. Restore it as many times as you like. A disposed one throws a StateError.
  • Ground is shared, not copied. A height field never changes once it is laid, so a snapshot keeps a reference to it.
  • It stays in memory. There is no file format. A snapshot does not go to disk or to another process.

contacts lists the contact points of the last step. Each has the two bodies, smaller number first, the point at midway between the surfaces, the normal out of the second body towards the first, the depth of the overlap in metres and the impulse the solver pushed with, in newton-seconds. They come in the order of the pairs’ numbers. A pair that is asleep is not listed, because nothing looked at it.

stats counts what the world holds and what its last step looked at: bodies by kind, how many are asleep, triggers, characters, joints, zones, rules, pairs, touching pairs and contact points. stepMicroseconds is how long the last step took in the engine. A snapshot does not carry it, because it is about the machine. There is no count of islands, because the solver has none yet.

import 'package:orblit_physics/orblit_physics.dart';
// How hard each body is pressing on body 7 over the last step.
Map<int, double> pressingOn(Physics physics) {
final pressing = <int, double>{};
for (final contact in physics.contacts) {
if (contact.a != 7 && contact.b != 7) continue;
final other = contact.a == 7 ? contact.b : contact.a;
pressing[other] = (pressing[other] ?? 0) + contact.impulse;
}
return pressing;
}
// One line for an overlay.
String overlay(Physics physics) {
final stats = physics.stats;
return '${stats.dynamicBodies} free, ${stats.asleep} asleep, '
'${stats.touching} of ${stats.pairs} pairs touching, '
'${stats.stepMicroseconds.round()} µs';
}

ScenePhysics has its own snapshot and restore. Its snapshot holds the world’s and what the scene keeps beside it: the document, which body each entity is, the joints and which have broken, the pairs told to ignore each other, the time still owed to the next step and the events of the last advance. In a scene that smooths it also holds how each body was moving in the blend, and which entities were switched off with stopSmoothing. A snapshot from a scene that smooths does not restore into one that does not, or the other way round. restore throws an ArgumentError.

restore returns the SceneDiff that takes the document from what it was to the snapshot’s. The diff is already applied to document, so hand it to whatever draws the scene.

import 'package:orblit_physics_scene/orblit_physics_scene.dart';
import 'package:orblit_scene/orblit_scene.dart';
// An undo that goes back a frame at a time.
final class Rewind {
Rewind(this.scene);
final ScenePhysics scene;
final List<ScenePhysicsSnapshot> _frames = [];
void mark() => _frames.add(scene.snapshot());
SceneDiff back() {
final snapshot = _frames.removeLast();
final diff = scene.restore(snapshot);
snapshot.dispose();
return diff;
}
}

The next body and joint numbers go back with the snapshot. An entity added after it gets the same number again on a replay, which matters because the world orders events and contacts by number. A snapshot restored into a scene with a different step does not replay. Restoring a disposed snapshot throws a StateError and changes nothing.

Select anything that is drawn and the inspector has a Physics body section. Add body fits the shape to the mesh, so a crate gets a crate-sized box. Fit to mesh does it again after the mesh changes, and switching the shape keeps the sizes the other shapes had. Mass, drag and how the body starts only show for a free body, because nothing else uses them.

The Shape rows are Box, Ball, Capsule, Cylinder, Hull and Ground, in two rows of three. A hull is cut from the mesh you see, and from the eight corners of its box when the editor has no mesh to read. An object that isn’t drawn gets a metre box. The line under the rows counts its corners, or says the points enclose no volume and the body does nothing until they do. Fit to mesh cuts it again. The view draws a hull’s edges and a cylinder’s rings. It draws every corner of a hull, even past the 255 the simulation keeps.

A body that stays put has three more rows. Acts as is Solid or Trigger. Stay events is Off or On, and turns on the reports every step. Belt speed is three numbers, in metres a second in the world, and is hidden on a trigger because nothing stands on one.

Those bodies also have a Zone section. Add zone puts one on and makes the body a trigger. Gravity, Drag and Spin drag each say Same, which leaves it to the body, or Own, which is the zone’s. A field starts at the world’s gravity or the body’s own drag, so switching it on changes nothing until you drag it. Priority decides which zone wins where two overlap.

While an object with a body is selected, the view draws its shape as a wireframe: green for a solid body, amber for a trigger or a zone. It is drawn the way the simulation will read it, scaled with the object. A body fitted to the wrong mesh shows up as a box of the wrong size while you are placing it, not later as a crate floating above the floor.

Anything with a body at or above it has a Joint section too. It says which two bodies the joint holds, which is where the entity sits in the tree and otherwise unseen until the scene runs. The kind comes from a menu, and each limit the kind reads is free, locked or a range, in degrees or metres. While a joint is selected, the view draws it in amber: its frame with x drawn long, a line to each body it holds, and its limits. A hinge’s limit is an arc from its y axis towards z, a slider’s is its travel in metres, and a cone’s is the ring it swings inside.

A free body has the body controls as rows: Gravity, Lock move and Lock turn with a switch for each axis, Max speed, Max spin, Weight at and Inertia. Zero in a cap is no limit, and Inertia is used once all three parts are above zero.

Every body has a Material menu of the six presets. It reads Custom once friction or bounce is dragged to something that matches none. Two grids of thirty-two switches, Is in and Sees, set layers and cares. A pair meets when either one sees the other’s layer.

The scene’s own inspector has a Layers section for naming them. It shows a field for each named layer and one more to type the next into, and Show all 32 opens the rest. A name shows when the pointer rests on a switch.

The editor doesn’t run the simulation yet. It doesn’t make pairs that ignore each other either, since that is made while the scene runs.

  • Give the same answer on two different machines. Replaying the same inputs at the same step on one machine gives the same result. Two different machines can drift apart. Guaranteeing that they don’t would make the solver slower on every platform, and only lockstep networking needs it. Orblit’s multiplayer sends state instead. A restored snapshot follows the same rule.
  • Draw contacts. contacts gives the points and normals. Nothing draws them in the stage or the editor yet.
  • Stop fast, small things passing through walls. A small enough step for the speeds in play is the answer for now. For the few things that can’t afford one, cast along where they are about to go.
  • Call your code in the middle of a contact. A rule is data for one pair of bodies, set before they meet. See Changing one contact.
  • Move a character with a zone. A zone acts on free bodies. A character asks for its own gravity.
  • Let a body pass up through a platform. There are no one-way platforms.
  • Make a character ignore a body. A character doesn’t read pair rules. Use layers.
  • Make a shape with a hole in it. A hull is convex, and there is no mesh shape yet. A room, a bowl or a doorway is several bodies.
  • Run on the web. The solver is C++ behind a build hook, so it goes where a native toolchain goes.
  • Tie a loop in a scene file. A joint component holds its body to the one above it in the tree, so a chain fixed at both ends is joined by hand on scene.physics. There are no springs either: a joint holds or it breaks.