Table of Contents

Physics Extensions

Stride ships two physics engines, and the toolkit wraps both. The scene setup chooses the engine, new SceneSetup3D().UseBepu() or .UseBullet(), and the core's CreatePrimitive, Add3DGround and Add2DGround attach that engine's bodies and colliders; each package's entity extensions attach its physics explicitly, so both namespaces can be imported together. A third, Box2D.NET, is not a Stride engine at all but a 2D world the toolkit runs beside Stride; its wrapper is at the end of this page.

Bepu is the newer engine and the toolkit's default; the examples choose it with UseBepu() unless they say otherwise. Bullet is maintained, not extended: the package follows every toolkit change and its three examples stay, but new helpers and examples are written for Bepu, the engine Stride itself is moving to.

Tip

New to Bepu, or seeing a mesh that moves without colliding? Read Bepu: Who Owns the Transform? first. It covers the one-way physics-to-transform sync and the silent failures that follow from it.

Building joints or motors? Bepu: Why Isn't My Constraint Doing Anything? covers the equivalent silent failures on the constraint side - jammed joints, motors that produce no force, and settings that are discarded without warning.

Want something to bounce or slide? Bepu: Bounce and Friction lists the four contact properties, their defaults, and how two collidables combine them.

Bepu

Scene setup

  • UseBepu() - Makes Bepu the physics engine of a SceneSetup3D or SceneSetup2D: game.Run(new SceneSetup3D().UseBepu(), start: Start) is a scene you can fly around, with a ground that collides.
  • BepuPhysicsSetup - What UseBepu() sets: the core's CreatePrimitive then attaches a BodyComponent (a Body2DComponent for a flat shape) with the collider fitted to the shape, and the grounds a StaticComponent.
  • Add3DGround() and Add2DGround() are the core's; with Bepu chosen, the ground gets a static collider on its own, when you do not want the rest of the scene setup.

Primitives with colliders

  • CreatePrimitive(Primitive3D) and CreatePrimitive(Primitive2D) are the core's; with Bepu chosen, the entity comes with a matching Bepu collider. A Bepu3DPhysicsOptions or Bepu2DPhysicsOptions passed as the options sets the body component and whether a collider is included; IncludePhysics = false on any options leaves the physics out. Mass on the options is the mass of the fitted collider shape, where Bepu keeps it.

Raycasting

  • Raycast() - Casts from the camera through a screen position and reports the first hit.
  • RaycastMouse() - The same, reading the mouse position for you.
  • BepuSimulation.RayCast() - Casts a RaySegment directly, with no camera and no maximum distance to choose: the segment's own length is the limit. Pairs with ScreenToWorldRaySegment().
  • RayCastPenetrating() - Returns every hit along the segment rather than stopping at the first. An overload fills a collection you supply, to avoid allocating.

A raycast sees colliders only. For what the eye sees under a screen point - a mesh with no body, an instanced model, a teapot - the Effects package has a GPU picker that reads the answer from the rendered frame.

Shape queries

A raycast asks what a line meets first. Bepu also answers what a convex shape meets, in place or along a path, with the same HitInfo a raycast gives:

  • BepuSimulation.Overlap(shape, pose, buffer) lists every collidable the shape intersects at that pose. The Span<CollidableStack> overload writes into stack memory and allocates nothing; the ICollection<OverlapInfo> overload appends records that also carry the penetration direction and depth of each.
  • BepuSimulation.SweepCast(shape, pose, velocity, maxDistance, out hit) throws the shape along the velocity and reports the first contact: the point, the normal, the collidable and how far along the path it happened. SweepCastPenetrating collects every contact on the way.
Span<CollidableStack> buffer = stackalloc CollidableStack[32];

foreach (var collidable in simulation.Overlap(new Box(2f, 1.5f, 2f), new RigidPose(centre, Quaternion.Identity), buffer))
{
    // Every body and static the box intersects right now
}

