Box2D Events
Shapes drop through a sensor zone onto a floor and one handler listens to everything Box2D reports: begin and end touch, hits with their approach speed, and a sensor's visitors. Every kind is opt-in on the shape definition, where the library's defaults are all off, and switched on the simulation as well; the keys flip the simulation's switches to show which lines of the log each one owns. A shape inside the sensor turns gold and a hit shape flashes.
The Program.cs file shows how to:
- IContactEventHandler and ISensorEventHandler registered on the Box2DSimulation
- enableContactEvents, enableHitEvents and enableSensorEvents on the shape definition, all off by default
- A sensor shape that detects and never collides
- The hit event's approach speed as a measure of the impact
- Using helpers: SceneSetup2D, Box2DSimulation, ShapeFixtureBuilder, ShapeComponent, AddGrabber2D, TextOverlay
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.Overlay;
using Stride.CommunityToolkit.Rendering.ProceduralModels;
using Stride.CommunityToolkit.Rendering.Text;
using Stride.CommunityToolkit.Shapes;
using Stride.Core.Mathematics;
using Stride.Engine;
using Stride.Games;
using Stride.Input;
// What Box2D tells you about touching, and what you have to ask for. Shapes drop through a sensor
// zone onto a floor, and one handler listens to all three kinds of event the library delivers:
//
// begin and end touch - a pair of shapes starts and stops touching;
// hit - an impact, with the point, the normal and the approach speed;
// sensor - a shape enters or leaves a sensor, a shape that detects and never collides.
//
// Every one of them is opt-in on the shape: Box2D.NET's default shape definition has contact, hit and
// sensor events all off, so a shape that does not say enableContactEvents reports nothing however
// loudly it lands. The simulation has a switch for each kind as well, the second gate, and the keys
// here flip those to show which lines of the log each one owns. A shape inside the sensor turns gold;
// a shape that is hit flashes, brighter the faster it came in.
const float FloorHalfWidth = 22f;
const float SpawnHeight = 20f;
const int BatchSize = 8;
const int LogLines = 8;
var pink = new Color(0xFF, 0xC0, 0xCB);
var paleGreen = new Color(0x98, 0xFB, 0x98);
var royalBlue = new Color(0x41, 0x69, 0xE1);
var wheat = new Color(0xF5, 0xDE, 0xB3);
var gold = new Color(0xFF, 0xD7, 0x00);
var log = new EventLog(LogLines);
var shapes = new List<(Entity Entity, ShapeComponent Component, Color Colour)>();
var random = new Random(5);
using var game = new Game();
using var simulation = new Box2DSimulation();
var sceneSetup = new SceneSetup2D
{
ClearColor = new Color(0.2f, 0.2f, 0.2f),
Camera = CameraSetup.Orthographic(position: new Vector3(0f, 9f, 50f), orthographicSize: 30f).WithController(),
Ground = null,
WindowTitle = "Box2D Events - Stride Community Toolkit",
Resizable = true,
};
game.Run(sceneSetup, start: Start, update: Update);
void Start(Scene scene)
{
var batch = game.AddShapeBatch();
batch.BorderWidth = 1.5f;
batch.Fill.Alpha = 0.4f;
simulation.Gravity = new Vector2(0f, -10f);
// The handler hears every shape that opted in; one object serves both interfaces
simulation.RegisterContactEventHandler(log);
simulation.RegisterSensorEventHandler(log);
// Left mouse picks a shape up and throws it, which is the quickest way to make a hit
game.AddGrabber2D(simulation);
BuildFloor(scene);
BuildSensor(scene);
Spawn(scene);
TextOverlay.GetOrCreate(game).AddSection("Box2D events", BuildOverlayLines);
}
void Update(Scene scene, GameTime time)
{
simulation.Update(time.Elapsed);
var input = game.Input;
if (input.IsKeyPressed(Keys.Space)) Spawn(scene);
if (input.IsKeyPressed(Keys.C)) Clear();
if (input.IsKeyPressed(Keys.E)) simulation.EnableContactEvents = !simulation.EnableContactEvents;
if (input.IsKeyPressed(Keys.I)) simulation.EnableHitEvents = !simulation.EnableHitEvents;
if (input.IsKeyPressed(Keys.N)) simulation.EnableSensorEvents = !simulation.EnableSensorEvents;
log.Age((float)time.Elapsed.TotalSeconds);
// Every shape's colour each frame: gold inside the sensor, a white flash fading after a hit, else its own
foreach (var (entity, component, colour) in shapes)
{
var flash = log.FlashOf(entity);
var resting = log.IsInsideSensor(entity) ? gold : colour;
component.Color = flash > 0f ? Color.Lerp(resting, Color.White, flash) : resting;
}
}
void BuildFloor(Scene scene)
{
var size = new Vector2(FloorHalfWidth * 2f, 1f);
var entity = new Entity("Floor") { new ShapeComponent { Vertices = [.. ShapeComponent.Box(size)], Color = paleGreen } };
entity.Transform.Position = new Vector3(0f, -0.5f, 0f);
entity.Scene = scene;
var body = simulation.CreateStaticBody(entity, new Vector3(0f, -0.5f, 0f));
// The floor reports too: a hit is between two shapes, and either may be the one that opted in
ShapeFixtureBuilder.AttachShape(Primitive2D.Rectangle(size.X, size.Y), body, Reporting(ShapeFixtureBuilder.CreateDefaultShapeDef()));
}
/// <summary>A sensor across the middle of the drop: it detects what passes and stops nothing.</summary>
void BuildSensor(Scene scene)
{
var size = new Vector2(16f, 5f);
var centre = new Vector3(0f, 9f, 0f);
var entity = new Entity("Sensor") { new ShapeComponent { Vertices = [.. ShapeComponent.Box(size)], Color = wheat, FillAlpha = 0.12f } };
entity.Transform.Position = centre;
entity.Scene = scene;
var body = simulation.CreateStaticBody(entity, centre);
var definition = ShapeFixtureBuilder.CreateCustomShapeDef(1f, 0.6f, 0f, isSensor: true);
// A sensor needs its own flag on, and so does every shape it is to notice
definition.enableSensorEvents = true;
ShapeFixtureBuilder.AttachShape(Primitive2D.Rectangle(size.X, size.Y), body, definition);
}
/// <summary>A row of boxes and discs above the sensor, each a little off the one beside it so they land at different times.</summary>
void Spawn(Scene scene)
{
for (var i = 0; i < BatchSize; i++)
{
var x = (i - (BatchSize - 1) / 2f) * 2.4f + (random.NextSingle() - 0.5f);
var position = new Vector3(x, SpawnHeight + random.NextSingle() * 4f, 0f);
if (i % 2 == 0)
{
AddBox(scene, position, new Vector2(1.2f + random.NextSingle(), 1.2f + random.NextSingle()), i % 4 == 0 ? pink : royalBlue);
}
else
{
AddDisc(scene, position, 0.6f + random.NextSingle() * 0.5f, i % 4 == 1 ? royalBlue : pink);
}
}
}
void AddBox(Scene scene, Vector3 position, Vector2 size, Color colour)
{
var component = new ShapeComponent { Vertices = [.. ShapeComponent.Box(size)], Color = colour };
var entity = new Entity("Box") { component };
entity.Transform.Position = position;
entity.Scene = scene;
var body = simulation.CreateDynamicBody(entity, position, random.NextSingle());
ShapeFixtureBuilder.AttachShape(Primitive2D.Rectangle(size.X, size.Y), body, Reporting(ShapeFixtureBuilder.CreateDefaultShapeDef()));
entity.Add(new Box2DBodyComponent { BodyId = body });
shapes.Add((entity, component, colour));
}
void AddDisc(Scene scene, Vector3 position, float radius, Color colour)
{
var component = new ShapeComponent { Vertices = [Vector2.Zero], Radius = radius, Color = colour };
var entity = new Entity("Disc") { component };
entity.Transform.Position = position;
entity.Scene = scene;
var body = simulation.CreateDynamicBody(entity, position);
ShapeFixtureBuilder.AttachShape(Primitive2D.Circle(radius), body, Reporting(ShapeFixtureBuilder.CreateDefaultShapeDef()));
entity.Add(new Box2DBodyComponent { BodyId = body });
shapes.Add((entity, component, colour));
}
// The three opt-ins on a shape. Without them the shape is silent: the library's defaults are all off
static B2ShapeDef Reporting(B2ShapeDef definition)
{
definition.enableContactEvents = true;
definition.enableHitEvents = true;
definition.enableSensorEvents = true;
return definition;
}
void Clear()
{
foreach (var (entity, _, _) in shapes)
{
simulation.RemoveBody(entity);
entity.Scene = null;
}
shapes.Clear();
log.Forget();
}
IReadOnlyList<TextElement> BuildOverlayLines()
{
var lines = new List<TextElement>
{
new("Space", "Drop a batch", Color.Gold),
new("C", "Clear the shapes", Color.Gold),
new("Left mouse", "Grab and throw a shape", Color.Gold),
new("E", simulation.EnableContactEvents ? "Begin and end touch events: on" : "Begin and end touch events: off", simulation.EnableContactEvents ? Color.LightGreen : Color.OrangeRed),
new("I", simulation.EnableHitEvents ? "Hit events: on" : "Hit events: off", simulation.EnableHitEvents ? Color.LightGreen : Color.OrangeRed),
new("N", simulation.EnableSensorEvents ? "Sensor events: on" : "Sensor events: off", simulation.EnableSensorEvents ? Color.LightGreen : Color.OrangeRed),
new(""),
new($"Touches begun {log.Begins}, ended {log.Ends}, hits {log.Hits}, inside the sensor {log.InsideCount}", Color.White),
new(""),
};
foreach (var line in log.Lines) lines.Add(new(line, Color.LightGray));
return lines;
}
/// <summary>
/// One listener for both kinds of event: it keeps the last few as text, counts them, tracks who is inside a
/// sensor, and times a flash for every shape that was hit, scaled by how fast it came in.
/// </summary>
public sealed class EventLog(int capacity) : IContactEventHandler, ISensorEventHandler
{
private const float FlashSeconds = 0.4f;
private const float LoudSpeed = 12f;
private readonly Queue<string> _lines = new();
private readonly Dictionary<Entity, int> _inside = [];
private readonly Dictionary<Entity, float> _flashes = [];
private readonly List<Entity> _faded = [];
public int Begins { get; private set; }
public int Ends { get; private set; }
public int Hits { get; private set; }
public int InsideCount => _inside.Count;
public IEnumerable<string> Lines => _lines;
public bool IsInsideSensor(Entity entity) => _inside.ContainsKey(entity);
/// <summary>How bright the entity's flash is now, 1 at the hit and 0 once it has faded.</summary>
public float FlashOf(Entity entity) => _flashes.TryGetValue(entity, out var flash) ? flash : 0f;
public void OnContactEvent(ContactEventData data)
{
switch (data.Type)
{
case ContactEventType.BeginTouch:
Begins++;
Add($"{data.EntityA.Name} touches {data.EntityB.Name}");
break;
case ContactEventType.EndTouch:
Ends++;
Add($"{data.EntityA.Name} leaves {data.EntityB.Name}");
break;
case ContactEventType.Hit:
Hits++;
Add($"{data.EntityA.Name} hits {data.EntityB.Name} at {data.ApproachSpeed:0.0} m/s");
// The flash goes on whichever of the pair is a dropped shape; a static floor keeps its colour anyway
var brightness = Math.Clamp(data.ApproachSpeed / LoudSpeed, 0.3f, 1f);
_flashes[data.EntityA] = brightness;
_flashes[data.EntityB] = brightness;
break;
}
}
public void OnSensorEvent(SensorEventData data)
{
// Counted, not toggled: a shape that overlaps two sensor shapes leaves when it has left both
if (data.Type == SensorEventType.BeginTouch)
{
_inside[data.VisitorEntity] = _inside.GetValueOrDefault(data.VisitorEntity) + 1;
Add($"{data.VisitorEntity.Name} enters the sensor");
}
else if (_inside.TryGetValue(data.VisitorEntity, out var depth))
{
if (depth <= 1) _inside.Remove(data.VisitorEntity); else _inside[data.VisitorEntity] = depth - 1;
Add($"{data.VisitorEntity.Name} leaves the sensor");
}
}
/// <summary>Fades every flash by the frame's time.</summary>
public void Age(float elapsed)
{
_faded.Clear();
foreach (var (entity, flash) in _flashes)
{
var next = flash - elapsed / FlashSeconds;
if (next <= 0f) _faded.Add(entity); else _flashes[entity] = next;
}
foreach (var entity in _faded) _flashes.Remove(entity);
}
public void Forget()
{
_inside.Clear();
_flashes.Clear();
}
private void Add(string line)
{
_lines.Enqueue(line);
while (_lines.Count > capacity) _lines.Dequeue();
}
}