The simulation step
world.Step(timeStep, subStepCount) is where collision detection, constraint
solving and integration happen. Nothing else moves the simulation.
sequenceDiagram
participant App as Your game
participant World as PhysicsWorld
participant Box3D as Box3D (C)
App->>World: body.LinearVelocity = v
Note over World: finite check, 0.11 ns
World->>Box3D: b3Body_SetLinearVelocity
App->>World: Step(1/60)
World->>Box3D: b3World_Step
Note over Box3D: collide · solve · integrate<br/>buffers events internally
App->>World: Events.BodyMoves
World->>Box3D: b3World_GetBodyEvents
Box3D-->>World: pointer + count
Note over World: ref struct view,<br/>no copy, no allocation
World-->>App: only the bodies that moved
App->>World: RaycastClosest(...)
World->>Box3D: b3World_CastRayClosest
Box3D-->>App: RaycastHit
Keep the step fixed
A varying step makes the simulation irreproducible and hurts stability. Decouple it from the frame rate with an accumulator, and clamp the input so that one long frame does not spiral into a hundred catch-up steps:
const float FixedStep = 1.0f / 60.0f;
accumulator += MathF.Min(deltaTime, 0.25f);
while (accumulator >= FixedStep)
{
world.Step(FixedStep);
accumulator -= FixedStep;
}
Interpolate between the last two physics poses if you need to render at a higher rate than you simulate. Do not step by the frame time to get there.
Sub-steps
The second argument is how many solver sub-steps to take within the step. More is more accurate and more expensive; four is the usual choice and the default. Raise it for stacks that sag or joints that stretch, not for tunnelling — that is what continuous collision is for.
Sub-step count is part of the input to the simulation, so changing it changes the result. Keep it fixed for the same reason the time step is fixed.
Events are buffered, not raised
Box3D collects what happened during the step and hands it back afterwards instead of calling back mid-step. Two reasons: the solver is multithreaded, and applications usually want to change the world in response, which is unsafe while it is being solved.
The consequence for you is that events live between one step and the next, and creating or destroying bodies while reading them is fine.
Sleeping
A body that stops moving falls asleep and stops being simulated until something touches it. This is on by default and is the reason a settled scene of ten thousand bodies costs roughly what an empty one does.
world.SleepEnabled = false; // whole world; rarely what you want
body.CanSleep = false; // this body only
world.AwakeBodyCount // zero means everything has settled
Sleeping is a feature, not a compromise, but it does mean a benchmark over a settled scene measures the sleep check and nothing else. See Benchmarks.
Continuous collision
A body moving fast enough to pass through a wall within one step is a tunnelling problem, and the answer is not a smaller step.
// On by default. Leave it on; turning it off saves very little.
using var world = new PhysicsWorld(WorldSettings.Default with { EnableContinuous = true });
// Sweep this one against dynamic and kinematic bodies too.
Body shell = world.CreateBody(BodyDefinition.Dynamic(muzzle) with { IsBullet = true });
Use bullets sparingly. They are swept after everything else has moved, so they do not guarantee correct collision when both bodies move fast. For a projectile that must not miss, cast a ray along its path and place the hit yourself.
Sensors have no continuous collision at all.
Tuning a world
Most simulations only ever set Gravity and WorkerCount. The rest of
WorldSettings exists for when something specific
is wrong:
| Setting | Reach for it when |
|---|---|
RestitutionThreshold |
Slow contacts bounce when they should settle. Setting it very low prevents sleeping |
HitEventThreshold |
Too many, or too few, hit events |
MaximumLinearSpeed |
Something reaches an absurd speed and never comes back |
ContactSpeed |
Deep overlap is pushed apart explosively |
ContactHertz, ContactDampingRatio |
Bodies visibly sink into each other, or jitter. Advanced |
Settings are fixed when the world is created, except Gravity, SleepEnabled
and ContinuousEnabled, which are properties on the world itself.
What a step costs
The wrapper adds one P/Invoke to a step that takes tens of microseconds at the smallest useful scale, so its overhead is not measurable. The numbers are in Benchmarks.