if (simulation.SweepCast(new Sphere(0.35f), new RigidPose(start, Quaternion.Identity), new BodyVelocity(direction, Vector3.Zero), 16f, out var hit))
{
    var touch = start + direction * hit.Distance;   // Where the sphere's centre is when it first touches hit.Collidable
}

The shapes are Bepu's own (BepuPhysics.Collidables.Box, Sphere, Capsule, a convex hull), not the toolkit's colliders. maxDistance and hit.Distance are measured along the velocity: with a unit direction they are metres, with a real velocity they are seconds. A sweep is a straight line; gravity is not part of it. E05_3D_ShapeQueries shows both queries, and E05_3D_Raycast shows RayCastPenetrating listing every hit along a ray.

Triggers and contact events

A collidable's ContactEventHandler is told about its contacts, and the handler decides whether the collidable is solid:

  • Trigger is the engine's ready-made handler for a sensor volume. Its NoContactResponse is true, so bodies pass through as if the collidable were not there, and OnEnter and OnLeave fire with the other collidable. A static with a BoxCollider and no model is a zone.
  • Your own IContactHandler returns NoContactResponse false for a solid that also wants to know: OnStartedTouching, OnTouching and OnStoppedTouching receive the Contacts, with Other, every contact's point, normal and depth, and ComputeImpactForce(contact) for how hard a first touch was.
var trigger = new Trigger();

trigger.OnEnter += (_, other) => Console.WriteLine($"{other.Entity.Name} came in");
trigger.OnLeave += (_, other) => Console.WriteLine($"{other.Entity.Name} left");

var zone = new Entity("Zone")
{
    new StaticComponent { Collider = new CompoundCollider { Colliders = { new BoxCollider { Size = new Vector3(3f, 2f, 3f) } } }, ContactEventHandler = trigger },
};

The callbacks run on the main thread after the physics step, so they may change entities and materials. OnTouching does not fire for sleeping pairs. One body can touch through several contacts and several children of a compound, so keep a set of who touches, not a count. E05_3D_TriggerZones shows a zone, a solid plate that counts what stands on it, and a crate that measures its hits.

The simulation clock

BepuSimulation runs on a clock of its own, in fixed steps, and three of its properties can change while the game runs:

  • TimeScale runs it slower or faster than the game. It stacks with GameTime.Factor, so a game that slows its whole clock slows its physics with it.
  • Enabled pauses it; bodies hang where they are until it is set again.
  • PoseGravity takes effect on the next step, on every body.

A simulation running faster than the game needs more fixed steps per frame: at four times the speed and sixty frames a second it needs four, and the default MaxStepPerFrame of three lets it fall behind the clock. Raise the cap when a game runs fast. To read the simulation's clock exactly, give a component ISimulationUpdate: it is called once per fixed step with the step's length, before the step and after it. E05_3D_TimeControl puts a dropper on the game clock and a spinning bar on the physics clock, so the difference can be watched.

Grabbing bodies

  • GrabberScript - game.AddGrabber() puts it on the camera entity, or add it there in Game Studio under Physics, and any dynamic body can be picked up with the mouse, carried on the end of the camera ray, and thrown. The wheel changes the carry distance; T plus mouse movement turns the held body. The pick looks through trigger volumes, so a body inside a zone can still be grabbed; a solid static in front still hides what is behind it. It works through a linear and an angular servo constraint rather than by moving the body, so the held body still collides and pushes, and its force caps scale with mass so heavy and light bodies drag alike. Grab() and Release() do the same from code.
game.AddGrabber();

The same call serves a 2D scene: under an orthographic camera the pick ray runs along the view, a Body2DComponent is carried across its plane, and the mouse wheel does nothing for a 2D body or under an orthographic camera, since depth there is the plane itself.

