Box2D Joints
Every Box2D joint, one rig each, in a row you can pull on: a hinge pendulum with a motor and a limit, a slider on a spring, a wheel on a suspension, a rope of distance joints, a soft weld and a motor joint that springs its box back home. Joints2D takes world-space pivots and axes and turns them into the local frames Box2D wants, with options records for the per-type settings. The grabber on the camera picks any body up, and the joints are drawn from their anchors.
The Program.cs file shows how to:
- Creating every Box2D joint type through Joints2D with world-space pivots and axes
- Options records that mirror the Box2D definition and leave the rest at Box2D's defaults
- Toggling a joint's motor and limit at runtime through the Box2D joint functions
- Drawing joints, contacts, bounds and mass with Box2DDebugDraw, Box2D's debug draw through ShapeBatch
- Pulling on constrained bodies with Grabber2DScript
- Using helpers: SetupBase2D, Add2DCameraController, AddShapeBatch, ShapeComponent
Note
This example references Stride.CommunityToolkit.Box2D, which is not on NuGet yet. Run it from a clone of the
repository, where the package is a project reference; a copy of the project on its own will not build.

View on GitHub.
using Box2D.NET;
using Stride.CommunityToolkit.Box2D;
using Stride.CommunityToolkit.Engine;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Scripts.Utilities;
using Stride.CommunityToolkit.Shapes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;
using Stride.Input;
using static Box2D.NET.B2RevoluteJoints;
using static Box2D.NET.B2WheelJoints;
// Every Box2D joint, one rig each, in a row you can pull on.
//
// A joint definition wants its anchors as local frames - a point and a rotation in each body's
// own space - which is why raw Box2D code is full of GetLocalPoint calls. Joints2D takes the pivot
// and the axis in world space and does that once; the pose at creation is the joint's zero. The
// options records carry the per-type knobs under Box2D's own names, and anything left unset keeps
// Box2D's default.
//
// Joints are only interesting when something pulls on them, so the grabber is on the camera: pick
// any body up and drag it, then watch what its joint allows. Left to right: a hinge pendulum with a
// motor and a limit, a slider on a spring, a wheel on a suspension, a rope of distance joints, a
// soft weld, and a motor joint that spins a box and springs it back home. The joints themselves are
// drawn by Box2D's own debug draw through the shape batch - Box2DDebugDraw - which can also show
// contact points, bounding boxes and centres of mass on request.
Box2DSimulation? simulation = null;
ShapeBatch? shapeBatch = null;
Box2DDebugDraw? debugDraw = null;
var pink = new Color(0xFF, 0xC0, 0xCB);
var paleGreen = new Color(0x98, 0xFB, 0x98);
var royalBlue = new Color(0x41, 0x69, 0xE1);
var gold = new Color(0xFF, 0xD7, 0x00);
var background = new Color(0.2f, 0.2f, 0.2f);
List<B2JointId> joints = [];
List<Entity> spawned = [];
B2JointId pendulumJoint = default;
B2JointId wheelJoint = default;
var pendulumMotor = false;
var pendulumLimit = false;
var wheelMotor = true;
using var game = new Game();
game.Run(start: Start, update: Update);
simulation?.Dispose();
void Start(Scene rootScene)
{
game.Window.AllowUserResizing = true;
game.Window.Title = "Box2D Joints - Stride Community Toolkit";
game.SetupBase2D(clearColor: background);
var cameraEntity = game.Add2DCameraController();
game.AddProfiler();
var camera = rootScene.GetCamera() ?? throw new InvalidOperationException("Camera not found in scene");
camera.Entity.Transform.Position = new Vector3(0, 7, 50);
camera.OrthographicSize = 26;
shapeBatch = game.AddShapeBatch();
shapeBatch.BorderWidth = 1f;
shapeBatch.Fill.Alpha = 0.4f;
simulation = new Box2DSimulation();
// Box2D's own debug draw through the batch: joints, and on request contacts, bounds and mass.
// Shapes stay off - the entities' ShapeComponents already draw the bodies.
debugDraw = new Box2DDebugDraw(shapeBatch) { DrawShapes = false, DrawJointExtras = true };
// The 2D grabber: left mouse picks any dynamic body up. Every rig below is built to be pulled.
cameraEntity.Add(new Grabber2DScript { Simulation = simulation });
BuildScene(rootScene);
AddInstructions();
}
void BuildScene(Scene scene)
{
Ground(scene);
// 1. Hinge: an arm hanging from a fixed pivot. M switches the motor, L the +-60 degree limit.
var pivotA = new Vector2(-15, 13);
var post = StaticBox(scene, pivotA, new Vector2(0.6f, 0.6f));
var arm = DynamicBox(scene, pivotA + new Vector2(0, -2), new Vector2(0.5f, 4), pink);
pendulumJoint = simulation!.Joints.CreateRevolute(post, arm, pivotA, new RevoluteJointOptions
{
MaxMotorTorque = 500,
MotorSpeed = 2,
LowerAngle = -MathUtil.DegreesToRadians(60),
UpperAngle = MathUtil.DegreesToRadians(60),
});
joints.Add(pendulumJoint);
// 2. Slider: a block on a horizontal rail, sprung back to the middle and limited to +-3.
var railCentre = new Vector2(-7, 9);
var rail = StaticBox(scene, railCentre, new Vector2(7, 0.3f));
var block = DynamicBox(scene, railCentre + new Vector2(0, 0.8f), new Vector2(1.2f, 1.2f), royalBlue);
joints.Add(simulation.Joints.CreatePrismatic(rail, block, railCentre, Vector2.UnitX, new PrismaticJointOptions
{
EnableSpring = true,
Hertz = 1,
DampingRatio = 0.3f,
EnableLimit = true,
LowerTranslation = -3,
UpperTranslation = 3,
}));
// 3. Wheel: hung from a post on a vertical suspension with a motor. J switches the motor.
var hub = new Vector2(1, 9);
var strut = StaticBox(scene, hub + new Vector2(0, 3), new Vector2(0.4f, 6));
var wheel = DynamicCircle(scene, hub, 1.2f, gold);
wheelJoint = simulation.Joints.CreateWheel(strut, wheel, hub, Vector2.UnitY, new WheelJointOptions
{
EnableSpring = true,
Hertz = 2,
DampingRatio = 0.5f,
EnableLimit = true,
LowerTranslation = -1.5f,
UpperTranslation = 1.5f,
EnableMotor = true,
MaxMotorTorque = 50,
MotorSpeed = 6,
});
joints.Add(wheelJoint);
// 4. Rope: four circles hanging from an anchor on soft, limited distance joints.
var anchor = new Vector2(8, 15);
var previous = StaticBox(scene, anchor, new Vector2(0.6f, 0.6f));
var previousPoint = anchor;
for (var i = 1; i <= 4; i++)
{
var point = anchor + new Vector2(0, -1.6f * i);
var link = DynamicCircle(scene, point, 0.45f, pink);
joints.Add(simulation.Joints.CreateDistance(previous, link, previousPoint, point, new DistanceJointOptions
{
EnableSpring = true,
Hertz = 4,
DampingRatio = 0.5f,
EnableLimit = true,
MinLength = 1.2f,
MaxLength = 2.0f,
}));
previous = link;
previousPoint = point;
}
// 5. Weld: a T of two boxes, joined by a soft weld that bends when pulled and springs back.
var stem = DynamicBox(scene, new Vector2(14, 1.6f), new Vector2(0.8f, 3.2f), royalBlue);
var bar = DynamicBox(scene, new Vector2(14, 3.6f), new Vector2(4, 0.8f), royalBlue);
joints.Add(simulation.Joints.CreateWeld(stem, bar, new Vector2(14, 3.2f), new WeldJointOptions
{
AngularHertz = 3,
AngularDampingRatio = 0.5f,
}));
// 6. Motor joint: spins a box and springs it back to where it started when you throw it.
var home = new Vector2(20, 9);
var homePost = StaticBox(scene, home, new Vector2(0.4f, 0.4f));
var spinner = DynamicBox(scene, home, new Vector2(2, 2), gold);
joints.Add(simulation.Joints.CreateMotor(homePost, spinner, home, new MotorJointOptions
{
AngularVelocity = 2,
MaxVelocityTorque = 100,
LinearHertz = 1.5f,
LinearDampingRatio = 0.4f,
MaxSpringForce = 400,
CollideConnected = false,
}));
}
void Update(Scene rootScene, GameTime time)
{
simulation?.Update(time.Elapsed);
if (game.Input.IsKeyPressed(Keys.M))
{
pendulumMotor = !pendulumMotor;
b2RevoluteJoint_EnableMotor(pendulumJoint, pendulumMotor);
}
if (game.Input.IsKeyPressed(Keys.L))
{
pendulumLimit = !pendulumLimit;
b2RevoluteJoint_EnableLimit(pendulumJoint, pendulumLimit);
}
if (game.Input.IsKeyPressed(Keys.J))
{
wheelMotor = !wheelMotor;
b2WheelJoint_EnableMotor(wheelJoint, wheelMotor);
}
if (game.Input.IsKeyPressed(Keys.N) && debugDraw is not null)
{
debugDraw.DrawContactPoints = !debugDraw.DrawContactPoints;
debugDraw.DrawContactNormals = debugDraw.DrawContactPoints;
}
if (game.Input.IsKeyPressed(Keys.G) && debugDraw is not null)
debugDraw.DrawBounds = !debugDraw.DrawBounds;
if (game.Input.IsKeyPressed(Keys.T) && debugDraw is not null)
debugDraw.DrawMass = !debugDraw.DrawMass;
if (game.Input.IsKeyPressed(Keys.R))
Reset(rootScene);
// One call draws every joint - and whatever else is switched on - from Box2D's own view of the world.
if (simulation is not null)
debugDraw?.Draw(simulation);
}
void Reset(Scene scene)
{
foreach (var joint in joints)
simulation!.Joints.Destroy(joint);
joints.Clear();
foreach (var entity in spawned)
{
simulation!.RemoveBody(entity);
entity.Scene = null;
}
spawned.Clear();
pendulumMotor = false;
pendulumLimit = false;
wheelMotor = true;
BuildScene(scene);
}
void Ground(Scene scene)
{
var entity = new Entity("Ground") { new ShapeComponent { Vertices = [.. Rectangle(24, 0.5f)], Color = paleGreen } };
entity.Transform.Position = new Vector3(0, -0.5f, 0);
entity.Scene = scene;
spawned.Add(entity);
var body = simulation!.CreateStaticBody(entity, new Vector3(0, -0.5f, 0));
ShapeFixtureBuilder.AttachShape(Primitive2DModelType.Rectangle, new Vector2(48, 1), body);
}
Entity StaticBox(Scene scene, Vector2 centre, Vector2 size)
{
var entity = new Entity("Static") { new ShapeComponent { Vertices = [.. Rectangle(size.X / 2, size.Y / 2)], Color = paleGreen } };
entity.Transform.Position = new Vector3(centre, 0);
entity.Scene = scene;
spawned.Add(entity);
var body = simulation!.CreateStaticBody(entity, new Vector3(centre, 0));
ShapeFixtureBuilder.AttachShape(Primitive2DModelType.Rectangle, size, body);
return entity;
}
Entity DynamicBox(Scene scene, Vector2 centre, Vector2 size, Color color)
{
var entity = new Entity("Box") { new ShapeComponent { Vertices = [.. Rectangle(size.X / 2, size.Y / 2)], Color = color } };
entity.Transform.Position = new Vector3(centre, 0);
entity.Scene = scene;
spawned.Add(entity);
var body = simulation!.CreateDynamicBody(entity, new Vector3(centre, 0));
ShapeFixtureBuilder.AttachShape(Primitive2DModelType.Rectangle, size, body);
entity.Add(new Box2DBodyComponent { BodyId = body });
return entity;
}
Entity DynamicCircle(Scene scene, Vector2 centre, float radius, Color color)
{
var entity = new Entity("Circle") { new ShapeComponent { Vertices = [Vector2.Zero], Radius = radius, Color = color } };
entity.Transform.Position = new Vector3(centre, 0);
entity.Scene = scene;
spawned.Add(entity);
var body = simulation!.CreateDynamicBody(entity, new Vector3(centre, 0));
ShapeFixtureBuilder.AttachShape(Primitive2DModelType.Circle, new Vector2(radius, radius), body);
entity.Add(new Box2DBodyComponent { BodyId = body });
return entity;
}
void AddInstructions()
{
var overlay = DebugOverlay.GetOrCreate(game);
overlay.AddSection("Joints", () =>
[
new("M", $"Hinge motor {(pendulumMotor ? "on" : "off")}", Color.Yellow),
new("L", $"Hinge limit {(pendulumLimit ? "on" : "off")}", Color.Yellow),
new("J", $"Wheel motor {(wheelMotor ? "on" : "off")}", Color.Yellow),
new("N", $"Draw contacts {(debugDraw?.DrawContactPoints == true ? "on" : "off")}", Color.Yellow),
new("G", $"Draw bounds {(debugDraw?.DrawBounds == true ? "on" : "off")}", Color.Yellow),
new("T", $"Draw mass {(debugDraw?.DrawMass == true ? "on" : "off")}", Color.Yellow),
new("R", "Rebuild the rigs", Color.Yellow),
new("Left mouse", "Pull on anything", Color.Yellow),
new(""),
new("Left to right: hinge, slider, wheel, rope, weld,", Color.LightGray),
new("motor joint", Color.LightGray),
]);
}
static Vector2[] Rectangle(float halfWidth, float halfHeight) =>
[
new(-halfWidth, -halfHeight),
new(halfWidth, -halfHeight),
new(halfWidth, halfHeight),
new(-halfWidth, halfHeight),
];