Box2D Explosion
A grenade in one call: Explode gives every shape within a radius an impulse away from the centre, per metre of perimeter facing the blast, so a wide slab flies harder than a ball of the same mass. Space detonates at the cursor, rings show the radius and the falloff band, J and K set the impulse, and the grabber on the camera rebuilds the pyramid by hand between blasts.
The Program.cs file shows how to:
- Radial impulses with Box2DSimulation.Explode - radius, falloff and impulse per length
- Why the impulse is per metre of perimeter, shown with a slab and a ball of equal mass
- Drawing a transient effect with ShapeBatch rings that fade
- Picking bodies up 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 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;
// A grenade in one call: Box2D's explosion gives every shape within a radius an impulse away from
// the centre, and the impulse is per metre of the shape's perimeter facing the blast - so a broad
// flat body catches more of it than a small round one of the same mass, the way a pressure wave
// would. Space detonates at the cursor; the rings show the full-strength radius and the falloff
// band beyond it. Two bodies of the same mass stand side by side on the right, one a wide slab and
// one a ball, to make the perimeter rule visible. The grabber on the camera rebuilds the pyramid
// by hand between blasts, or R does it for you.
const float Radius = 5f;
const float Falloff = 3f;
const float RingSeconds = 0.6f;
var impulse = 12f;
Box2DSimulation? simulation = null;
ShapeBatch? shapeBatch = null;
CameraComponent? camera = 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 salmon = new Color(0xFA, 0x80, 0x72);
var background = new Color(0.2f, 0.2f, 0.2f);
List<Entity> spawned = [];
Vector2? lastBlast = null;
var ringAge = float.MaxValue;
var blasts = 0;
var sinceStart = 0f;
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 Explosion - Stride Community Toolkit";
game.SetupBase2D(clearColor: background);
var cameraEntity = game.Add2DCameraController();
game.AddProfiler();
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();
// Left mouse picks bodies up: rebuild the pyramid by hand, or throw something into the blast.
cameraEntity.Add(new Grabber2DScript { Simulation = simulation });
BuildScene(rootScene);
AddInstructions();
}
void BuildScene(Scene scene)
{
Ground(scene);
// A pyramid of boxes to knock down.
const int rows = 7;
const float box = 1f;
for (var row = 0; row < rows; row++)
{
var count = rows - row;
var y = box / 2 + row * box;
for (var i = 0; i < count; i++)
{
var x = -8 + (i - (count - 1) / 2f) * box * 1.05f;
DynamicBox(scene, new Vector2(x, y), new Vector2(box, box), pink);
}
}
// Balls to scatter.
for (var i = 0; i < 5; i++)
DynamicCircle(scene, new Vector2(2 + i * 1.6f, 0.5f), 0.45f, royalBlue);
// The perimeter rule: a wide slab and a ball with the same mass, at the same distance from a
// blast centred between them. The slab shows the wave more of its edge, so it flies harder.
DynamicBox(scene, new Vector2(13, 2), new Vector2(4, 0.5f), gold, density: 1f);
DynamicCircle(scene, new Vector2(19, 0.8f), 0.8f, gold, density: 4f / (MathF.PI * 0.8f * 0.8f));
}
void Update(Scene rootScene, GameTime time)
{
simulation?.Update(time.Elapsed);
var dt = (float)time.Elapsed.TotalSeconds;
// One blast on its own a second in, at the pyramid, so the scene shows what it is about before a key is pressed.
sinceStart += dt;
if (blasts == 0 && sinceStart > 1f)
Detonate(new Vector2(-8, 1));
if (game.Input.IsKeyPressed(Keys.Space) && camera?.CalculateRayPlaneIntersectionPoint(game.Input.MousePosition) is { } point)
{
Detonate(point);
}
var change = (game.Input.IsKeyDown(Keys.K) ? 1 : 0) - (game.Input.IsKeyDown(Keys.J) ? 1 : 0);
if (change != 0)
impulse = Math.Clamp(impulse + change * 10 * dt, 1, 60);
if (game.Input.IsKeyPressed(Keys.R))
Reset(rootScene);
ringAge += dt;
DrawBlast();
}
void Detonate(Vector2 point)
{
simulation!.Explode(point, Radius, impulse, Falloff);
lastBlast = point;
ringAge = 0;
blasts++;
}
// The blast is a moment; the rings are how it is seen: the full-strength radius inside, the
// falloff band outside, fading over a fraction of a second.
void DrawBlast()
{
if (shapeBatch is null || lastBlast is not { } centre || ringAge > RingSeconds) return;
var alpha = 1 - ringAge / RingSeconds;
var ring = salmon;
ring.A = (byte)(255 * alpha);
var band = gold;
band.A = (byte)(120 * alpha);
shapeBatch.DrawAnnulus(centre, Radius + Falloff, Radius, band);
shapeBatch.DrawAnnulus(centre, Radius, Radius - 0.15f, ring);
}
void Reset(Scene scene)
{
foreach (var entity in spawned)
{
simulation!.RemoveBody(entity);
entity.Scene = null;
}
spawned.Clear();
lastBlast = null;
BuildScene(scene);
}
void Ground(Scene scene)
{
var entity = new Entity("Ground") { new ShapeComponent { Vertices = [.. Rectangle(26, 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(52, 1), body);
}
void DynamicBox(Scene scene, Vector2 centre, Vector2 size, Color color, float density = 1f)
{
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, ShapeFixtureBuilder.CreateCustomShapeDef(density, 0.6f, 0.1f));
entity.Add(new Box2DBodyComponent { BodyId = body });
}
void DynamicCircle(Scene scene, Vector2 centre, float radius, Color color, float density = 1f)
{
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, ShapeFixtureBuilder.CreateCustomShapeDef(density, 0.6f, 0.1f));
entity.Add(new Box2DBodyComponent { BodyId = body });
}
void AddInstructions()
{
var overlay = DebugOverlay.GetOrCreate(game);
overlay.AddSection("Explosion", () =>
[
new("Space", "Detonate at the cursor", Color.Yellow),
new(["J", "K"], $"Impulse per metre of perimeter, {impulse:0.0}", Color.Yellow),
new("R", "Rebuild", Color.Yellow),
new("Left mouse", "Pick a body up and throw it", Color.Yellow),
new(""),
new($"Radius {Radius} m, falloff {Falloff} m, blasts {blasts}"),
new("Gold slab and ball on the right: same mass, same", Color.Gray),
new("distance, and the slab flies harder", Color.Gray),
]);
}
static Vector2[] Rectangle(float halfWidth, float halfHeight) =>
[
new(-halfWidth, -halfHeight),
new(halfWidth, -halfHeight),
new(halfWidth, halfHeight),
new(-halfWidth, halfHeight),
];