Debug views

  • AddPhysicsDebug() - One entity carrying Bepu's two scene-wide debug views: CollidableGizmoScript draws every collider on P, and DebugRenderComponentScript turns the physics debug meshes on for every Bepu entity on F11. Both act on the whole scene, so one entity is all a scene needs; a second copy of either script answers the same key and toggles the first one back. contactsVisible: true adds the solver's contact points and normals to the debug meshes (Stride 4.4.1 or later).
game.AddPhysicsDebug(gizmoColor: new Color4(0.4f, 0.843f, 0, 0.9f));

E08_3D_CollidableGizmo and E08_3D_DebugRenderComponent show one view each; E05_3D_TriggerZones, E05_3D_ShapeQueries and E05_3D_MeshColliders carry both.

Convex hulls

In the Stride.CommunityToolkit.Bepu namespace, for giving an arbitrary mesh a collider.

  • ToConvexHullCollider() - Wraps mesh data in a single convex hull. Cheap, but it fills in any concave detail.
  • ToDecomposedHulls() - Splits the mesh into several hulls so concave shapes collide correctly. More accurate, more expensive to build.

Mesh colliders

The engine's MeshCollider is the model's own triangles, so a hole stays a hole where a hull would fill it. It needs the model, so it is attached once the entity has one: create the primitive with IncludePhysics = false, then add the component.

var ring = game.CreatePrimitive(Primitive3D.Torus(1.2f, 0.35f), new Bepu3DPhysicsOptions { IncludePhysics = false });

ring.Add(new StaticComponent { Collider = new MeshCollider { Model = ring.Get<ModelComponent>().Model, Closed = true } });

What the triangles are not, measured in E05_3D_MeshColliders: a mesh is a surface with no inside, so a body that starts inside a closed mesh falls out through it; two meshes do meet, triangle against triangle, so a moving mesh works but costs more than a hull. Closed says the surface has no holes and is what the inertia of a moving mesh is computed from. The entity's scale scales the mesh.

Bullet

Scene setup

  • UseBullet() - Makes Bullet the physics engine of a scene setup, the counterpart of UseBepu().
  • BulletPhysicsSetup - What UseBullet() sets: the core's CreatePrimitive then attaches a RigidbodyComponent with the collider fitted to the shape, and the grounds a StaticColliderComponent.
  • AddInfinite3DGround() - A ground plane nothing can fall off the edge of. Bullet only; there is no Bepu equivalent.

Primitives and colliders

  • CreatePrimitive(Primitive3D) and CreatePrimitive(Primitive2D) are the core's; with Bullet chosen, the entity comes with a matching Bullet collider. A Bullet3DPhysicsOptions or Bullet2DPhysicsOptions passed as the options sets the physics component and whether a collider is included. Mass on the options is the mass of the rigid body.
  • AddBulletPhysics() - Attaches a collider to an entity you already built, rather than creating both together.
  • AddBulletPhysics() - The 2D equivalent.

Raycasting

  • Raycast() - Casts from the camera through a screen position. Takes either a ScriptComponent or a Simulation.
  • RaycastMouse() - The same, reading the mouse position for you.
  • Simulation.Raycast() - Casts a RaySegment directly, with no camera involved. Overloads also cast from an entity along a direction.
  • RaycastPenetrating() - Returns every hit along the ray rather than stopping at the first. An overload fills a list you supply, to avoid allocating.

Debugging

  • ShowColliders() - Draws the collider shapes over the scene, which is how you find out that the collider is not where the model is.

Box2D

In Stride.CommunityToolkit.Box2D, for a 2D world run by Box2D.NET beside Stride. The wrapper is built around Box2DSimulation, which owns the world and steps it; see the Box2D examples for the whole pattern.

