Bepu: Who Owns the Transform?
Most surprises when working with Bepu physics come from one rule:
Transform sync is one-way: physics →
TransformComponent.
Once an entity has a body attached, the simulation owns its position and rotation. Assigning
Entity.Transform.Position moves the mesh only; the body stays exactly where the simulation put
it, and the next frame usually overwrites your change.
Most of the symptoms below produce no error or warning. Each section starts with what you observe.
"My mesh moves, but nothing collides with it"
You set Entity.Transform.Position each frame, the mesh visibly moves, and collisions happen
somewhere else entirely - or not at all. The collider was left behind at the body's real position.
This one has a second, more confusing form. Only awake bodies are synced back to their transform. A dynamic body that settles and falls asleep stops overwriting the transform, so direct transform writes suddenly appear to start working. The visual moves, the collider does not.
Instead, move the body deliberately:
Teleport(...)jumps a body to a new pose without checking collisions along the way.- For scripted motion that should collide, use a body with
Kinematic = trueand drive it with velocity.
"I set LinearVelocity and the body stops after a while"
Setting LinearVelocity does not wake a sleeping body. The motion runs until the body sleeps,
then silently stops.
Set Awake = true as well whenever you assign a velocity to a body that might have gone to
sleep.
"My kinematic body flies off to NaN"
This one appears within seconds when driving a body with SetTargetPose(...) from a per-frame
Update.
SetTargetPose derives its velocity from (target - position) / FixedTimeStep, which assumes
exactly one physics tick per call. When the frame rate falls below the physics rate, two ticks
run on that velocity, the body overshoots, the next correction overshoots further, and it diverges to
NaN.
SetTargetPose is safe when the caller runs once per physics tick - from
ISimulationUpdate.SimulationUpdate - or when the frame rate is pinned to the physics rate.
Otherwise, integrate a velocity you compute yourself, and add a small proportional pull towards the
ideal position to stop drift.
"My component reference is null inside SimulationUpdate"
ISimulationUpdate.SimulationUpdate can run before StartupScript.Start and SyncScript.Start.
A component is registered with the simulation as soon as it enters the scene, whereas Start waits
its turn in the script system.
Resolve references lazily rather than caching them in Start:
public void SimulationUpdate(BepuSimulation simulation, float simTimeStep)
{
_body ??= Entity.Get<BodyComponent>();
// ...
}
"I want the model without a body"
IncludePhysics = false on the options leaves the physics out altogether. A BodyComponent whose
CompoundCollider holds no shapes never attaches to the simulation, so a body added by hand with an
empty compound is an inert component rather than no physics.
"Bepu crashes with an AccessViolationException"
The stack lands somewhere in the solver - Contact4.ApplyDescription, AddToSimulationSpeculative
- on a physics worker thread, after a few seconds of simulation.
This one is unresolved. What is known about it:
- It is intermittent, on the order of one run in five with the same binary and the same scene. A change that survives one run has not been shown to fix anything.
- Every case observed so far used a hull-backed shape -
TriangularPrism,Cone,TeapotorTorus- at a few thousand bodies. It has not been reproduced with cubes or spheres, which is suggestive but not proof, since more bodies and more contacts also make it likelier. - Sharing hull data across bodies does not stop it, so the finalizer hazard described in the next section, real as it is, is not a sufficient explanation.
Until the cause is found, the practical options are to keep hull-shaped body counts down, or to substitute an analytic collider - a box, a cylinder or a capsule - where the shape allows it.
"One hull per body is slow, and freed from a finalizer"
This is not the crash above; it is a cost and a genuine thread-safety hazard, worth avoiding on its own account.
Stride caches the built Bepu hull against the DecomposedHulls instance it came from, so a fresh
instance per body means a fresh hull per body: ten thousand identical prisms build, store and
eventually free ten thousand identical hulls. Those hulls hold unmanaged buffers taken from a
static BufferPool, and Stride returns them from a finalizer - so the garbage collector can
hand memory back to that pool while the simulation is allocating from it on its worker threads.
The toolkit shares one hull per distinct shape and size through SharedHullCache, so
CreatePrimitive builds a single hull no matter how many bodies use it, and nothing is ever
finalized.
If you build a ConvexHullCollider yourself, share the hull data. Assign the same
DecomposedHulls instance to every collider rather than calling ToConvexHullCollider() per body;
ToDecomposedHulls() gives you just the data to hold onto.
// Once
var hull = TriangularPrismProceduralModel.New(size).ToDecomposedHulls();
// Per body
Collider = new CompoundCollider { Colliders = { new ConvexHullCollider { Hull = hull } } }
"Spawning bodies in a grid kills the process with a Stack overflow"
The process dies outright - no exception, just Stack overflow. and a stack that repeats one frame
thousands of times:
Stack overflow.
Repeated 7990 times:
at BepuPhysics.Trees.Tree.Refit2WithCacheOptimization(Int32, Int32, Int32, NodeChild ByRef, ...)
at BepuPhysics.Trees.Tree.Refit2WithCacheOptimization(Buffer`1<Node>)
at BepuPhysics.CollisionDetection.BroadPhase.Update2(IThreadDispatcher, Boolean)
That function recurses once per level of the broad-phase tree, so its depth is the tree's height. Over a few thousand bodies a healthy tree is a dozen or so levels deep, not eight thousand: the tree has degenerated into something close to a linked list, and the recursion has no depth guard. Bepu does refine the tree incrementally every frame, but on a fixed budget, and some scenes degrade it faster than that budget repairs it.
The trigger is a perfectly regular lattice of exactly-touching bodies - 5,000 spheres one
diameter apart on a grid, all on the same plane, reproducibly dead within about ten seconds. Unlike
the AccessViolationException above it is deterministic, and it is not a race: it reproduces just as
readily single-threaded, and with sleeping disabled.
Break the symmetry and it goes away. Any of these survived indefinitely:
- a millimetre or so of random jitter on each spawn position - the bodies are just as close, and some overlap, which is fine;
- wider spacing, so nothing touches at spawn;
- random placement.
// Instead of an exact lattice
entity.Transform.Position = new Vector3(x, y, 0);
// Nudge each one off the grid
entity.Transform.Position = new Vector3(
x + (Random.Shared.NextSingle() - 0.5f) * 0.05f, y, 0);
"A 2D pile of prisms eats all my memory"
Thousands of TriangularPrism bodies on a Body2DComponent, and within seconds the process is
holding tens of gigabytes and dies. The stack, if you catch one, is in
NarrowPhase.ExecutePreflushJob. The same scene with an ordinary BodyComponent is untroubled.
The cause was not the hull, the mesh, or contact volume, all of which were measured and cleared.
It was Body2DComponent zeroing the X and Y terms of the inverse inertia tensor to lock rotation.
What matters is that only some terms were zeroed. Three runs each at 20,000 prisms:
| Inverse inertia tensor | Result |
|---|---|
| Full, untouched | no failure |
| Every term zeroed | no failure |
| X and Y zeroed, Z left responsive | fails 3/3, tens of GB in seconds |
A fully zeroed tensor is the idiom Bepu's own character demo uses - it means "no torque can rotate this" - and it is perfectly stable. So the problem is not that the tensor is degenerate; it is that it is degenerate in some directions and not others.
The toolkit scales those terms by 1e-4 instead of zeroing them, leaving the body four orders of
magnitude harder to rotate out of plane than within it while keeping every term non-zero. Anything
that leaks through is removed by the angular velocity clamp on the same step. Measured across 20 runs
at 8,000 and 20,000 bodies: no runaway, no increase in bodies squeezed out of the pile, and a pile
that settles where the zeroed version kept churning.
If you write your own per-axis rotation lock, scale the inertia terms rather than zeroing them - or zero the whole tensor, if you can live without rotation on every axis. A one-body constraint, solved alongside the contacts, is the more robust design again where the per-body cost is acceptable.
Why a partly-zeroed tensor misbehaves is not established; the evidence above is empirical.
"My torus collides as though the hole were filled"
A convex hull is exact for a convex shape, so TriangularPrism, Cone and Teapot collide as they
look. A torus is not convex, and its hull spans the hole.
There is no single-shape fix: an accurate torus needs a compound built from several shapes, or a mesh collider if it can be static.
Summary
| Symptom | Cause | Fix |
|---|---|---|
| Mesh moves, collider does not | Writing Transform.Position on a body-owning entity |
Teleport(...), or a kinematic body driven by velocity |
| Transform writes "start working" after a while | Body fell asleep and stopped syncing | Treat sleeping as the real state; do not write the transform directly |
| Velocity-driven motion stops | LinearVelocity does not wake a body |
Also set Awake = true |
Kinematic body diverges to NaN |
SetTargetPose called more than once per physics tick |
Call it from SimulationUpdate, or integrate your own velocity |
Null reference in SimulationUpdate |
It can run before Start |
Resolve lazily with ??= |
Inert BodyComponent |
A body whose compound holds no shapes | IncludePhysics = false for no physics; your own shapes in the compound otherwise |
AccessViolationException in the solver |
Unknown; intermittent, seen only with hull shapes at scale | None known - keep hull body counts down, or use an analytic collider |
| Slow spawns and finalizer churn with hull shapes | A hull per body, freed from a finalizer into a static pool | Share one DecomposedHulls per shape; the toolkit does this already |
Stack overflow in Refit2WithCacheOptimization |
A degenerate broad-phase tree from a perfectly regular lattice | Jitter the spawn positions, or space the bodies apart |
| Runaway memory in a 2D prism pile | Zeroing some inverse inertia terms and not others | Scale the terms instead of zeroing them; fixed in Body2DComponent |
| Torus collides with its hole filled | A convex hull cannot represent a concave shape | Build a compound, or use a mesh collider for statics |
Note
Do not combine Bepu and Bullet physics components on the same entity.