Physics
Physics lives in its own repository,
ChxisB/orblit-physics, as three
packages.
orblit_physicsis 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_sceneconnects it to scene files. It simulates every entity that has abodycomponent and answers each step with aSceneDiff, so whatever draws a document draws the simulation too.orblit_physics_terrainlays a terrain in the world as ground, the regions near the camera, and lays them again when the terrain is edited.
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_terrainBecause 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.
A world you step
Section titled “A world you step”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.
Cylinders and hulls
Section titled “Cylinders and hulls”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.
dropHulltakes one away, and answers false while a body still uses it. - Some points lay nothing.
layHullanswers 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.
In a scene file
Section titled “In a scene file”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.physicstakes a negative number, as a body does, so the two never clash. - A snapshot keeps which hull each body is made of.
restorebrings the hulls back with it.
Bodies in a scene file
Section titled “Bodies in a scene file”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.
centreis 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.
planeis endless ground. It faces the entity’s own up and passes throughcentre. It never moves, whatevermotionsays.- 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.
Simulating a document
Section titled “Simulating a document”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.
Edits are teleports
Section titled “Edits are teleports”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.
Smooth display
Section titled “Smooth display”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');}Pushing, touching and casting
Section titled “Pushing, touching and casting”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.
Asking the world
Section titled “Asking the world”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.
layersandignore. The question is a body on those layers, so it meets what it cares about or what cares about it, as two bodies do.ignoreleaves 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.castAnystops 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.castAllandoverlaptake alimit, 32 by default. A fullcastAllkeeps the nearest, not the first it found.overlapgives 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.
Body controls
Section titled “Body controls”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, ), );}lockshold any of six ways a body can go:moveX,moveY,moveZ,turnX,turnYandturnZ, 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.gravityScaleis 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.maxSpeedandmaxSpincap 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.centreis where the weight is, in the body’s own frame. In a scene file it iscentreOfMass, becausecentrethere 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. Apushwith noatgoes through the centre of mass.inertiais 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.
The world’s gravity
Section titled “The world’s gravity”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.
Switching motion
Section titled “Switching motion”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.
Two bodies that ignore each other
Section titled “Two bodies that ignore each other”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');}ignorereturns 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
ScenePhysicsand 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.
Materials
Section titled “Materials”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.
Layer names
Section titled “Layer names”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.
Triggers, zones and belts
Section titled “Triggers, zones and belts”Triggers
Section titled “Triggers”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); }}enteredwhen a body comes in, andexitedwhen it leaves or when either one is removed.ais the trigger andbis the body, whichever number is smaller. This is the one pair that is not named smaller number first.atandnormalsay where a body came in, and are zero on exit.stayon the trigger or on the body addsinside, every step the body is still in. Either one asking is enough.- A body with
stayalso getstouchStayfor each solid contact that goes on, so a game can burn a foot on hot ground without counting steps itself. touchStaystops when the body goes to sleep, because a pair at rest has its touch ended, astouchEndedsays elsewhere. A trigger looks at where bodies are, not at what they touch, soinsidegoes 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
linearDampingnever 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. setZonereturns false, changing nothing, for a body that isn’t a trigger. Setting it again replaces the zone.removeZonetakes 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
surfaceon thebody, three numbers. - A crate that a belt is carrying doesn’t go to sleep. It is still being moved.
Changing one contact
Section titled “Changing one contact”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));}frictionandrestitutionreplace what the pair would have used.moveScaleAandmoveScaleBsay 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
eventsand 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. setRulereturns 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.
Joints
Section titled “Joints”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.
fixedwelds the two together.pointholds two points together and lets both bodies turn any way: a pendulum, or a ball and socket.hingeturns about the frame’s x axis only. A door, a wheel, a knee.slidermoves along x only, never turning. A drawer, a piston.distancekeeps 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, soJointLimit(0, 2)is a two-metre rope, slack until it is taut.conelets the second body’s x axis swing withinswingof the first’s, in any direction, and twist within a limit. A shoulder or a hip.sixAxissets 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 asb, it is the world’s angle as the door sees it, which turns the other way. jointStateOfreads 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 choosingbreakingForceorbreakingTorque. Past either, the joint breaks, and abrokeevent names it:ais the joint andbis 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
brokeevent, because nothing broke. Whatever they held wakes up and falls. - The two bodies don’t collide with each other unless
collidesays 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.
Joints in a scene file
Section titled “Joints in a scene file”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.
ScenePhysicshands the world radians. - An edit makes it again.
applymakes 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.brokenlists 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;}Characters
Section titled “Characters”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
stepHeightand stops at taller ones. - It stands on slopes up to
steepestand 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.
It does not fall by itself
Section titled “It does not fall by itself”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.
velocityis 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.groundedis 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.groundis the body underneath, or 0 for none, andnormalis which way that surface faces.carriedis how fast the ground moved it, andturningis how fast the ground turned it about up, in radians a second. These are kept apart fromvelocity, so a walk animation plays the walk it asked for and not the ride. A camera can addturningto turn with a turntable.
footingsOf reads a whole crowd in one trip, and gives null for anything
that isn’t a character.
Root motion
Section titled “Root motion”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.
In a scene
Section titled “In a scene”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);}Ground
Section titled “Ground”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
layGroundwith 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.
Terrain
Section titled “Terrain”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.
Going back, and seeing what happened
Section titled “Going back, and seeing what happened”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
disposewhen 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 aStateError. - 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 and counts
Section titled “Contacts and counts”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';}In a scene
Section titled “In a scene”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.
In the editor
Section titled “In the editor”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.
What it does not do
Section titled “What it does not do”- 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.
contactsgives 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.