Joints

  • Joints2D - Every Box2D joint type with world-space anchors: CreateRevolute, CreatePrismatic, CreateWheel, CreateDistance, CreateWeld, CreateMotor, CreateFilter, plus Destroy, IsValid and GetAnchors for drawing. A raw definition wants each anchor as a local frame in the body's own space; these take the pivot and the axis in world space and work the frames out, with the pose at creation as the joint's zero.
  • Box2DSimulation.Joints - The same factories with the world id filled in, and overloads that take the entities the simulation created bodies for.
  • Options records - RevoluteJointOptions, PrismaticJointOptions, WheelJointOptions, DistanceJointOptions, WeldJointOptions, MotorJointOptions - mirror the per-type knobs under Box2D's names. A property left null keeps Box2D's default, so an initialiser changes exactly what it names.
var hinge = simulation.Joints.CreateRevolute(post, arm, pivot, new RevoluteJointOptions
{
    EnableMotor = true,
    MotorSpeed = 2,
    MaxMotorTorque = 500,
});

Debug drawing

  • Box2DDebugDraw - Box2D's own debug draw, rendered through a ShapeBatch: one Draw(simulation) call a frame shows every shape, joint, contact point, force, bounding box, island and body name the testbed can, with the testbed's toggles as properties. Shapes and joints are on by default; switch DrawShapes off when your entities already draw themselves.
var debugDraw = new Box2DDebugDraw(shapeBatch) { DrawShapes = false, DrawContactPoints = true };

// each frame, after the simulation update
debugDraw.Draw(simulation);

Terrain

  • ShapeFixtureBuilder.AttachChain() - A chain of segments for terrain, tracks and room walls: unlike a row of separate segments it has no internal corners for a rolling body to catch on. Two rules Box2D imposes, both handled or documented: a chain collides on the right of its direction of travel, so a floor is listed right to left; and an open chain needs a ghost point at each end, which the method adds for you.
  • SvgPath2D.Parse() - The straight-line commands of an SVG path as points, so a level outline drawn in Inkscape becomes a chain: offset, scale, and the y flip from SVG's downward axis. Curves are refused; flatten them in the editor.

Queries

  • PhysicsQueries2D.CastCircleClosest() and CastSegmentClosest() - Shape casts: sweep a circle, a point or a segment along a displacement and get the closest hit with its point, normal and fraction, filtered by category bits. CastShapeClosest() takes any convex proxy. The same calls exist on Box2DSimulation, alongside the raycasts and overlaps.

Characters

  • CharacterMover2D - A platformer character with no rigid body, on Box2D v3's mover API: a capsule the game moves itself, Quake style, that asks the world only what it touches - collect the contact planes, solve a translation that respects them, sweep it - with a pogo shape cast from the feet that floats it above the ground and decides whether it is standing. Register it with the simulation and it steps after every fixed physics step; set Throttle and call Jump() from your update; give it an Entity and it moves the transform. How hard a shape pushes back is per shape through SetResponse(): a small push limit makes a soft obstacle the mover walks through, an elevator gets a firm one and velocity clipping so it carries the mover.
var mover = new CharacterMover2D(new Vector2(2, 8)) { Entity = hero };
simulation.RegisterSimulationUpdate(mover);

// each frame
mover.Throttle = (input.IsKeyDown(Keys.D) ? 1 : 0) - (input.IsKeyDown(Keys.A) ? 1 : 0);
if (input.IsKeyPressed(Keys.Space)) mover.Jump();

Explosions

  • Box2DSimulation.Explode() - A radial blast: every shape within the radius gets an impulse away from the centre, per metre of its perimeter facing the blast, fading over the falloff distance beyond the radius.

Grabbing bodies

  • Grabber2DScript - The 2D counterpart of GrabberScript: on the camera entity, given the simulation, the left button picks any dynamic body up, drags it and throws it. Box2D v3 has no mouse joint, so it uses a kinematic anchor moved to the cursor and a motor joint with a mass-scaled force cap and angular friction, the idiom of Box2D's own sample browser. Every Box2D example carries it.
game.GetCameraEntity().Add(new Grabber2DScript { Simulation = simulation });