# onephysics docs, all pages (OnePhysics 0.4.0-preview.1)
Every page of https://recursiveai.org/onephysics/docs/ and https://recursiveai.org/onephysics/cookbook/ as markdown, getting started first. Index: https://recursiveai.org/onephysics/llms.txt
# Getting started with OnePhysics
Install the OnePhysics preview for .NET 10, drop a ball on the ground, walk a character, cast a ray, and take a snapshot and a world hash, with the same world state, bit for bit, on every platform.
## Install
OnePhysics targets `net10.0`, so you need the .NET 10 SDK. It is a preview: this guide uses 0.4.0-preview.1, and the API and results may change between previews.
```shell
dotnet new console -o OnePhysicsStart
cd OnePhysicsStart
dotnet add package RecursiveAI.OnePhysics --version 0.4.0-preview.1
```
Its only dependency is [OneMath](https://recursiveai.org/onemath/), installed with it, for vectors and deterministic math. It works under NativeAOT, trimming and browser WebAssembly.
The 2D engine is the supported surface. The 3D engine is in the same package as a preview of its own, marked `[Experimental("OPHYS0002")]`: see [the 3D preview](https://recursiveai.org/onephysics/cookbook/3d-preview/).
## A first world
```csharp
using RecursiveAI.OneMath; // Vector2F, Vector2D
using RecursiveAI.OnePhysics; // BodyType, events, exceptions
using RecursiveAI.OnePhysics.Physics2D; // PhysicsWorld2D, Body, Character
var world = new PhysicsWorld2D();
world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -1), type: BodyType.Static);
var ball = world.AddCircle(0.5f, at: new Vector2D(0, 5));
ball.OnCollisionEnter += c => Console.WriteLine($"hit {c.Other} along {c.Normal}");
var player = world.AddCharacter(at: new Vector2D(3, 1));
for (int i = 0; i < 600; i++)
{
player.Move(new Vector2F(0.01f, -0.1f), 1f / 60);
world.Step(1f / 60); // fixed dt; events fire after the step, in a deterministic order
}
Console.WriteLine($"ball at {ball.Position}, player at {player.Position}");
if (world.Raycast(new Vector2D(0, 10), new Vector2F(0, -1), out var hit)) Console.WriteLine($"ray hit {hit.Body} at {hit.Point}");
byte[] saved = world.Snapshot(); // byte-exact; Restore(saved) continues bit for bit
ulong hash = world.ComputeHash(); // the same on every platform
Console.WriteLine($"snapshot {saved.Length} bytes, hash {hash:X16}");
```
```text
hit Body(world 1, #0, gen 1) along (-0, -1)
ball at (0, -5.037127994000912E-05), player at (8.99999986588955, 0.3999999836087227)
ray hit Body(world 1, #1, gen 1) at (0, 0.49994962872006)
snapshot 1804 bytes, hash C1C403756B09A31E
```
## What each part does
- **`AddBox`, `AddCircle`** and the other one-liners create a body with one shape. Sizes are full extents in meters, positions are `double` and everything else is `float`. Bodies are dynamic unless you pass `BodyType.Static` or `Kinematic`.
- **`OnCollisionEnter`** fires after the step, in a fixed order, so handlers run the same way on every machine. The `Body` in the message is a small struct handle: world, slot and generation.
- **`AddCharacter`** adds a character mover: `Move(displacement, dt)` moves it and resolves its contacts. Here it walks 6 m to the right and stands on the ground.
- **`Step(dt)`** advances the world by a fixed time step. Use one `dt` for the whole session.
- **`Snapshot()`** writes the whole world to bytes; `Restore(bytes)` continues from there bit for bit. **`ComputeHash()`** is a 64-bit hash of the state, the same on every platform.
## Determinism
Only IEEE `+ - * /` and square root decide a result, with OneMath’s own transcendental functions, fixed-order reductions and constraint coloring. The world hash after N steps is the same on x64, arm64 and WebAssembly, scalar or SIMD, one thread or all cores. Previews are not bit-compatible with each other: a snapshot or replay from one preview doesn’t carry to the next. See [determinism](https://recursiveai.org/onephysics/docs/determinism/).
## Next
- [Rollback and resimulation](https://recursiveai.org/onephysics/cookbook/rollback/)
- [Events and sensors](https://recursiveai.org/onephysics/cookbook/events-and-sensors/)
- [Queries and layers](https://recursiveai.org/onephysics/cookbook/queries-and-layers/)
- [Coming from Unity](https://recursiveai.org/onephysics/docs/unity-mapping/)
# OnePhysics for .NET
Deterministic 2D rigid-body physics for .NET, written from first principles: the same world state, bit for bit, on every platform, CPU, vector width and thread count. Package `RecursiveAI.OnePhysics`, built on [onemath](https://github.com/recursive-ai-oss/onemath-csharp) for its deterministic math.
**Status: preview.** The 2D engine is the supported surface: circles, capsules, rounded polygons, segments and chains; revolute, prismatic, weld, wheel, distance, motor and mouse joints; bullets and continuous collision; a character mover; layers, filters, sensors, events and queries; byte-exact snapshot and restore; per-step world hashes; debug geometry. The 3D engine (`RecursiveAI.OnePhysics.Physics3D`) ships as a **preview marked `[Experimental("OPHYS0002")]`**: it works and is deterministic, but its API and its results may change between previews. The raw tier (`RecursiveAI.OnePhysics.LowLevel`) is `[Experimental("OPHYS0001")]`. Previews are not bit-compatible with each other: a snapshot or replay from one preview does not carry to the next.
## Install
```
dotnet add package RecursiveAI.OnePhysics --prerelease
```
Targets `net10.0`; works under NativeAOT, trimming and browser WebAssembly. Its only dependency is `RecursiveAI.OneMath`.
## 2D in a minute
```csharp
using System;
using RecursiveAI.OneMath; // Vector2F, Vector2D
using RecursiveAI.OnePhysics; // BodyType, events, exceptions
using RecursiveAI.OnePhysics.Physics2D; // PhysicsWorld2D, Body, Character
var world = new PhysicsWorld2D();
world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -1), type: BodyType.Static);
var ball = world.AddCircle(0.5f, at: new Vector2D(0, 5));
ball.OnCollisionEnter += c => Console.WriteLine($"hit {c.Other} along {c.Normal}");
var player = world.AddCharacter(at: new Vector2D(3, 1));
for (int i = 0; i < 600; i++)
{
player.Move(new Vector2F(0.05f, -0.1f), 1f / 60);
world.Step(1f / 60); // fixed dt; events fire after the step, in a deterministic order
}
if (world.Raycast(new Vector2D(0, 10), new Vector2F(0, -1), out var hit)) Console.WriteLine(hit.Point);
byte[] saved = world.Snapshot(); // byte-exact; Restore(saved) continues bit for bit
ulong hash = world.ComputeHash(); // the same on every platform
```
## 3D, a preview
The 3D world has the same members with 3D math types. Using it raises the experimental diagnostic `OPHYS0002` until you acknowledge it:
```csharp
#pragma warning disable OPHYS0002 // the 3D engine is a preview
using System;
using RecursiveAI.OneMath; // Vector3F, Vector3D
using RecursiveAI.OnePhysics; // BodyType, events, exceptions
using RecursiveAI.OnePhysics.Physics3D; // PhysicsWorld3D, Body, Character
var world = new PhysicsWorld3D();
world.AddBox(new Vector3F(20, 1, 20), at: new Vector3D(0, -1, 0), type: BodyType.Static);
var ball = world.AddSphere(0.5f, at: new Vector3D(0, 5, 0));
ball.OnCollisionEnter += c => Console.WriteLine($"hit {c.Other} along {c.Normal}");
for (int i = 0; i < 300; i++) world.Step(1f / 60);
if (world.Raycast(new Vector3D(0, 10, 0), new Vector3F(0, -1, 0), out var hit)) Console.WriteLine(hit.Point);
byte[] saved = world.Snapshot();
ulong hash = world.ComputeHash();
```
## What you get
- **Deterministic.** Only IEEE-exact `+ - * /` and `sqrt` and our own transcendental functions; fixed-order reductions and constraint coloring. The world hash after N steps is the same on x64, arm64 and WebAssembly, scalar or SIMD, one thread or all cores.
- **Three tiers, one engine.** One-liners (`world.AddBox(...)`, `body.OnCollisionEnter`), definitions and polled events, and a raw tier, each built on the one below.
- **Rollback-ready.** Byte-exact `Snapshot`/`TrySnapshot` and `Restore`, `ComputeHash` per step, `SuppressCallbacks` for resimulation, allocation-free steady state after `Warmup`.
- **Handles you can hold.** `Body`, `Shape` and `Joint` are small structs; a destroyed one throws a `StaleHandleException` naming it.
- **Hostile input is refused,** with typed errors that name the value and the limit.
## Guide
- [Install](https://recursiveai.org/onephysics/docs/install/) and [quick start](https://recursiveai.org/onephysics/docs/quick-start/)
- [The three tiers](https://recursiveai.org/onephysics/docs/tiers/)
- [Events](https://recursiveai.org/onephysics/docs/events/)
- [Queries, layers and filters](https://recursiveai.org/onephysics/docs/queries-and-layers/)
- [Rollback and lockstep](https://recursiveai.org/onephysics/docs/rollback/)
- [Determinism guarantees](https://recursiveai.org/onephysics/docs/determinism/)
- [Units and sizes](https://recursiveai.org/onephysics/docs/units/), [names and conventions](https://recursiveai.org/onephysics/docs/api-names/), [input limits](https://recursiveai.org/onephysics/docs/limits/)
- [Coming from Unity](https://recursiveai.org/onephysics/docs/unity-mapping/)
- [Port specifications](https://github.com/recursive-ai-oss/onephysics-csharp/tree/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec): the normative, language-neutral description of every choice that affects the bits.
- [Changelog](https://recursiveai.org/onephysics/docs/changelog/) and [contributing](https://github.com/recursive-ai-oss/onephysics-csharp/blob/fa8eed5a2021c452ca8051b6099917e09538b4f4/CONTRIBUTING.md)
## Licence
`MIT OR Apache-2.0`, at your option. Copyright (c) Recursive AI, LLC. See `LICENSE-MIT` and `LICENSE-APACHE`.
# Install
```
dotnet add package RecursiveAI.OnePhysics --prerelease
```
The package id is `RecursiveAI.OnePhysics`. Its only dependency is `RecursiveAI.OneMath` (the deterministic vectors and functions the API is written in: `Vector2F`, `Vector2D`, `Vector3F`, `Vector3D`), which NuGet restores for you.
- **Target:** `net10.0`.
- **NativeAOT and trimming:** the package is marked AOT-compatible and trimmable and has no reflection over your types. The package is one assembly for both engines; trimming keeps what you reference, but a 2D-only app shedding the whole 3D engine is a goal, not yet a guarantee.
- **Browser WebAssembly:** supported, single-threaded (leave `ThreadCount` at its default of 1 there). A world gives the same bits in the browser as on a desktop.
- **Platforms:** results are bit-identical on Windows, Linux, macOS, x64 and arm64.
## Preview markers
Two parts of the package carry `[Experimental]` and fail to compile until you opt in, so you cannot use them by accident:
| Part | Diagnostic | Opt in |
|---|---|---|
| The 3D engine, namespace `RecursiveAI.OnePhysics.Physics3D` | `OPHYS0002` | `$(NoWarn);OPHYS0002` in the project, or `#pragma warning disable OPHYS0002` in a file |
| The raw tier, namespace `RecursiveAI.OnePhysics.LowLevel` | `OPHYS0001` | the same, with `OPHYS0001` |
Both may change between previews. **0.x previews are not bit-compatible with each other**: do not carry snapshots, saved games or replays across a package update (the [changelog](https://recursiveai.org/onephysics/docs/changelog/) says which previews moved results).
## Next
[Quick start](https://recursiveai.org/onephysics/docs/quick-start/).
# Quick start
The namespaces you import: `RecursiveAI.OneMath` for the vector types, `RecursiveAI.OnePhysics` for what both dimensions share (`BodyType`, `EventKind`, `EventMask`, exceptions, raw handles), and `RecursiveAI.OnePhysics.Physics2D` (or `Physics3D`) for the world, `Body`, `Shape`, `Joint`, `Character` and the definitions.
## A world, some bodies, a step loop
```csharp
var world = new PhysicsWorld2D();
var ground = world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
var crate = world.AddBox(new Vector2F(1, 1), at: new Vector2D(0, 3), material: new Material2D(friction: 0.8f));
var ball = world.AddCircle(0.5f, at: new Vector2D(2, 4));
ball.LinearVelocity = new Vector2F(-1, 0);
crate.OnCollisionEnter += c => Console.WriteLine($"crate hit {c.Other}");
for (int i = 0; i < 120; i++) world.Step(1f / 60);
Console.WriteLine($"{crate.Position} {crate.LinearVelocity} awake={crate.IsAwake}");
```
- Positions are `double` (`Vector2D`); sizes, velocities and everything else are `float` (`Vector2F`).
- `AddBox(size)` takes the **full** size. See [units](https://recursiveai.org/onephysics/docs/units/).
- `Body` is a small struct. Copy it freely; it allocates nothing. After `body.Destroy()` any use throws `StaleHandleException`.
- `Step` takes a fixed dt. The world has no clock; always pass the same value (see [determinism](https://recursiveai.org/onephysics/docs/determinism/)). After the step, handlers run and the events of the step are available to poll ([events](https://recursiveai.org/onephysics/docs/events/)).
## A character and a ray
```csharp
var player = world.AddCharacter(at: new Vector2D(-3, 1));
player.Move(new Vector2F(0.05f, -0.1f), 1f / 60); // this frame's displacement, not a velocity
world.Step(1f / 60);
bool grounded = player.IsGrounded;
if (world.Raycast(new Vector2D(0, 10), new Vector2F(0, -1), out var hit)) Console.WriteLine($"{hit.Body} at {hit.Point}, {hit.Distance} away");
```
`Move` takes the displacement for this frame (use `MoveVelocity` for a velocity times dt) and slides along slopes, climbs steps and pushes bodies; `CharacterDef.Humanoid`, `Child` and `Giant` are presets, and `AddCharacter(at, def)` takes your own tuned copy.
## 3D, a preview
The 3D engine is `[Experimental("OPHYS0002")]`; acknowledge it with `#pragma warning disable OPHYS0002` or a `NoWarn`. The world has the same members with 3D math types.
```csharp
var world = new PhysicsWorld3D();
world.AddBox(new Vector3F(20, 1, 20), at: new Vector3D(0, -0.5, 0), type: BodyType.Static);
var crate = world.AddBox(new Vector3F(1, 1, 1), at: new Vector3D(0, 3, 0));
var ball = world.AddSphere(0.5f, at: new Vector3D(2, 4, 0));
for (int i = 0; i < 120; i++) world.Step(1f / 60);
Console.WriteLine($"{crate.Position} {ball.Position}");
```
To use both dimensions in one file, import neither dimension namespace and alias what you use: `using Body2D = RecursiveAI.OnePhysics.Physics2D.Body;` and `using Body3D = RecursiveAI.OnePhysics.Physics3D.Body;`.
## Next
[The three tiers](https://recursiveai.org/onephysics/docs/tiers/) for definitions, filters and polled events; [rollback](https://recursiveai.org/onephysics/docs/rollback/) if your host resimulates.
# The three tiers
Each tier is built on the one below, so you can start at the top and drop down for one object without rewriting the rest.
## 1. The happy path
`new PhysicsWorld2D()`, `world.AddBox(size, at: ...)`, `body.OnCollisionEnter += ...`, `world.Raycast(...)`, `world.AddCharacter(...)`, `world.Step(dt)`. Unity's vocabulary, full sizes, radians, sensible defaults (gravity -9.81, four substeps, friction 0.6, sleeping on). Nothing here asks you to implement an interface or write a generic struct. See the [quick start](https://recursiveai.org/onephysics/docs/quick-start/).
Filtering on this tier is a layer matrix (`body.Layer`, `world.IgnoreLayerCollision(a, b)`) plus an optional delegate (`WorldOptions2D.PairFilter`, a `PairPredicate`) and query filters with an optional predicate. See [queries, layers and filters](https://recursiveai.org/onephysics/docs/queries-and-layers/).
## 2. The middle tier
Definitions with defaults, polled events, a reusable query object, struct callbacks and solver control.
- **`BodyDef`, `ShapeDef` and the joint definitions** are valid at their zero value and then mean their documented defaults (`new ShapeDef { Friction = 0.9f }` changes only friction). They are structs; fill one, then pass it to `CreateBody`, `CreateBox` and the other `Create*` methods, or `CreateJoint(ref ...)`. The `Add*` one-liners are thin wrappers over these.
- **`WorldOptions2D`** (`WorldOptions3D` in 3D): gravity, `Substeps`, `VelocityIterations`, `RelaxIterations`, sleep settings, the event mask, `ThreadCount`, `WorldId` and capacity options. Substep and iteration counts are inputs of the simulation, never chosen from the clock or the machine.
- **`GetEvents(kind)` and `Events`** return the events of the last step as a span, with no allocation.
- **`PhysicsQuery2D`/`PhysicsQuery3D`** hold a world and a filter and add batch forms that run many queries in parallel.
- **`SetContactFilter(T filter) where T : struct, IContactFilter`** decides per shape pair, specialized for your struct.
```csharp
struct NotTheSameBody : IContactFilter
{
public bool ShouldCollide(Shape a, Shape b) => a.Body != b.Body;
}
```
```csharp
var world = new PhysicsWorld2D(new WorldOptions2D
{
Gravity = new Vector2F(0, -9.81f),
Substeps = 4, // substeps per Step call
VelocityIterations = 1, // solve passes with bias per substep
RelaxIterations = 1, // relax passes without bias per substep
});
var floor = world.CreateBody(new BodyDef { Type = BodyType.Static, Position = new Vector2D(0, -0.5) });
world.CreateBox(floor, new Vector2F(20, 1), def: new ShapeDef { Friction = 0.9f, Layer = 1 });
var box = world.CreateBody(new BodyDef { Position = new Vector2D(0, 2), Angle = 0.3f });
world.CreateBox(box, new Vector2F(1, 1), def: new ShapeDef { Density = 2f, Restitution = 0.1f, Layer = 2 });
world.IgnoreLayerCollision(2, 3);
world.SetContactFilter(new NotTheSameBody());
for (int i = 0; i < 60; i++)
{
world.Step(1f / 60);
foreach (var e in world.GetEvents(EventKind.ContactBegin))
{
Console.WriteLine($"{world.GetBody(e.BodyA)} touched {world.GetBody(e.BodyB)}");
}
}
var query = new PhysicsQuery2D(world) { Filter = QueryFilter2D.OnLayer(1) };
var hits = new QueryHit2D[16];
int n = query.RaycastAll(new Vector2D(-5, -0.25), new Vector2F(1, 0), hits);
```
The contact filter is not saved in snapshots: install it again after `Restore`.
Joints follow the same pattern: the one-liner (`AddHinge`) or a definition (`RevoluteJointDef`) returns a typed view (`HingeJoint`) that converts implicitly to `Joint`. A member another kind of joint has does not compile on the view. See [names and conventions](https://recursiveai.org/onephysics/docs/api-names/) for the table.
## 3. The raw tier
`RecursiveAI.OnePhysics.LowLevel`: struct-of-arrays body and constraint storage, the constraint coloring and scheduler, broadphase trees, narrowphase and manifold routines, snapshot readers and writers. It is `[Experimental("OPHYS0001")]`: using it needs `$(NoWarn);OPHYS0001`, and it may change between previews. Use it when you build your own pipeline; nothing on tiers 1 and 2 requires it. The behaviour it implements is written down in [../spec](https://github.com/recursive-ai-oss/onephysics-csharp/tree/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec).
## Namespaces
- `RecursiveAI.OnePhysics`: what game code names in both dimensions: `BodyType`, `EventKind`, `EventMask`, `PhysicsEvent`, `OptionChanges`, the raw handles (`BodyHandle`, `ShapeHandle`, `JointHandle`) and the exceptions.
- `RecursiveAI.OnePhysics.Physics2D` and `Physics3D`: the world, `Body`, `Shape`, `Joint`, `Character`, definitions, queries and options. Both use the same member names.
- `RecursiveAI.OnePhysics.Debug`: debug geometry (shapes, contacts, joints, islands, broadphase bounds) as plain data for you to draw.
- `RecursiveAI.OnePhysics.LowLevel`: the raw tier.
# Events
A step produces events: contacts beginning and ending, sensor overlaps, sleeping and waking, joint breaks. They are **collected during the step and delivered after it**, in a fixed order that does not depend on threads or platform. You can consume them two ways.
## Per-body handlers
Subscribe on the body. Handlers run at the end of `Step`, after the simulation has finished.
```csharp
var ball = world.AddCircle(0.5f, at: new Vector2D(0, 3));
ball.OnCollisionEnter += c => enters++;
ball.OnCollisionStay += c => stays++;
ball.OnCollisionHit += c => { hitCount++; Console.WriteLine($"landed at {c.ApproachSpeed} m/s"); };
```
`Body` has `OnCollisionEnter`, `OnCollisionStay`, `OnCollisionExit`, `OnCollisionHit`, `OnTriggerEnter`, `OnTriggerExit`, `OnSleep` and `OnWake`. The `Collision` argument has `Body`, `Other`, `Shape`, `OtherShape`, `Normal`, `Point`/`GetPoint(i)`, `NormalImpulse` and `ApproachSpeed`. Sensors report through `OnTriggerEnter`/`OnTriggerExit` with a `Trigger`:
```csharp
var zone = world.CreateBody(new BodyDef { Type = BodyType.Static, Position = new Vector2D(0, 1.5) });
world.CreateBox(zone, new Vector2F(4, 1), def: new ShapeDef { IsSensor = true });
zone.OnTriggerEnter += t => { triggers++; Console.WriteLine($"{t.Other} entered the zone"); };
```
## Polled events
`world.Events` is every event of the last step; `world.GetEvents(kind)` is the slice of one kind. Both are spans, valid until the next `Step`, and allocate nothing.
```csharp
world.Step(1f / 60);
polled += world.GetEvents(EventKind.ContactBegin).Length;
foreach (var e in world.Events)
{
if (e.Kind == EventKind.Sleep) Console.WriteLine($"{world.GetBody(e.BodyA)} fell asleep");
}
```
A `PhysicsEvent` carries raw handles (`BodyA`, `BodyB`, `ShapeA`, `ShapeB`, `Joint`) and a `Value` (the break value of a joint, the approach speed of a hit). Turn a raw handle into a live `Body` with `world.GetBody(handle)`; `world.IsBodyAlive(handle)` tells you whether it still exists without throwing. Prefer polling when you roll back (below) or want events in your own order.
## Which events exist: `EventMask`
`WorldOptions2D.Events` chooses what the world records; a disabled kind costs nothing.
| Kind | In `EventMask.Default` | Notes |
|---|---|---|
| `ContactBegin`, `ContactEnd` | yes | |
| `SensorEnter`, `SensorExit` | yes | |
| `JointBreak` | yes | |
| `Sleep`, `Wake` | yes | |
| `ContactPersist` | **no** | one event per touching pair per step; `OnCollisionStay` needs it |
| `ContactHit` | **no** | a contact that approached faster than the world's hit threshold; `OnCollisionHit` needs it |
```csharp
var world = new PhysicsWorld2D(new WorldOptions2D
{
Events = EventMask.Default | EventMask.ContactPersist | EventMask.ContactHit,
});
```
## Order
Events are sorted after the step on every field: first by kind (`ContactBegin`, `ContactPersist`, `ContactEnd`, `SensorEnter`, `SensorExit`, `JointBreak`, `Sleep`, `Wake`, `ContactHit`), then by the bodies' and shapes' slots, so the same inputs give the same list on every platform and thread count. Handlers run in that same order, first for body A of a pair and then body B. The precise sort keys are in [../spec/world2d.md](https://github.com/recursive-ai-oss/onephysics-csharp/blob/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec/world2d.md).
## Rollback
After `Restore`, handlers stay attached to their handles, and resimulating would call them a second time. Wrap resimulation in `world.SuppressCallbacks()`; polled events are still produced. See [rollback](https://recursiveai.org/onephysics/docs/rollback/).
# Queries, layers and filters
## Layers
Every shape has a layer, 0 to 31 (`ShapeDef.Layer`; `body.Layer` reads the first shape's layer and sets all of them, as Unity's per-object layer does). The world has a 32 x 32 matrix saying which layers collide; by default all do.
```csharp
const int Ground = 1, Player = 2, Enemy = 3;
var world = new PhysicsWorld2D();
var ground = world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
var player = world.AddCircle(0.5f, at: new Vector2D(-2, 1));
var enemy = world.AddCircle(0.5f, at: new Vector2D(2, 1));
ground.Layer = Ground;
player.Layer = Player;
enemy.Layer = Enemy;
world.IgnoreLayerCollision(Player, Enemy); // layers 2 and 3 pass through each other
bool collide = world.GetLayerCollision(Player, Ground);
```
## Deciding whether two shapes collide
The first rule that answers decides (the full text is in [../spec/world2d.md](https://github.com/recursive-ai-oss/onephysics-csharp/blob/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec/world2d.md)):
1. Two sensors never collide.
2. The layer matrix.
3. The group index: shapes with the same non-zero `GroupIndex` collide only if it is positive.
4. Category and mask bits: `(maskA & categoryB) != 0` and `(categoryA & maskB) != 0`.
5. The optional `WorldOptions2D.PairFilter` delegate (happy path) and then the optional struct `IContactFilter` from `SetContactFilter` (middle tier), called in a fixed order.
Changing a layer, mask, group or the matrix, or installing a filter, re-evaluates the existing pairs at the next step: contacts that are no longer allowed end (with an event). Call `RefilterPairs()` when your delegate's answer changes without any of those. A filter must depend only on the two shapes' state, never on the clock or on other mutable data, or two peers will disagree.
## Queries
Queries read the world; they never change it, so querying or not does not change the world hash. Each kind has a nearest-hit form, an `...Any` form (stops at the first hit, cheapest), and an `...All` form that fills your span or returns an array.
| Query | Methods |
|---|---|
| Ray | `Raycast`, `RaycastAny`, `RaycastAll` |
| Swept shape | `CircleCast`, `BoxCast`, `CapsuleCast`, `ShapeCast` (each with `Any`/`All`) |
| Overlap | `OverlapPoint`, `OverlapCircle`, `OverlapBox`, `OverlapShape` (each with `Any`/`All`) |
| Nearest point | `ClosestPoint` (with `Any`/`All`) |
Every query takes an optional `QueryFilter2D`: `LayerMask`, `CategoryBits`/`MaskBits`, `IncludeSensors` (default false) and a `Predicate` (`Func`). `QueryFilter2D.OnLayer(n)` and `QueryFilter2D.Layers(mask)` cover the common cases.
```csharp
var enemiesOnly = QueryFilter2D.OnLayer(Enemy);
var notThePlayer = new QueryFilter2D { Predicate = s => s.Body != player };
var around = world.OverlapCircleAll(new Vector2D(2, 1), 1.5f, enemiesOnly);
bool cast = world.CircleCast(new Vector2D(-5, 1), 0.25f, new Vector2F(1, 0), out var hit, 100f, notThePlayer);
if (cast) Console.WriteLine($"first thing in the way: {hit.Body} after {hit.Distance} m");
```
A `QueryHit2D` has `Body`, `Shape`, `Point`, `Normal`, `Fraction` along the cast and `Distance`. Positions are `double`, directions and distances `float`. Pass a span or array to the `...All` forms with a `Span` overload and a steady-state query allocates nothing; the array-returning forms allocate the result.
For many queries at once, `PhysicsQuery2D` (see [the tiers](https://recursiveai.org/onephysics/docs/tiers/)) has batch forms (`RaycastBatch`, `CircleCastBatch`, `OverlapCircleCountBatch`) that run in parallel on the world's workers with results in input order. Do not query while the world is stepping.
# Units and sizes
- **Meters, kilograms, seconds.** Positions cross the API as `double`; everything else is `float`. Gravity defaults to (0, -9.81); density is kg/m^2 in 2D and kg/m^3 in 3D.
- **Full sizes.** `AddBox(size)`, `CreateBox(body, size, ...)` and the box queries (`OverlapBox`, `BoxCast`) take the full width and height (and depth). A parameter that is a half extent says so in its name: `halfLength` (capsules), `halfHeight` (cylinders), `ShapeHalfSize` (ragdoll parts), `halfAngle` (cones). A radius is a radius.
- **Radians.** Every angle, limit, motor speed and angular velocity is in radians (per second); Unity's degrees need converting. Parameters that are angles in a method with other numbers say so: `angleRadians` on creators, queries and `SetTransform`. `CharacterDef`'s slope limit is in radians too (`SlopeLimitRadians`).
- **Counter-clockwise.** In 2D a positive angle rotates counter-clockwise, with +y up (gravity is negative y).
- **Definitions have zero-value defaults.** `default(BodyDef)` is a dynamic body; `default(ShapeDef)` has density 1 and friction 0.6. You never need to remember to fill a field in to get the documented default.
- **Mass.** Mass comes from shapes: `density x area` (2D) or `x volume` (3D). A shape that would take a body's mass or inertia outside 1e-18..1e30 is refused with a typed error naming the value; see [input limits](https://recursiveai.org/onephysics/docs/limits/).
The full list of conventions, including handles and joint names, is in [names and conventions](https://recursiveai.org/onephysics/docs/api-names/).
# Input limits
Inputs outside these limits are refused with a typed error that names the value and the limit; nothing is changed when an operation refuses.
- **Mass properties.** A dynamic body's total mass, and in 2D its inertia (when not zero) or in 3D each diagonal entry of its inertia tensor, must lie within 1e-18..1e30, and the 3D tensor must be invertible. A shape that would take a body outside that range is refused with `ArgumentOutOfRangeException` and the body keeps its previous mass properties. Removing a shape never throws; a body left with unusable properties falls back to unit mass and inertia.
- **Convex shape thickness.** A box, cylinder, capsule or tapered capsule must be at least 2^-20 as thick as it is long (`ConvexShape3D.MaxAspectRatio`): the cast and distance routines work in float32 with tolerances relative to the largest coordinate they see.
- **Material coefficients.** Friction and rolling resistance are within 0..1e15 (`MaterialLimits.MaxCoefficient`), so the geometric mean of two materials cannot overflow.
- **Fluids.** `SurfaceOffset` divided by the length of `SurfaceNormal` must stay within the coordinate range.
- **Diverged bodies.** `Step` refuses (`BodyOutOfBoundsException`, world unchanged) when a moving dynamic body has a non-finite position, velocity or rotation, as well as when its bounds leave the supported range.
- **Origin shifts** check every shape's shifted bounds first and refuse with `BodyOutOfBoundsException` before changing anything.
- **Rays.** A ray whose whole length is below 1e-6 is tested against the shape's padded bounds first, and a hit whose ray point lies outside those bounds is a miss, so a ray cast and a compound's bounds-pruned cast agree.
# Names and conventions in the public API
One place for the choices a host has to know. The two worlds (`PhysicsWorld2D`, `PhysicsWorld3D`) share every name here; only the math types differ.
## Units and sizes
- **Full sizes.** Every `Add*` one-liner and every `CreateBox` takes full extents (`AddBox(size)`, `CreateBox(body, size, ...)`, `ShapeSpec3D.Box(size)`). A parameter that is a half extent says so in its name: `halfLength` (capsule, both dimensions), `halfHeight` (cylinder), `RagdollPartDef.ShapeHalfSize`, and the cone's `halfAngle`.
- **Radians everywhere.** All angles, limits, motor speeds and rates are radians (per second). A parameter that is an angle in a method that also takes other numbers is named for it: `angleRadians` on the 2D creators, queries and `SetTransform`. `CharacterDef.SlopeLimitRadians` and `Character.SlopeLimitRadians` are radians; Unity's degrees need converting.
- **Meters, kilograms, seconds.** Positions cross the boundary as `double`; everything else is `float`.
## Handles
- `Body`, `Shape`, `Joint` (and its typed views), `Character`, and in 3D `Ragdoll` and `Vehicle` are small structs (world, index, generation). They allocate nothing and throw `StaleHandleException` naming the object once it is destroyed.
- The raw, serializable form of each is its `RawHandle` (`BodyHandle`, `ShapeHandle`, `JointHandle`, `CharacterHandle`, `RagdollHandle`, `VehicleHandle`). The world turns one back into a live struct with `GetBody`, `GetCharacter`, `GetRagdoll`, `GetVehicle` and answers `IsBodyAlive`, `IsCharacterAlive`, ... without throwing.
- Event and query results (`Collision`, `Trigger`, `QueryHit2D/3D`, `ControllerColliderHit`, `JointBreak`) expose their bodies, shapes and joints as readonly **fields**, so `hit.Other.LinearVelocity = v` compiles. `Body.SetLinearVelocity`, `SetAngularVelocity` and `SetPosition` do the same where a property cannot be assigned (a list element, a ref return).
- Both dimensions use the same type names. To use both in one file, alias the ones you need: `using Body3D = RecursiveAI.OnePhysics.Physics3D.Body;`.
## Joints: one-liner, definition, view
The `Add*` one-liners use Unity's vocabulary; the definitions (`CreateJoint(ref XDef)`) use the engine names. Both return the same typed view, which converts implicitly to `Joint`. Members of the general `Joint` that do not apply to its kind throw `InvalidJointException` naming the kind.
| One-liner (2D) | Definition | Typed view |
|---|---|---|
| `AddHinge` | `RevoluteJointDef` | `HingeJoint` |
| `AddSlider` | `PrismaticJointDef` | `SliderJoint` |
| `AddFixed` | `WeldJointDef` | `FixedJoint` |
| `AddWheel` | `WheelJointDef` | `WheelJoint` |
| `AddDistance`, `AddSpring` | `DistanceJointDef` | `DistanceJoint` |
| `AddTarget` | `MouseJointDef` | `TargetJoint` |
| `AddRelative` | `MotorJointDef` | `RelativeJoint` |
| One-liner (3D) | Definition | Typed view |
|---|---|---|
| `AddFixed` | `FixedJointDef` | `FixedJoint` |
| `AddPoint` | `PointJointDef` | `PointJoint` |
| `AddDistance`, `AddSpring` | `DistanceJointDef` | `DistanceJoint` |
| `AddHinge` | `HingeJointDef` | `HingeJoint` |
| `AddSlider` | `SliderJointDef` | `SliderJoint` |
| `AddCone` | `ConeJointDef` | `ConeJoint` |
| `AddCharacterJoint` | `SwingTwistJointDef` | `SwingTwistJoint` |
| `AddConfigurable` | `SixDofJointDef` | `SixDofJoint` |
| `AddPath` | `PathJointDef` | `PathJoint` |
| `AddGear` | `GearJointDef` | `GearJoint` |
| `AddRackAndPinion` | `RackAndPinionJointDef` | `RackAndPinionJoint` |
| `AddPulley` | `PulleyJointDef` | `PulleyJoint` |
Member names are the definition's names on the handle:
| Definition field | Joint member |
|---|---|
| `EnableLimit`, `EnableMotor`, `EnableSpring` | the same, read and write |
| `LowerAngle`, `UpperAngle` (revolute) | the same |
| `LowerTranslation`, `UpperTranslation` (prismatic, wheel) | the same |
| `MinLength`, `MaxLength` (distance) | the same |
| `MaxMotorTorque` (rotational motors), `MaxMotorForce` (linear motors) | the same; asking a rotational joint for a force, or the reverse, throws and names the right member |
## Characters
Presets are on the definition: `CharacterDef.Humanoid`, `Child`, `Giant` (and in 3D `RigidHumanoid`). Take one, change fields, pass it to `AddCharacter`.
## Callbacks and rollback
A body's `OnCollision*`, `OnTrigger*`, `OnSleep` and `OnWake` handlers belong to its handle, so they survive a `Restore` that brings the same handle back; the world keeps the handlers of the 1024 most recently destroyed bodies, joints and characters of each kind, and only once it has taken a snapshot (a world that never snapshots drops them at destroy), so anything older is released.
Rollback hosts should read polled events (`Events`, `GetEvents`) and resimulate inside `using (world.SuppressCallbacks())`: no handler fires there, polled events are still produced, and the scope allocates nothing and nests. `Restore` returns an `OptionChanges` value naming the construction options the snapshot replaced (`None` when it changed none).
# Rollback and lockstep
A rollback host runs the same fixed-dt simulation on every peer, keeps a ring of recent snapshots, and when a late input arrives restores an old frame and resimulates. The world is built for it: snapshots are byte-exact, restores are atomic, and a world hash per step lets peers verify they agree.
## The loop
```csharp
const float dt = 1f / 60; // one fixed dt for the whole session
var world = new PhysicsWorld2D(new WorldOptions2D
{
WorldId = 1, // handles stay valid across worlds built with the same id
PairCapacity = 256, ContactCapacity = 256, IslandCapacity = 64,
});
world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
var ball = world.AddCircle(0.5f, at: new Vector2D(0, 4));
int sounds = 0;
ball.OnCollisionEnter += c => sounds++;
world.Warmup(); // after the bodies exist: steps, snapshots and restores no longer allocate
const int Ring = 8;
var ring = new byte[Ring][];
var lengths = new int[Ring];
var hashes = new ulong[Ring];
for (int i = 0; i < Ring; i++) ring[i] = new byte[world.GetSnapshotSizeBound()]; // never too small while the capacities hold
int frame = 0;
void Advance()
{
int slot = frame % Ring;
if (!world.TrySnapshot(ring[slot], out lengths[slot])) throw new InvalidOperationException("snapshot buffer too small");
world.Step(dt);
hashes[slot] = world.ComputeHash(); // exchange these with peers to catch a desync
frame++;
}
```
```csharp
int back = 5; // a late input for frame 95 arrived
int target = frame - back;
OptionChanges changed = world.Restore(ring[target % Ring].AsSpan(0, lengths[target % Ring]));
frame = target;
using (world.SuppressCallbacks()) // no sound twice; polled events still work
{
for (int i = 0; i < back; i++) Advance();
}
```
Apply the late input to your game state (and set body velocities or call `Move` before the step) between `Restore` and the resimulation steps, as your game requires; the world itself only needs the same dt and the same inputs to produce the same bits.
## The pieces
- **`TrySnapshot(span, out written)`** writes into your buffer and returns false if it is too small; `GetSnapshotSize()` is the current size. `Snapshot()` allocates a new array. The same world writes the same bytes on every platform. Snapshots hold contact caches, warm-start impulses, sleep state and the events of the last step, which is why a restored world continues bit for bit.
- **`GetSnapshotSizeBound()`** is an upper bound on `GetSnapshotSize()` for sizing rollback buffers once: it holds at every step of a world whose storage does not grow, including the step a pile first makes contact (the size can jump by more than half then, because contacts are saved). Set `ContactCapacity` and `PairCapacity` (or call `Warmup()`) after building the scene, read the bound, and allocate that. It is fixed while you only step; it changes when you create or destroy bodies, shapes, joints, characters and the like, change the event mask, or exceed a capacity (the arrays then double). It exceeds the real size by the unused capacity, and it assumes at most one round of sleep and wake calls between two steps. It allocates nothing and costs a pass over the bodies' shapes and the constraint slots, not over contacts.
- **`Restore(bytes)`** validates the whole snapshot first and changes nothing if it is invalid (`SnapshotFormatException`). It returns an **`OptionChanges`** flags value naming which saved options (gravity, substeps, iterations, sleep settings, event mask) the snapshot replaced in this world, or `None`. Options that are not part of the saved state (`ThreadCount`, capacities, solver tuning, the pair filter delegate and struct filter) are left alone: install a struct contact filter again after a restore.
- **`WorldId` and handle validity.** A handle carries the id of the world that made it. Handles taken before a snapshot stay valid in a world with the same `WorldId` that holds the same bodies, so you can keep `Body` values across a restore, or restore into a freshly built world (a new peer joining). With the default `WorldId` of 0 the world takes the next number from a process-wide counter, which depends on creation order; set it explicitly when you need stable ids.
- **Handlers across a restore.** Per-body handlers belong to the handle. A restore that brings a body back keeps its handlers. The world remembers the handlers of the 1024 most recently destroyed bodies, joints and characters of each kind, and only once it has taken a snapshot; anything older is released, so rolling back past more than 1024 destroys of one kind loses those handlers.
- **`SuppressCallbacks()`** returns a scope that stops per-body handlers firing while it is open; polled events (`Events`, `GetEvents`) are still produced. It allocates nothing and nests. Use it for the frames you replay, so sounds and particles are not triggered twice. Or avoid handlers in rollback code and read polled events.
- **`ComputeHash()`** is a 64-bit hash over the canonical state in a documented order (see [../spec/architecture.md](https://github.com/recursive-ai-oss/onephysics-csharp/blob/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec/architecture.md)): equal worlds give equal hashes on every platform. It does not cover handlers, the events of the last step, or the snapshot header.
- **Zero allocation.** `Warmup()` sizes storage to `PairCapacity`, `ContactCapacity` and `IslandCapacity` (derived from the body count where they are 0), starts the worker threads and exercises the hash, snapshot and restore paths once, so steady-state steps, queries, `ComputeHash`, `TrySnapshot` and `Restore` allocate nothing while the world stays inside those counts. Call it after creating the bodies, from the thread that will query. The first step after creating many new bodies may still grow storage; raise the capacities if you create bodies mid-session.
## Lockstep without rollback
The same pieces work without a ring: step every peer with the same inputs and dt, exchange `ComputeHash()` every N steps, and on a mismatch send a snapshot from the authority to the peer and `Restore` it. A mismatch is a bug in your inputs or in the engine, never a platform difference (see [determinism](https://recursiveai.org/onephysics/docs/determinism/)).
# Determinism guarantees
**The promise:** a world's state after N steps depends only on its inputs (the options, the calls you made, in order, and the dt you passed) and on the package version. It does not depend on the operating system, the CPU, the SIMD width, the runtime version or JIT tier, native versus NativeAOT versus browser WebAssembly, or the thread count.
## How, in short
- Only IEEE-exact `+ - * /` and `sqrt` on `float` and `double`, in a fixed order; no fused multiply-add and no estimate instructions. Every transcendental function comes from onemath, which returns the correctly rounded result.
- Reductions and parallel work combine in a fixed order. Constraints are colored and batched from the constraint graph alone, so one thread, all cores and single-threaded WebAssembly give the same bits. Wide SIMD kernels compute, lane by lane, exactly what the scalar code computes.
- Ties break on stable indices (body, shape, constraint, feature id, insertion order), never on hash codes or the order of a dictionary.
- The world has no clock. Step and iteration counts come from options; `dt` is yours.
The repository's hash-matrix tool steps its scenes and compares the per-step world hash across x64 and arm64, scalar and every vector width, one thread and all cores, JIT and NativeAOT, and WebAssembly. [../spec/hash-matrix.md](https://github.com/recursive-ai-oss/onephysics-csharp/blob/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec/hash-matrix.md) describes it and [../spec/architecture.md](https://github.com/recursive-ai-oss/onephysics-csharp/blob/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec/architecture.md) defines the hash and the snapshot layout.
## What is not promised
- **Between previews.** 0.x previews are not bit-compatible with each other. A snapshot, saved game or replay from one preview does not carry to another, and a world stepped on a newer preview may differ from one stepped on an older. The [changelog](https://recursiveai.org/onephysics/docs/changelog/) lists the previews that moved results. The 1.0 contract is the one that will be kept across versions.
- **Different inputs.** Two worlds fed the same calls in a different order are different worlds: creation order decides handles and slots, and slots decide event order. Different `dt`, substeps, iterations or gravity are different simulations. `ThreadCount` and `WorldId` are the exceptions: they never change a result.
- **Your code.** The world is deterministic; your game around it must be too. Do not feed it `Math.Sin` results, wall-clock times or a hash-ordered loop, and use the same `dt` on every peer. onemath provides the deterministic functions (`RecursiveAI.OneMath.Math64` and friends).
- **Single writer.** Call a world from one thread at a time. Queries only read, and single queries may run concurrently when nothing writes.
## The coordinate rule
Body positions cross the public API as `double` (`Vector2D`, `Vector3D`). Inside the engine they are stored as `double` relative to a world origin; rotations, velocities and solver math are `float`.
- **Exact within ±2^21.** A host that keeps positions in Q31.32 fixed point can pass any such value: it converts to `double` exactly for |v| < 2^21 (about 2.1 million units), and the engine stores it exactly. A body created beyond ±2^21 is rejected with an `ArgumentOutOfRangeException` naming the value and the bound.
- **Larger worlds: `ShiftOrigin`.** Call `world.ShiftOrigin(offset)` between steps, by the same offset at the same step on every peer. Every body position decreases by `offset`, `world.Origin` grows by it, and a host coordinate is `world.Origin + body.Position`. Offset components must be multiples of 2^-31 (every Q31.32 value is) within ±2^22, else an exception names the value; nothing changes when it refuses. A shifted world then steps exactly like the unshifted one, translated: positions differ by the offset and everything else is bit-identical (positions advance on a 2^-32 grid for this; see the world spec). The origin is part of the snapshot and of the hash.
- **Past ±2^22** a moving body's bounds are out of range and `Step` throws `BodyOutOfBoundsException` before changing anything.
```csharp
var world = new PhysicsWorld2D();
var crate = world.AddBox(new Vector2F(1, 1), at: new Vector2D(2_000_000, 0));
world.Step(1f / 60);
world.ShiftOrigin(new Vector2D(2_000_000, 0)); // between steps, by the same amount on every peer
double hostX = world.Origin.X + crate.Position.X; // the position in host coordinates is unchanged
```
Shifting exists for the ±2^21 bound. Contact and solver math runs on float32 values relative to a body or contact point, so a large coordinate does not make contacts coarser.
# Soft bodies
Cloth, rubber and jelly in the 3D world (a preview, like the rest of `Physics3D`: `#pragma warning disable OPHYS0002`). A soft body is a mesh of particles held together by edge, bend and volume constraints (position-based dynamics, XPBD). Particles are small spheres that collide with every rigid shape (static, kinematic and dynamic; compounds, meshes and height fields included), with each other, and with friction. They push dynamic rigid bodies and are pushed by them.
## Happy path
```csharp
SoftBody cloth = world.AddCloth(width: 2f, depth: 2f, segments: 16, at: new Vector3D(0, 1.8, 0));
SoftBody cube = world.AddSoftBox(new Vector3F(0.6f, 0.6f, 0.6f), at: new Vector3D(2, 1, 0), SoftMaterial.Jelly);
SoftBody ball = world.AddSoftBall(0.3f, at: new Vector3D(-2, 1, 0), SoftMaterial.Rubber);
Vector3D middle = cloth.GetPosition(8 * 17 + 8);
```
- `SoftBody` is a small struct handle like `Body`: it allocates nothing and throws `StaleHandleException` after `Destroy()`. There is no callback anywhere on it.
- Sizes are full extents, positions are `double`, everything else `float`, as for rigid bodies.
- `GetPosition(i)` reads one particle without allocating. Read particles with `GetPosition(i)`, `GetVelocity(i)`, `CopyPositions(span)`, or the raw spans (`RawPositions`, `RawVelocities`, `RawEdgeA`, ...); `Volume`, `RestVolume`, `Bounds`, `CenterOfMass`, `Momentum` and `IsAsleep` are cheap.
- `SetPinned(i, true)` fixes a particle where it is; `SetPosition` teleports one; `ApplyImpulse` and `AddVelocity` push.
- Presets: `SoftMaterial.Cloth` (inextensible, bends easily, self-collision on; use with an open surface such as `SoftBodyMesh.Cloth`), `Rubber` (stiff, springs back) and `Jelly` (wobbly). The meshes `SoftBodyMesh.Cloth`, `Box` and `Sphere` are generated; `FromTriangles` and `FromTetrahedra` take your own.
## Middle tier
`SoftBodyDef` holds every parameter with a default: mass, particle radius, the compliance (metres per newton; 0 is as stiff as it gets) and damping of the edge, bend and volume constraints, `InternalPressure` for a closed surface, friction, damping, gravity scale, self-collision, pins, and the layer, category/mask and group used by the rules in [queries and layers](https://recursiveai.org/onephysics/docs/queries-and-layers/). `SoftBodyDef.Cloth`, `Rubber` and `Jelly` start from the presets. The world options `SoftSubsteps` (default 4) and `SoftIterations` (default 3) trade cost for stiffness: many small substeps beat many iterations (Macklin et al. 2019).
## Skinning to a skeleton
A soft body can follow an animated skeleton: clothing on a character, flesh on a bone. `SoftSkin.Create` takes the skeleton (parents before children, as in a ragdoll definition), the joints' bind positions in the mesh's coordinates, four joint numbers and weights per particle (linear blend skinning) and a **maximum distance** per particle. Each step the particle is held within that distance of its skinned position and never beyond; a distance of 0 makes the particle kinematic. Optional **back-stop** spheres (a distance behind the skinned surface along its normal, and a radius) keep a cloth from being pushed into the body it covers.
```csharp
var sheet = SoftBodyMesh.Cloth(1f, 1f, 4, 4);
int n = sheet.VertexCount;
var joints = new int[4 * n]; // four joint numbers per particle (all joint 0 here)
var weights = new float[4 * n]; // and four weights
var maxDistance = new float[n];
for (int i = 0; i < n; i++) { weights[4 * i] = 1f; maxDistance[i] = i < 5 ? 0f : 0.05f; }
var skin = SoftSkin.Create(sheet, [-1], [new Vector3F(0, 0, 0)], joints, weights, maxDistance);
SoftBody banner = world.AddSoftBody(new SoftBodyDef { Mesh = sheet, Skin = skin, Position = new Vector3D(0, 4, 3) });
banner.SetSkeletonPose([QuaternionF.Identity], at: new Vector3D(0.5, 4, 3)); // move the bone; the targets follow over the next step
```
- Pose the skeleton before `world.Step` with `SetSkeletonPose` (one rotation per joint, relative to the parent in bind-pose axes, as ragdoll poses are) or with `SetSkeletonTransforms` (a world position and rotation per joint, for an animation system that has them). The targets move linearly to the new pose over the step.
- `teleport: true` places the particles on their skinned positions at once with zero velocity; use it to spawn or reset a body.
- A body with a moving pose does not sleep; a changed pose wakes it. `GetSkinnedPosition(i)` and `CopySkinnedPositions` read the targets, and the debug geometry carries them.
- The limits hold after every step to within float32 rounding, even against contacts: where a contact and a limit disagree the limit wins, so keep the skin where the cloth can reach the shapes it should collide with.
## What to expect
- Stiff, heavy rigid bodies on light soft ones sink: a 5 kg box on an 8 kg cushion is fine; 300 kg on a 5 kg block is not, because the soft body is solved with a few passes. Add iterations or mass.
- Thin shapes are not tunnelled at the speeds of the scenes (particles that move more than half their radius in a substep are swept).
- Soft bodies sleep like rigid bodies and wake when an awake rigid body comes near.
- Snapshots, hashes and rollback include soft bodies (an optional section: worlds without soft bodies keep the bytes and hashes they had). Soft bodies are not solved in parallel; the thread count does not change a bit.
- Not yet: tearing, and solving separate bodies in parallel.
Details and the order of a step are in [the specification](https://github.com/recursive-ai-oss/onephysics-csharp/blob/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec/softbody.md).
# Coming from Unity
OnePhysics uses Unity's vocabulary for the happy path where it can, with these differences: there are no GameObjects, so the `Body` is the thing you hold; the world is an object you create and step (there is no global `Physics2D`); angles are radians; box sizes are full sizes; and positions are `double`. The 3D names are the same as the 2D names listed here, on `PhysicsWorld3D` and its `Body`.
| Unity | OnePhysics | Note |
|---|---|---|
| `Physics2D.Simulate(dt)`, `FixedUpdate` | `world.Step(dt)` | Fixed dt; the world has no clock. |
| `Time.fixedDeltaTime` | the `dt` you pass | You choose it, the same on every peer. |
| `Physics2D.gravity` | `world.Options.Gravity` | Takes effect at the next step. |
| `Rigidbody2D` | `Body` | A struct handle; allocates nothing. |
| `rb.bodyType` (Dynamic, Kinematic, Static) | `body.Type`, `BodyType.Dynamic/Kinematic/Static` | Fixed at creation (`type:` argument or `BodyDef.Type`). |
| `rb.isKinematic` | `body.IsKinematic` | |
| `rb.velocity` | `body.LinearVelocity` | |
| `rb.angularVelocity` (degrees/s) | `body.AngularVelocity` | Radians/s. |
| `rb.position`, `rb.rotation` (degrees) | `body.Position`, `body.Angle` (radians), `body.Rotation` | `SetTransform(position, angleRadians)` teleports. |
| `rb.mass` | `body.Mass` | From shape density. |
| `rb.AddForce(f)` | `body.ApplyForce(f)` | Cleared each step. |
| `rb.AddForce(f, ForceMode2D.Impulse)` | `body.ApplyImpulse(i)` | |
| `rb.AddForceAtPosition(f, p)` | `body.ApplyForceAt(f, p)`, `ApplyImpulseAt` | |
| `rb.AddTorque(t)` | `body.ApplyTorque(t)` | |
| `rb.IsSleeping()`, `Sleep()`, `WakeUp()` | `!body.IsAwake`, `body.Sleep()`, `body.Wake()` | |
| `rb.gravityScale`, `drag`, `angularDrag` | `BodyDef.GravityScale`, `LinearDamping`, `AngularDamping` | |
| freeze rotation constraint | `BodyDef.FixedRotation` | |
| `CollisionDetectionMode2D.Continuous` | `BodyDef.IsBullet` | Static geometry is always swept; bullets also sweep against other moving bodies. |
| `Destroy(gameObject)` | `body.Destroy()` | A destroyed handle throws `StaleHandleException`. |
| `BoxCollider2D`, `CircleCollider2D`, `CapsuleCollider2D`, `PolygonCollider2D`, `EdgeCollider2D` | `AddBox`, `AddCircle`, `AddCapsule`, `AddPolygon`, `AddSegment`/`AddChain`; or `CreateBox` etc. on an existing body | A body can have many shapes. |
| `collider.isTrigger` | `ShapeDef.IsSensor` | |
| `PhysicsMaterial2D` (friction, bounciness) | `Material2D(friction, restitution, rollingResistance)`, or `ShapeDef` fields | |
| `gameObject.layer` | `body.Layer` (0 to 31) | |
| `Physics2D.IgnoreLayerCollision(a, b)` | `world.IgnoreLayerCollision(a, b)` | |
| `OnCollisionEnter2D`, `Stay`, `Exit` | `body.OnCollisionEnter`, `OnCollisionStay`, `OnCollisionExit` | Stay needs `EventMask.ContactPersist` in the world options. |
| `collision.gameObject`, `collision.collider` | `c.Other`, `c.OtherShape` | |
| `collision.GetContact(0).point` | `c.Point`, `c.GetPoint(i)` | |
| `collision.relativeVelocity` | `c.ApproachSpeed` | Approach speed along the normal. |
| `OnTriggerEnter2D`, `OnTriggerExit2D` | `body.OnTriggerEnter`, `OnTriggerExit` | `Trigger.Other`. |
| `Physics2D.Raycast`, `RaycastAll` | `world.Raycast`, `RaycastAll` | Positions are `double`; the direction is a `float` vector. |
| `Physics2D.CircleCast`, `BoxCast`, `CapsuleCast` | `world.CircleCast`, `BoxCast`, `CapsuleCast` | |
| `Physics2D.OverlapCircle`, `OverlapBox`, `OverlapPoint` | `world.OverlapCircle`, `OverlapBox`, `OverlapPoint` (and `...All`, `...Any`) | |
| `Physics2D.OverlapCircleNonAlloc` | `world.OverlapCircleAll(center, radius, span)` | Fills your span. |
| `Collider2D.ClosestPoint`, `Physics2D.Distance` | `world.ClosestPoint` | |
| `ContactFilter2D` (layer mask, trigger toggle) | `QueryFilter2D` (`LayerMask`, `IncludeSensors`, `Predicate`) | |
| `HingeJoint2D` | `world.AddHinge(a, b, anchor)` returning `HingeJoint` | Radians. |
| `SliderJoint2D`, `FixedJoint2D`, `WheelJoint2D` | `AddSlider`, `AddFixed`, `AddWheel` | |
| `DistanceJoint2D`, `SpringJoint2D` | `AddDistance`, `AddSpring` | |
| `TargetJoint2D`, `RelativeJoint2D` | `AddTarget`, `AddRelative` | |
| `CharacterController.Move(motion)` | `character.Move(displacement, dt)` | Displacement for this frame. |
| `CharacterController.SimpleMove(v)` | `character.MoveVelocity(velocity, dt)` | |
| `isGrounded`, `slopeLimit` (degrees), `stepOffset` | `IsGrounded`, `SlopeLimitRadians`, `StepOffset` | Radians. |
| `OnControllerColliderHit` | `character.OnControllerColliderHit` | |
| `Physics.Raycast`, `Rigidbody`, ... (3D) | the same names on `PhysicsWorld3D` and its `Body` | The 3D engine is a preview. |
The joint one-liner to definition to view table is in [names and conventions](https://recursiveai.org/onephysics/docs/api-names/). If a Unity feature you rely on is missing, the middle tier ([the tiers](https://recursiveai.org/onephysics/docs/tiers/)) usually has it under the engine's own name.
# OnePhysics guide
Task-oriented pages with examples that are compiled and run by the test suite. For the exact members, read the XML documentation in your editor. The normative, bit-level descriptions for ports live in [../spec](https://github.com/recursive-ai-oss/onephysics-csharp/tree/fa8eed5a2021c452ca8051b6099917e09538b4f4/docs/spec).
| Page | Read it to |
|---|---|
| [Install](https://recursiveai.org/onephysics/docs/install/) | add the package, know the targets |
| [Quick start](https://recursiveai.org/onephysics/docs/quick-start/) | step a world, react to a collision, cast a ray, move a character |
| [The three tiers](https://recursiveai.org/onephysics/docs/tiers/) | choose between one-liners, definitions and the raw tier |
| [Events](https://recursiveai.org/onephysics/docs/events/) | pick per-body handlers or polled events; know the order |
| [Queries, layers and filters](https://recursiveai.org/onephysics/docs/queries-and-layers/) | decide what collides and what a query sees |
| [Rollback and lockstep](https://recursiveai.org/onephysics/docs/rollback/) | snapshot into a ring, restore, resimulate, hash |
| [Determinism](https://recursiveai.org/onephysics/docs/determinism/) | know exactly what is guaranteed, and the coordinate rule |
| [Units and sizes](https://recursiveai.org/onephysics/docs/units/) | avoid half-extent and degree mistakes |
| [Names and conventions](https://recursiveai.org/onephysics/docs/api-names/) | handles, joint one-liners and definitions, callbacks across restore |
| [Soft bodies](https://recursiveai.org/onephysics/docs/soft-bodies/) | make a cloth, a jelly or a pressurised ball; know what they collide with and how they push rigid bodies |
| [Input limits](https://recursiveai.org/onephysics/docs/limits/) | know what is refused |
| [Coming from Unity](https://recursiveai.org/onephysics/docs/unity-mapping/) | find the name of the thing you know |
# Changelog
What changed for hosts in each preview, newest first. "Moved results" lists the changes that alter a world's state after the same inputs (and so its snapshot bytes and world hashes).
**0.x previews are not bit-compatible with each other: snapshots, saved games and replays from one preview do not carry to another.**
## [Unreleased]
## [0.4.0-preview.1]
- **Soft bodies complete the 3D feature set for this preview line; the 3D engine stays `[Experimental("OPHYS0002")]`.** Every 3D scene, soft bodies included, is within its stability limits on x64 and arm64 with identical metrics, and results are bit-identical across x64, arm64, WebAssembly, vector widths and thread counts. Not every speed row meets the bar yet (within 1.15x of the fastest stable managed engine, BepuPhysics v2 or Jitter2, per scene and thread count). Measured on a Ryzen 9 7950X3D (ours / reference median step time, t1 / t8 on one core complex / t32):
| scene | t1 | t8 | t32 |
|---|---|---|---|
| large-pile-3d | 1.05 | 1.08 | 1.25 |
| ragdoll-pile-3d | 2.19 | 1.12 | 1.70 |
| sleep-wake-3d | 2.51 | 1.51 | 1.40 |
| queries-3d (settle phase) | 1.27 | 1.26 | 1.19 |
| cloth-drape-3d (soft body, against Jitter2) | 0.93 | 3.9 | 2.5 |
| heightfield-terrain-3d | 1.13 | 0.89 | 0.64 |
| mesh-terrain-3d | 1.08 | 0.89 | 0.85 |
| joint-grid-3d | 0.10 | 0.16 | 0.09 |
On an Apple M2 (t1 / t8) every 3D row meets the bar except ragdoll-pile-3d t1 (1.27) and sleep-wake-3d t1 (1.64), and cloth-drape-3d is 0.84 / 1.87. joint-chains-3d and pyramid-3d-30 have no stable managed reference, and the other soft-body scenes (soft-pile-3d, soft-rigid-3d, soft-mesh-3d, soft-skinned-3d) have none either; they are judged on their stability limits, which they meet. Every 2D scene is within the bar at every thread count.
- **3D soft bodies solve about 2.5 times faster on a cloth (indicative, one thread), and large bodies use several threads. Moved results (soft bodies only; rigid worlds and 2D are unchanged).** The step works on float32 positions relative to each body, the constraints are coloured once at creation (edges, bends and tetrahedra solved colour by colour, in `Vector128` lanes where four constraints of a colour are available, with the same bits as the scalar loop), bodies of 1024 particles or more are split across threads by colour, contact detection and the particle pair search run in tasks that are joined in particle order, and particle pairs are searched once per step instead of once per substep. The order in which constraints are solved changes, so soft-body states, snapshot bytes and hashes differ from the previous preview; the snapshot layout is unchanged. Details in `docs/spec/softbody.md` (Solver).
- **3D soft bodies follow a skeleton (skinning).** `SoftSkin.Create` builds per-particle joint weights (four influences, linear blend skinning), a maximum distance and an optional back-stop sphere from a skeleton and the mesh; set it as `SoftBodyDef.Skin`. `SoftBody.SetSkeletonPose` (ragdoll-style local rotations) or `SetSkeletonTransforms` (world joint poses) drives it before each step, with `teleport` to place or reset; `GetSkinnedPosition`, `CopySkinnedPositions`, `HasSkin`, `Slot` and `PhysicsWorld3D.GetSoftBody(slot)` (to find the bodies again after `Restore`) are new. A particle with maximum distance 0 is kinematic and equals its linearly blended position bit for bit; the others are held within their distance (and out of the back-stop) after every step, and win against contacts. Snapshot section version 2 is written only while a skinned body exists, so other worlds keep their bytes and hashes; version 1 sections still load. `DebugSoftBody.FirstTargetPoint` carries the targets. New scene `soft-skinned-3d` (a sleeve on a swinging arm) with `skinMaxDistance`, `skinBackstop` and `skinCuffError` metrics. See `docs/spec/softbody.md`.
- **3D: the bounding-box stage takes about half the time, with identical results.** The world box of a sphere, box, capsule or sharp hull is computed straight from the body arrays with only the rotation-matrix entries the shape needs, and a hull's six extremes come from one pass with the three axes as lanes of one 128-bit vector (the same operations in the same order as the scalar code, so every platform and vector width gives the same bits). Profile build, one thread, indicative: large-pile-3d 0.24 to 0.11 ms, ragdoll-pile-3d 0.08 to 0.04, sleep-wake-3d 0.07 to 0.035. No world hash moves. See `docs/spec/world3d.md`, round 35.
- **3D soft bodies (core).** `PhysicsWorld3D.AddCloth`, `AddSoftBox`, `AddSoftBall`, `AddSoftBody(SoftBodyMesh, ...)` and `AddSoftBody(SoftBodyDef)` make cloth, rubber and jelly from a triangle or tetrahedral mesh: extended position-based dynamics with edge, isometric-bend, tetrahedron-volume and pressure constraints, particles that collide with every shape, with each other and with friction, two-way coupling with dynamic rigid bodies, pins, sleeping, debug geometry (`DebugContent.SoftBodies`), `SoftSubsteps` and `SoftIterations` options. The `SoftBody` handle reads particles without allocating. Soft bodies are stored apart from rigid bodies and solved serially after the rigid step, in a new **optional** snapshot section (`0x534F4654`) that is written, hashed and counted only once a soft body exists: worlds without soft bodies keep every byte and hash, and snapshot format version 1 is unchanged. New scenes `cloth-drape-3d`, `soft-pile-3d`, `soft-rigid-3d`, `soft-mesh-3d` with soft-body metrics and a Jitter2 driver; fuzz targets `world3d-softbodies` and `world3d-softbody-snapshot`. `DebugContent.All` is now 1023; it is a constant, so code compiled against an earlier preview keeps `All` = 511 and does not draw soft bodies until it is rebuilt.
## [0.3.0-preview.1]
- **Characters, vehicles, ragdolls and buoyancy are complete for this preview line.** The rigid and virtual 3D characters, wheeled, tracked and motorcycle vehicles, ragdolls with powered pose matching, and buoyancy and drag in fluid volumes are within their stability limits on every scene, with identical results on x64 and arm64: `characters-3d` (every stair climbed, character jitter under 1e-6 m/s, no slope violations), `vehicles-3d` (no rollovers, no wheel through the ground, suspension jitter 0.002), `tracked-3d`, `motorcycle-3d` (none fallen, lean error 0.006), `ragdoll-powered-3d` (95th-percentile pose error 0.058 rad, none fallen) and `buoyancy-3d` (equilibrium depth error 0.0026). No managed .NET engine covers these scenes, so they are judged on their stability limits. They stay under `[Experimental("OPHYS0002")]` with the rest of the 3D engine until every 3D speed row meets the bar.
- **`GetSnapshotSizeBound()` on `PhysicsWorld2D` and `PhysicsWorld3D`** returns an upper bound on `GetSnapshotSize()` that holds however the world steps while no storage grows, so a host can size rollback buffers once. Contacts and joints, shape pairs, islands, broadphase nodes and events are counted at the capacity that holds them (`ContactCapacity`, `PairCapacity`); what the host creates (bodies, shapes, joints, characters, vehicles, ragdolls, fluids) is counted as it is now. Additive; the snapshot format and every world hash are unchanged.
- **3D box-against-box edge axes are tested without normalizing each axis** (about 9% off the box–box routine on pairs from a sleep-wake pile). Separations can differ from the previous preview in the last bit in some poses, so 3D world hashes and snapshot bytes move.
- **3D: mild joints run on the contact-only schedule.** An island whose joints are all mild (no kinematic end, motor, spring or pose drive; mass ratio at most 50; no more joints than bodies) now takes the 2-substep contact-only schedule with its contacts, instead of the world's 4. ragdoll-pile-3d solve stage 0.67× the time (step about 0.8×, one thread, indicative); stiff joint scenes are unchanged. See `docs/spec/world3d.md`, round 33. Step time on a locked machine: ragdoll-pile-3d 0.80× at one thread, 0.85× at eight, 0.75× at 32; large-pile-3d and the joint scenes within noise.
- **3D steps scale better with threads, with identical results.** The sleep timers run in parallel blocks as in 2D, colouring blocks are 512 constraints instead of 4096, and threads waiting at a stage boundary pause for a few instructions a round and give the core up only after a long wait, instead of backing off and yielding after ten rounds. On a locked machine, steps at eight and 32 threads take 0.94–0.99× the time on piles, ragdolls, sleep-wake and joint grids, in 2D and 3D. No world hash moves.
## [0.2.0-preview.1]
- **The snapshot format is frozen at version 1.** Its layout (header, options, bodies, shapes, constraints, islands and registered sections, `docs/spec/architecture.md`) no longer changes within version 1. A later change is a new format version. Results may still move between previews.
- **Raw tier (`[Experimental("OPHYS0001")]`): `IBox` gains `Translated(ReadOnlySpan)`**, which the exact origin shift uses. A type that implements `IBox` outside the library must add it.
- **The 3D engine stays `[Experimental("OPHYS0002")]`.** It meets the speed bar on most scenes but not all yet: within 1.15x of the fastest stable managed engine (BepuPhysics v2, Jitter2), per scene and thread count. Measured on a Ryzen 9 7950X3D (ours / reference median step time, t1 / t8 on one core complex / t32):
| scene | t1 | t8 | t32 |
|---|---|---|---|
| large-pile-3d | 1.10 | 1.23 | 1.47 |
| ragdoll-pile-3d | 2.75 | 1.42 | 2.17 |
| sleep-wake-3d | 2.75 | 1.60 | 1.41 |
| queries-3d (settle phase) | 1.31 | 1.42 | 1.38 |
| heightfield-terrain-3d | 1.17 | 0.95 | 0.68 |
| mesh-terrain-3d | 1.10 | 0.92 | 0.68 |
| joint-grid-3d | 0.10 | 0.16 | 0.06 |
joint-chains-3d and pyramid-3d-30 have no stable managed reference and are judged on their stability limits, which they meet. Every 3D scene is within its stability limits, and every 2D scene is within the bar at every thread count. These rows predate the narrowphase entry below, which takes 3-6% off the ragdoll, box-pile and sleep-wake rows.
- **`Warmup` primes more of the process.** Its trial world now drops a pile that lands, settles and falls asleep (150 steps) and runs box and sphere casts and box and capsule overlaps, so those paths are loaded before a host's first step. Worlds, hashes and snapshots are unchanged.
**Moved results:** 3D worlds move (the broadphase keeps tighter boxes, so pairs start and end at different steps), and 2D and 3D worlds move again because positions now advance on a 2^-32 grid (see the origin shift entry).
- **3D narrowphase is cheaper in ragdoll and box piles, with identical results** (ragdoll-pile-3d about 5%, sleep-wake-3d about 3%, large-pile-3d about 4% per step, one thread, indicative): a capsule against a box skips box edges that cannot be nearest, box edge axes are rejected before they are normalized, separated boxes nearest on an edge pair skip GJK, and pairs that need a narrowphase run in their own method. No world hash moves.
- **3D ray casts and sphere casts are faster (ray 0.59x, sphere cast 0.79x, `OverlapSphereAll` 0.84x the time on a 3000-body pile, one thread, indicative).** Spheres, capsules, boxes and hulls are tested in closed form, and the GJK route only runs for starts inside, grazing hits, edge regions and other shape kinds. Which shape is hit and the hit counts do not change; the last bits of `Fraction`, `Distance`, `Point` and `Normal` of ray and sphere-cast hits do (within the old route's tolerance), so worlds that cast (vehicle wheel rays) move.
- **3D: a substep schedule per island.** Islands without a joint (and not touching a kinematic body or holding a vehicle) now run 2 substeps of 1 solve and 2 relax passes (`WorldOptions3D.ContactSubsteps`, `ContactVelocityIterations`, `ContactRelaxIterations`; `ContactSubsteps = 0` restores the world-wide schedule); islands with joints keep `Substeps`. Piles and terrain step about 1.15-1.35x faster and worlds with those islands move; adding a joint changes only the island that receives it. The options are saved in snapshots and covered by the world hash.
- **3D broadphase defaults:** `BroadphaseMargin` is 0.03 (was 0.1), boxes stretch over four steps of motion, and the tree is refitted between rebuilds. About half as many pairs are kept alive in piles; sleep-wake scenes step about 1.2× faster and large piles at eight threads about 1.15×. Stability is unchanged on every 3D scene.
- A fast body still inside the supported range no longer fails the step with an argument error when its stretched box would cross the range limit; the stretch stops at the limit.
- **`ShiftOrigin` is exact.** A shifted world now steps through exactly the states of the unshifted world, translated (positions differ by the offset; velocities, rotations and contact data are bit-identical), instead of drifting from it. The shift moves the stored broadphase boxes in place instead of rebuilding them. To make that true, a body's position increment each step is rounded to a multiple of 2^-32 (2.3e-10 m; a body slower than about 1.4e-8 m/s at 60 Hz does not move), shape bounds are rounded outward to the same grid, and the broadphase margin is rounded to it. Positions handed in as 31.32 fixed-point values are already on the grid.
- **3D box-against-box face contacts are built without loading polyhedra.** The reference face is now the one the separating axis test chose (no second choice by area), so manifolds and the worlds that contain boxes move. The box-box manifold costs about 0.78× as much per fresh manifold in a sleep-wake pile (845 against 1090 ns).
## [0.1.0-preview.6]
- **Fix: 0.1.0-preview.5 could refuse a snapshot of a valid 3D world.** Its `Restore` rejected saved contact impulses above a fixed plausibility limit, but the solver can legitimately produce larger ones (a very heavy body on a light one, or a tight pile, gave impulses in the trillions). A world that had stepped normally could therefore write a snapshot that the same library then refused with `SnapshotFormatException`, which breaks a host's rollback. This release removes that check, and the wheel and engine speed limits added with it: a snapshot the library wrote always restores, and saved impulses are checked only for being finite. A damaged snapshot that carries an enormous impulse restores, and the next step reports the flung body with `BodyOutOfBoundsException` (the world unchanged), as before. No step result moves: world hashes and snapshot bytes are the same as in preview.5 for the same inputs.
- **3D `Restore` refuses a dynamic body whose saved extents do not match its shapes** (`SnapshotFormatException`, world unchanged). A snapshot the library wrote always matches.
- **3D steps are faster, with identical results.** Pairs whose contacts are unchanged skip the serial write-back; the tree update tests boxes in parallel; height-field collision visits only the cells a box covers; a convex shape stays loaded across the triangles of a mesh; overlap counts no longer collect hits; solver batches precompute their contents once per step. Height-field worlds step about 1.1–1.2× faster and large piles at eight threads about 1.1×. No result moves.
- **Vehicles: a step can no longer write state its own snapshot reader refuses.** Fuzzing found a differential ratio of about 3e37 (finite, so accepted) that overflowed the drivetrain into NaN in one step. Vehicle definitions now bound their ratios (at most 1000), inertias (0.001 to 1e6), engine torque, clutch strength, tyre factors and low speed, and `AddVehicle` and `Restore` refuse values outside the bounds with the usual argument and snapshot errors. The step also keeps engine and wheel speeds finite, and `Restore` checks them against what a step can write. Valid worlds are unchanged.
- Tests now check, after every step of every fuzz scenario (2D and 3D), that the world's snapshot restores into a fresh world and re-serializes to the same bytes.
## [0.1.0-preview.5]
First public release. The 2D engine and the shared core are the supported surface; the 3D engine ships as a preview marked `[Experimental("OPHYS0002")]`. A user guide is in `docs/guide/`.
**Moved results:** 2D worlds are bit-identical to preview.4 for the same inputs. 3D worlds move: an exact capsule-against-box closest pair replaces the general path when the shapes are apart, and the joint solver computes each joint's block inverse once per substep. Convex hulls built from inputs that were near-coplanar or noisy can differ (see below). Snapshot layout is unchanged.
- **Allocation-free from the first step, also under load and with several threads.** `Warmup()` now prepares every pool thread (including threads an earlier world started) for the narrowphase and batch queries, runs the pool's parallel paths of a throwaway world whatever their size, and stocks the island body-list pool for the world's body count. Before, a pile that kept merging and splitting islands allocated a few hundred bytes at about one step in a few hundred, and the first step of a multi-threaded 3D world allocated on the calling thread. `ComputeHash()` is documented as streaming (it always was: no snapshot copy is made).
- **Breaking: `CharacterDef.SlopeLimit` and `Character.SlopeLimit` are `SlopeLimitRadians`** (2D and 3D); the unit is in the name. The value and its default are unchanged.
- **`world.BodyCount`, `world.Bodies`, `world.CharacterCount`, `world.Characters`** (2D and 3D, identical): count and `foreach` over the live bodies and characters in slot order with a struct enumerator that allocates nothing.
- **The 3D engine is `[Experimental("OPHYS0002")]`:** every public type in `RecursiveAI.OnePhysics.Physics3D`. Using it raises the diagnostic until you suppress it (`#pragma warning disable OPHYS0002` or ``); its API and its results may change between previews. The 2D engine and the shared types are not marked.
- The package now carries the README.
- **Convex hulls contain their input points.** `ConvexHull3D.Build` now decides face visibility with exact predicates, rescans every input point once the hull is built, and checks each merged face against every point, undoing a merge that would leave a point outside. Every input point is inside every face within the documented tolerance, or the build is refused with `HullBuildException`; before, some near-coplanar inputs gave a hull that left a point outside by up to a few centimetres. Building is about 20% slower on hostile inputs.
- **3D `Restore` refuses damaged contact and vehicle state, and a step reports a runaway sleeping body.** A snapshot holding a saved contact impulse no solver step could have produced (it would launch the bodies out of the world in one step), or a vehicle wheel with an absurd sprung mass or wheel or engine speed, is refused with `SnapshotFormatException` naming the value, and the world is unchanged. A sleeping body far from the origin is now checked at the start of every step (`BodyOutOfBoundsException`, world unchanged), and a vehicle wheel whose top lies just outside the supported range finds no ground instead of failing the step. Valid worlds are bit-identical.
- **3D, faster at 8 threads:** solve stages split into one task per constraint batch and the worker count follows the average stage width (terrain scenes about 1.5–1.8× faster at 8 threads); results do not depend on the thread count, as before.
- **3D integration runs lane-wide** with a bit-identical scalar twin.
- Package metadata, symbols (`.snupkg`) and Source Link for the public repository.
## [0.1.0-preview.4]
**Breaking API changes.** No result moves: world hashes, snapshot bytes and event order are the same as in preview.3 for the same inputs. Migration is by namespace, definition and exception contracts only.
- **Definitions are valid at their zero value.** `default(BodyDef)`, `default(ShapeDef)`, `default(CharacterDef)` and every joint definition (2D and 3D) now mean the documented defaults, exactly like `new XDef()` (a dynamic body, density 1, friction 0.6, collides with everything, unbreakable joints, `JointFrame3D` at the identity). A class field, an array element or `default` no longer falls through the ground or breaks a joint at once. Migration: none needed; code that wrote `new XDef()` keeps working and `default` now agrees with it. `BodyType` keeps its numeric values, so snapshots are unchanged. Reference-type definitions (`FluidDef`, `RagdollDef`, `VehicleDef` and friends) have no zero value and are unchanged.
- **Namespaces.** `RecursiveAI.OnePhysics.Core` is gone. Types game code names now live in `RecursiveAI.OnePhysics`: `BodyType`, `EventKind`, `EventMask`, `PhysicsEvent`, `BodyHandle`, `ShapeHandle`, `JointHandle`, `MaterialLimits`, the exceptions (`PhysicsException`, `InvalidHandleException`, `StaleHandleException`, `ForeignHandleException`, `SnapshotFormatException`, `BodyTypeException`, `InvalidJointException`, `InvalidShapeException`, `HullBuildException`, `MeshBuildException`, `StaticOnlyShapeException`, `ShapeCompositionException` and their error enums) and the shapes the query methods take (`Shape2D`, `Vec2`, `Rot2`, `Transform2`, `Aabb2`, `Chain2D`, `MassData2D`, `RayCastOutput2D`, `ConvexShape3D`, `ConvexHull3D` and its option and face types). Migration: replace `using RecursiveAI.OnePhysics.Core;` with `using RecursiveAI.OnePhysics;`.
- **The raw tier is `RecursiveAI.OnePhysics.LowLevel` and `[Experimental("OPHYS0001")]`.** Stores, the scheduler and coloring, islands, the broadphase trees, GJK/EPA and manifold routines, snapshot readers and writers, and the 2D collision functions moved there from `Core`, `Broadphase`, `Collision2D` and `Collision3D`. Using them needs `$(NoWarn);OPHYS0001`. Migration: replace the `using` lines of those four namespaces with `using RecursiveAI.OnePhysics.LowLevel;`. `PhysicsWorld2D.Core` and `PhysicsWorld3D.Core` are marked experimental too. Test fixtures (`ParticleScene`, `SplitMix64`, `CollisionHash2D`, `StageProbe`, `ManifoldLayout`) are internal.
- **Exception contracts.** Argument errors are `ArgumentException`s whose `ParamName` is the caller's parameter (`at`, `size`, `radius`, `layerA`, `force`, `def`, and the second body of an `AddHinge`-style call), with messages in the caller's terms (`AddBox` reports `size`, not half extents). `InvalidJointException` now derives from `ArgumentException` instead of `PhysicsException` (catch it by its own type, or as `ArgumentException`). `InvalidShapeException.Reason` is now `Error`, like `HullBuildException`, `MeshBuildException` and `ShapeCompositionException`. `ShapeError` has a neutral zero value, `None`. `BodyOutOfBoundsException.Body` is a `Body` (2D and 3D), so recovery is `e.Body.Destroy()`. Failed `AddBox`, `AddCircle`, `AddCapsule` and `AddSegment` no longer leave a body behind.
- The README quick start compiles (it was missing `using RecursiveAI.OnePhysics;`); a test now builds and runs the README's code blocks.
**Breaking API changes, round 2.** No result moves here either: hashes, snapshot bytes and event order are unchanged. See `docs/guide/api-names.md` for the conventions.
- **Handles in results are fields.** `Collision`, `Trigger`, `QueryHit2D/3D`, `ControllerColliderHit` and `JointBreak` expose `Body`, `Other`, `Shape`, `OtherShape`, `Collider`, `Joint`, `BodyA` and `BodyB` as readonly fields, so `hit.Other.LinearVelocity = v` compiles. Migration: none for reads; code that took the property as a method group or through reflection changes. New `Body.SetLinearVelocity`, `SetAngularVelocity` and `SetPosition` for places a property cannot be assigned (list elements).
- **Full sizes.** 2D `CreateBox(body, halfWidth, halfHeight, ...)` is now `CreateBox(body, Vector2F size, ...)`; 3D `CreateBox` and `ShapeSpec3D.Box(halfExtents)` take full `size`. Migration: double the old values. `RagdollPartDef.ShapeSize` is `ShapeHalfSize` (the value is unchanged; the name now says what it is). Capsule parameters are `halfLength` in both dimensions (`ShapeSpec3D.Capsule`/`TaperedCapsule` and the 3D capsule queries were `halfHeight`); cylinders keep `halfHeight`.
- **Radians.** `CharacterDef.SlopeLimit` and `Character.SlopeLimit` are radians (they were degrees); the defaults mean the same 45 and 40 degrees. Migration: multiply degrees by pi/180. 2D `angle` parameters are named `angleRadians` (creators, queries, `Body.SetTransform`); rename named arguments.
- **One name per joint concept.** The general `Joint` uses the definition names: `LimitEnabled` is `EnableLimit`, `MotorEnabled` is `EnableMotor`, `SpringEnabled` is `EnableSpring`, `LowerLimit`/`UpperLimit` are `LowerAngle`/`UpperAngle` (revolute), `LowerTranslation`/`UpperTranslation` (prismatic, wheel) or `MinLength`/`MaxLength` (distance). `MaxMotorForce` (linear) and `MaxMotorTorque` (rotational) are separate and asking the wrong one throws `InvalidJointException` naming the right one.
- **Typed joint views.** `Add*` and `CreateJoint(ref XDef)` return `HingeJoint`, `SliderJoint`, `FixedJoint`, `WheelJoint`, `DistanceJoint`, `TargetJoint`, `RelativeJoint` (2D) and `FixedJoint`, `PointJoint`, `DistanceJoint`, `HingeJoint`, `SliderJoint`, `ConeJoint`, `SwingTwistJoint`, `SixDofJoint`, `PathJoint`, `GearJoint`, `RackAndPinionJoint`, `PulleyJoint` (3D) instead of `Joint`. Each converts implicitly to `Joint` (explicit back, checking the kind). Migration: `var` and `Joint` variables keep compiling; code that held the result in a `Joint` field is unchanged.
- **Characters, ragdolls, vehicles.** Presets moved from `Character` to `CharacterDef` (`CharacterDef.Humanoid`, `Child`, `Giant`, 3D `RigidHumanoid`). `Character.Index`/`Generation` are replaced by `Character.RawHandle` (`CharacterHandle`); `Ragdoll.RawHandle` and `Vehicle.RawHandle` are new (`RagdollHandle`, `VehicleHandle`). `GetCharacter(index, generation)` and `IsCharacterAlive(index, generation)` are now `GetCharacter(handle)` and `IsCharacterAlive(handle)`, with the ragdoll and vehicle forms in 3D.
- **`Restore` returns `OptionChanges`** (it returned nothing): the construction options the snapshot replaced. Ignore it if you do not care.
- **Rollback and handlers.** `using (world.SuppressCallbacks())` mutes body, joint and character handlers (polled events still work; it allocates nothing and nests). Per-body handlers now stay registered when a restore brings the same handle back: the world keeps the handlers of the last 1024 destroyed handles per kind once it has taken a snapshot, and a world that never snapshots releases handlers at destroy as before.
- `ToString` on `PhysicsEvent`, `Collision`, `Trigger`, `JointBreak`, `ControllerColliderHit` and `QueryHit2D/3D`.
## [0.1.0-preview.3]
- **OneMath 1.0.0.** The package now depends on `RecursiveAI.OneMath` 1.0.0 (from 0.1.0-preview.5). Its results are bit-identical to the preview, so this change alone moves no world.
- **Input limits.** Inputs that would break the world later are refused with typed errors: material coefficients beyond the limit, shapes with unrepresentable mass or inertia, convex shapes thinner than 2^-20 of their length, non-finite body state at the start of a step, invalid wheel dimensions in a snapshot. See `docs/guide/limits.md`.
- **Restore exactness.** The test that skips pairs still apart is now bounded by both shapes' reach, so a restored world containing kinematic or static bodies continues exactly like the original. Host-style hash scenarios with restores in the middle of a run now cover this.
- `ShiftOrigin` validates the shift before changing anything: a shift that would move a body out of range is refused with a typed error, and the world is unchanged.
- **Moved results: continuous collision against compounds.** Compound sweeps are now clipped to the compound's range, and short ray and normal edge cases in the 3D narrowphase are fixed, so some worlds with fast bodies or compounds differ from preview.2.
- **Moved results: sensors.** A sensor overlap is no longer a constraint. It is a mark on the pair, so a sensor never takes a constraint slot, a pair slot or a color that a contact could have had. Adding, moving or removing a sensor of any body type (static, kinematic, dynamic) leaves every solid body bit-identical. Worlds that contain sensors give different results and different snapshot bytes than preview.2 (in preview.2 sensors paired with kinematic bodies and characters reordered contact solving). Worlds without sensors are unchanged by this change.
- Sensor enter and exit events are unchanged in kind and order; an exit is now also reported when a body or shape that was inside a sensor is destroyed.
- Snapshot layout: the free-pair list of both engines now also carries the slots freed by sensor pairs (as complements, after the contact slots), and a 3D sensor overlap is written as a one-entry pair chain holding -2. Worlds without sensor pairs write the same bytes as before.
- New capacity options on `WorldOptions2D` and `WorldOptions3D` (`PairCapacity`, `ContactCapacity`, `IslandCapacity`, `QueryScratchCapacity`) and `Warmup()` on both worlds, so a host can reach an allocation-free steady state from the first step. Capacity options never change a result and are not saved in snapshots.
- `ComputeHash()` streams over the world state in snapshot order and allocates nothing (as since preview.2); this changelog now records it.
- Allocation: a deep-contact path in the 3D narrowphase allocated 96 bytes per call and no longer does; a restore of a larger world now allocates nothing after `Warmup()`.
- **Moved results: speed rounds and fixes since preview.2.** These change the state after the same inputs, for the scenes named: the 2D pair stage (refit between tree rebuilds) moves 2D worlds with many moving bodies; the 3D solver kernels move worlds with joints and ragdolls; the character and services fixes for stale displacement move 2D worlds with characters or services.
## [0.1.0-preview.2]
- Host hooks: `TrySnapshot`, `GetSnapshotSize` and `Restore(ReadOnlySpan)` without per-call copies; allocation-free `ComputeHash`; handle liveness queries; character index and generation; typed errors for velocity and bullet settings on static bodies.
- **Moved results: sensors for kinematic bodies and characters.** Sensors now pair with kinematic bodies and characters, so those worlds' events, constraint order and results differ from preview.1.
- Speed rounds between preview.1 and preview.2 (pair stage, solver glue, 3D contact preparation) left the world hashes of the project's scenes unchanged; the sensor change above is what moved results in preview.2.
## [0.1.0-preview.1]
First preview: the 2D engine, the shared core (handles, deterministic scheduler and coloring, islands and sleeping, layers and filters, events and sensors, queries, materials, snapshot and restore, world hashes, debug geometry), and the 3D rigid core with characters, vehicles, ragdolls and buoyancy in progress.
[0.1.0-preview.5]: https://github.com/recursive-ai-oss/onephysics-csharp/releases/tag/v0.1.0-preview.5
[0.1.0-preview.6]: https://github.com/recursive-ai-oss/onephysics-csharp/releases/tag/v0.1.0-preview.6
# Rollback and resimulation
Build a rollback loop with OnePhysics: a ring of snapshots, a late input that rewinds five frames, quiet resimulation, and a check that the result equals a world that had the input on time.
A stack of ten boxes and a ball. The ball is pushed at frame 60; at frame 120 a peer’s input for frame 115 arrives late.
```csharp
using RecursiveAI.OneMath;
using RecursiveAI.OnePhysics;
using RecursiveAI.OnePhysics.Physics2D;
const float dt = 1f / 60;
var world = new PhysicsWorld2D(new WorldOptions2D { WorldId = 1 });
world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
for (int i = 0; i < 10; i++) world.AddBox(new Vector2F(0.8f, 0.8f), at: new Vector2D(0.05 * i, 0.5 + i));
var ball = world.AddCircle(0.5f, at: new Vector2D(-6, 1));
int sounds = 0;
ball.OnCollisionEnter += c => sounds++;
world.Warmup(); // steps, snapshots and restores no longer allocate
const int Ring = 8;
var ring = new byte[Ring][];
var lengths = new int[Ring];
for (int i = 0; i < Ring; i++) ring[i] = new byte[world.GetSnapshotSizeBound()];
int frame = 0;
void Advance(Vector2F? push = null)
{
int slot = frame % Ring;
world.TrySnapshot(ring[slot], out lengths[slot]);
if (push is { } v) ball.LinearVelocity += v; // this frame's input, if any
world.Step(dt);
frame++;
}
for (int i = 0; i < 120; i++) Advance(i == 60 ? new Vector2F(12, 0) : null);
Console.WriteLine($"frame {frame}: hash {world.ComputeHash():X16}, sounds {sounds}");
// A peer's input for frame 115 arrives late: a second push. Roll back five frames and resimulate.
int target = frame - 5;
world.Restore(ring[target % Ring].AsSpan(0, lengths[target % Ring]));
frame = target;
using (world.SuppressCallbacks()) // handlers stay quiet while frames replay
{
for (int i = 0; i < 5; i++) Advance(frame == 115 ? new Vector2F(0, 6) : null);
}
ulong corrected = world.ComputeHash();
Console.WriteLine($"frame {frame} after rollback: hash {corrected:X16}, sounds {sounds}");
// A second world fed the same inputs from the start reaches the same bits.
var replay = new PhysicsWorld2D(new WorldOptions2D { WorldId = 1 });
replay.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
for (int i = 0; i < 10; i++) replay.AddBox(new Vector2F(0.8f, 0.8f), at: new Vector2D(0.05 * i, 0.5 + i));
var ball2 = replay.AddCircle(0.5f, at: new Vector2D(-6, 1));
for (int f = 0; f < 120; f++)
{
if (f == 60) ball2.LinearVelocity += new Vector2F(12, 0);
if (f == 115) ball2.LinearVelocity += new Vector2F(0, 6);
replay.Step(dt);
}
Console.WriteLine($"replayed from scratch: hash {replay.ComputeHash():X16}, equal: {replay.ComputeHash() == corrected}");
```
```text
frame 120: hash A30B068101DD5CBF, sounds 3
frame 120 after rollback: hash 3013CDBC50D11693, sounds 3
replayed from scratch: hash 3013CDBC50D11693, equal: True
```
## The pieces
- **`Warmup()`**, after the bodies exist, sizes storage and exercises the snapshot, restore and hash paths, so steady-state steps, `TrySnapshot` and `Restore` allocate nothing.
- **`GetSnapshotSizeBound()`** is an upper bound on the snapshot size while no storage grows, so the ring is allocated once. `TrySnapshot` writes into it and returns false if it is too small.
- **`Restore`** validates the whole snapshot first and changes nothing if it is invalid. A `WorldId` you set keeps `Body` handles valid across restores, and in a freshly built world.
- **`SuppressCallbacks()`** keeps handlers quiet while frames replay, so the impact sound doesn’t play twice; polled events are still produced.
- **`ComputeHash()`** is what peers exchange every few frames to catch a desync.
## Good to know
Inputs must be applied the same way on every peer and in every replay. Writing `LinearVelocity += (0, 0)` on frames without input is not a no-op: it turns `-0` into `+0`, a different bit pattern, and the hashes part ways. Apply an input only when there is one, as `Advance` does.
The whole loop, `OptionChanges` and handle rules are in [rollback and lockstep](https://recursiveai.org/onephysics/docs/rollback/).
# Events and sensors
React to collisions, impacts, sleep and sensor overlaps in OnePhysics, with handlers or polled events, in an order that is the same on every machine.
A ball falls through a sensor zone onto the ground:
```csharp
using RecursiveAI.OneMath;
using RecursiveAI.OnePhysics;
using RecursiveAI.OnePhysics.Physics2D;
var world = new PhysicsWorld2D(new WorldOptions2D { Events = EventMask.Default | EventMask.ContactHit });
var ground = world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
var ball = world.AddCircle(0.5f, at: new Vector2D(0, 6));
ball.OnCollisionEnter += c => Console.WriteLine($"enter: ball touches {(c.Other == ground ? "ground" : c.Other.ToString())}");
ball.OnCollisionHit += c => Console.WriteLine($"hit: landed at {c.ApproachSpeed:F3} m/s");
ball.OnSleep += b => Console.WriteLine($"the ball fell asleep");
// A sensor: reports overlaps, never pushes.
var zone = world.CreateBody(new BodyDef { Type = BodyType.Static, Position = new Vector2D(0, 3) });
world.CreateBox(zone, new Vector2F(4, 1), def: new ShapeDef { IsSensor = true });
zone.OnTriggerEnter += t => Console.WriteLine("trigger: something entered the zone");
zone.OnTriggerExit += t => Console.WriteLine("trigger: it left the zone");
int begins = 0;
for (int i = 0; i < 300; i++)
{
world.Step(1f / 60);
begins += world.GetEvents(EventKind.ContactBegin).Length; // polled: a span, valid until the next step
}
Console.WriteLine($"polled ContactBegin events: {begins}");
```
```text
trigger: something entered the zone
trigger: it left the zone
enter: ball touches ground
hit: landed at 10.464 m/s
the ball fell asleep
polled ContactBegin events: 1
```
## Two ways to listen
- **Handlers on a body:** `OnCollisionEnter`, `OnCollisionStay`, `OnCollisionExit`, `OnCollisionHit`, `OnTriggerEnter`, `OnTriggerExit`, `OnSleep` and `OnWake`. A `Collision` carries the other body and shape, the normal, the contact points, the normal impulse and the approach speed.
- **Polled events:** `world.Events` is every event of the last step and `world.GetEvents(kind)` the slice of one kind. Both are spans, valid until the next `Step`, and allocate nothing. Prefer them in rollback code.
`WorldOptions2D.Events` chooses what is recorded: `ContactHit` (impacts with their speed) and `ContactPersist` are off by default, and a kind that is off costs nothing.
## Order
Events are sorted after each step, first by kind (`ContactBegin`, `ContactPersist`, `ContactEnd`, `SensorEnter`, `SensorExit`, `JointBreak`, `Sleep`, `Wake`, `ContactHit`) and then by the slots of the bodies and shapes, and handlers run in that order: first for body A of a pair, then body B. The same inputs give the same list on every platform and thread count. Details in [events](https://recursiveai.org/onephysics/docs/events/).
# Queries and layers
Decide which bodies collide with layers in OnePhysics, then ask the world what is where: a raycast, an overlap test filtered to one layer, and a circle cast that skips chosen bodies.
```csharp
using RecursiveAI.OneMath;
using RecursiveAI.OnePhysics;
using RecursiveAI.OnePhysics.Physics2D;
const int Ground = 1, Player = 2, Enemy = 3;
var world = new PhysicsWorld2D();
var ground = world.AddBox(new Vector2F(20, 1), at: new Vector2D(0, -0.5), type: BodyType.Static);
var player = world.AddCircle(0.5f, at: new Vector2D(-2, 0.5));
var enemy = world.AddCircle(0.5f, at: new Vector2D(2, 0.5));
var crate = world.AddBox(new Vector2F(1, 1), at: new Vector2D(5, 0.5));
ground.Layer = Ground;
player.Layer = Player;
enemy.Layer = Enemy;
world.IgnoreLayerCollision(Player, Enemy); // players and enemies pass through each other
Console.WriteLine($"player collides with ground: {world.GetLayerCollision(Player, Ground)}, with enemies: {world.GetLayerCollision(Player, Enemy)}");
world.Step(1f / 60);
// Raycast from above: the first shape the ray meets.
if (world.Raycast(new Vector2D(2, 5), new Vector2F(0, -1), out var down))
Console.WriteLine($"ray down at x=2 hits {(down.Body == enemy ? "enemy" : "?")} at y={down.Point.Y:F3}, {down.Distance:F3} m");
// Overlap: what is near a point, enemies only.
var enemiesOnly = QueryFilter2D.OnLayer(Enemy);
QueryHit2D[] around = world.OverlapCircleAll(new Vector2D(1, 0.5), 1.5f, enemiesOnly);
Console.WriteLine($"enemies within 1.5 m of (1, 0.5): {around.Length}");
// A circle cast that ignores the player and the enemy: the first thing in the way is the crate.
var solidOnly = new QueryFilter2D { Predicate = s => s.Body != player && s.Body != enemy };
if (world.CircleCast(new Vector2D(-5, 0.5), 0.25f, new Vector2F(1, 0), out var hit, 100f, solidOnly))
Console.WriteLine($"circle cast: {(hit.Body == crate ? "crate" : "?")} after {hit.Distance:F3} m, normal {hit.Normal}");
```
```text
player collides with ground: True, with enemies: False
ray down at x=2 hits enemy at y=1.000, 4.000 m
enemies within 1.5 m of (1, 0.5): 1
circle cast: crate after 9.250 m, normal (-1, -2.7258277E-05)
```
## The pieces
- **Layers** are numbers you assign with `body.Layer`. `IgnoreLayerCollision(a, b)` makes two layers pass through each other; `GetLayerCollision` reads the table. Group indexes, category and mask bits and a pair filter give finer rules; the first rule that answers decides.
- **Queries**: `Raycast`, `CircleCast` and the other shape casts return the first hit, a `QueryHit2D` with `Body`, `Shape`, `Point`, `Normal`, `Fraction` and `Distance`. The `…All` overlap forms return every hit; their span overloads allocate nothing.
- **`QueryFilter2D`** narrows a query: `OnLayer(layer)`, or a `Predicate` on the shape.
The circle cast stops at 9.25 m: the crate’s face is at x = 4.5 and the circle’s radius is 0.25. Queries never change the world, so they don’t change its hash. `PhysicsQuery2D` runs batches of queries in parallel with results in input order. More in [queries and layers](https://recursiveai.org/onephysics/docs/queries-and-layers/).
# Joints: a chain and a spring
Connect bodies with OnePhysics joints: a five-link chain hinged to a fixed anchor, and a box hung from a spring.
```csharp
using RecursiveAI.OneMath;
using RecursiveAI.OnePhysics;
using RecursiveAI.OnePhysics.Physics2D;
var world = new PhysicsWorld2D();
var anchor = world.AddBox(new Vector2F(0.2f, 0.2f), at: new Vector2D(0, 10), type: BodyType.Static);
// A chain of five links, each hinged to the one before, swinging from the anchor.
Body previous = anchor;
var links = new List();
for (int i = 0; i < 5; i++)
{
var link = world.AddCapsule(0.15f, 0.5f, at: new Vector2D(0.5 + i, 10)); // radius, half length
world.AddHinge(previous, link, new Vector2D(i, 10)); // the hinge point, in world coordinates
links.Add(link);
previous = link;
}
// A spring holds a box under a ceiling.
var ceiling = world.AddBox(new Vector2F(2, 0.2f), at: new Vector2D(8, 10), type: BodyType.Static);
var weight = world.AddBox(new Vector2F(0.5f, 0.5f), at: new Vector2D(8, 8));
world.AddSpring(ceiling, weight, new Vector2D(8, 10), new Vector2D(8, 8), 2f, 0.3f); // 2 Hz, damping ratio 0.3
for (int step = 0; step <= 180; step++)
{
if (step % 60 == 0)
Console.WriteLine($"t={step / 60.0:F0}s chain end {links[^1].Position} weight y={weight.Position.Y:F4}");
world.Step(1f / 60);
}
```
```text
t=0s chain end (4.5, 10) weight y=8.0000
t=1s chain end (0.715487866429612, 5.590997472172603) weight y=7.9398
t=2s chain end (-4.190456279087812, 9.45277669513598) weight y=7.9390
t=3s chain end (-1.8731370756868273, 6.337280875071883) weight y=7.9390
```
## The joint one-liners
| One-liner | Definition | Typed view | What it does |
|---|---|---|---|
| `AddHinge(a, b, anchor)` | `RevoluteJointDef` | `HingeJoint` | Rotation about one point |
| `AddSlider(a, b, anchor, axis)` | `PrismaticJointDef` | `SliderJoint` | Motion along one axis |
| `AddFixed(a, b, anchor)` | `WeldJointDef` | `FixedJoint` | No relative motion |
| `AddWheel(a, b, anchor, axis)` | `WheelJointDef` | `WheelJoint` | A suspension along an axis |
| `AddDistance(a, b, pA, pB)` | `DistanceJointDef` | `DistanceJoint` | A rigid rod at the current distance |
| `AddSpring(a, b, pA, pB, hertz, damping)` | `DistanceJointDef` | `DistanceJoint` | A spring at the current distance |
The typed view converts to `Joint`. Limits, motors and springs are switched on with `EnableLimit`, `EnableMotor` and `EnableSpring`, and every angle, limit and motor speed is in radians.
A 2 Hz spring under gravity settles about g / (2π · 2)² ≈ 0.062 m below its rest length, which is where the box ends up. The table of names is in [names and conventions](https://recursiveai.org/onephysics/docs/api-names/).
# The 3D preview: rigid bodies and cloth
Try OnePhysics’s 3D engine: a sphere dropped on a box, and a cloth draped over a block, with a snapshot and a world hash like the 2D engine’s.
The 3D engine is a preview inside the preview, marked `[Experimental("OPHYS0002")]`: it works and is deterministic, but its API and results may change between previews. Using it is a compiler error until you acknowledge that, here with `#pragma warning disable OPHYS0002`.
```csharp
#pragma warning disable OPHYS0002 // the 3D engine is a preview
using RecursiveAI.OneMath;
using RecursiveAI.OnePhysics;
using RecursiveAI.OnePhysics.Physics3D;
var world = new PhysicsWorld3D();
world.AddBox(new Vector3F(20, 1, 20), at: new Vector3D(0, -0.5, 0), type: BodyType.Static);
var ball = world.AddSphere(0.5f, at: new Vector3D(0, 5, 0));
ball.OnCollisionEnter += c => Console.WriteLine($"ball hit the ground along {c.Normal}");
// A cloth dropped over a box (soft bodies are new in 0.4.0-preview.1).
world.AddBox(new Vector3F(1, 1, 1), at: new Vector3D(3, 0.5, 0), type: BodyType.Static);
SoftBody cloth = world.AddCloth(width: 2f, depth: 2f, segments: 16, at: new Vector3D(3, 2, 0));
for (int i = 0; i < 180; i++) world.Step(1f / 60);
Console.WriteLine($"ball at {ball.Position}");
Console.WriteLine($"cloth middle at {cloth.GetPosition(8 * 17 + 8)}, corner at {cloth.GetPosition(0)}");
if (world.Raycast(new Vector3D(0, 10, 0), new Vector3F(0, -1, 0), out var hit)) Console.WriteLine($"ray hit at {hit.Point}");
Console.WriteLine($"snapshot {world.Snapshot().Length} bytes, hash {world.ComputeHash():X16}");
```
```text
ball hit the ground along (-0, -1, -0)
ball at (0, 0.49990872712805867, 0)
cloth middle at (2.996202104142867, 1.0705060136970133, -0.0004592029581544921), corner at (2.1870665550231934, 0.42688015825115144, -0.7950175404548645)
ray hit at (0, 0.9999087271280587, 0)
snapshot 29448 bytes, hash 14867385E479387F
```
## Good to know
- The 3D world has the same members as the 2D one, with 3D math types: `PhysicsWorld3D`, `AddBox`, `AddSphere`, `Raycast`, `Snapshot`, `ComputeHash`.
- **Soft bodies** are new in 0.4.0-preview.1: `AddCloth`, `AddSoftBox` and `AddSoftBall` with the `Cloth`, `Rubber` and `Jelly` materials, or `AddSoftBody` from your own triangle or tetrahedral mesh. Particles collide with every shape and with each other and push dynamic bodies. `GetPosition(i)` reads one particle without allocating; particle `8 * 17 + 8` is the middle of a 16-segment cloth.
- 3D also has characters, vehicles, ragdolls and buoyancy. Results are bit-identical across x64, arm64, WebAssembly, vector widths and thread counts.
See [soft bodies](https://recursiveai.org/onephysics/docs/soft-bodies/) and the [changelog](https://recursiveai.org/onephysics/docs/changelog/).