Recursive AI

Draw a 2D physics collider debug overlay in C#

onegame is a free, open-source game engine for .NET from Recursive AI. MIT or Apache-2.0, no editor required.

Inspect collider outlines over a 2D game. F3 enables Draw2D.DebugVisible in development; the rendering example needs a GPU.

Terminal
dotnet new console
dotnet add package RecursiveAI.OneGame.Physics

Requires .NET 10 (net10.0); .NET 9 and earlier aren’t supported.

Fixed and FVec2 are onegame’s deterministic fixed-point number and 2D vector types.

C#
using OneGame.Core;
using OneGame.Physics;
 
var space = new PhysicsSpace2D();
space.AddBox(new FVec2(2, 1), FVec2.Zero);
var shapes = new PhysicsDebugShapes();
space.AddDebugShapes(shapes);
Console.WriteLine(shapes.Lines.Count);

In the full program’s ColliderView, this method draws the collected lines when F3 has enabled DebugVisible; shapes, World and Pixels are defined in the complete class below.

C#
[View]
public void Draw(Draw2D draw)
{
    LineCount = 0;
    if (!draw.DebugVisible ||
        PhysicsSpace2D.Of(World!) is not { } space)
        return;
    shapes.Clear();
    space.AddDebugShapes(shapes);
    foreach (var line in shapes.Lines)
    {
        draw.Line(
            Pixels(line.From), Pixels(line.To),
            Color.Rgba(line.Rgba), 1.5f, 9);
        LineCount++;
    }
}

Collects the box outline and prints 4; the full program draws those lines through a view.

Text
4

Unzip the example, then dotnet run.

The full program

The full program renders headless frames with the overlay off and on. It draws via Draw2D only when DebugVisible is true, and converts physics coordinates into pixels. The zip includes Noto Sans under its OFL license so the host creates the 2D rendering stage; it needs no external input files.

The project references Core directly so its source generator supplies the entity, synced properties and command code. Add it alongside Physics:

Terminal
dotnet add package RecursiveAI.OneGame.Core

The view and headless captures also use the host package:

Terminal
dotnet add package RecursiveAI.OneGame.Host

The generated registry namespace uses the assembly name Example. Keep this project setting if you copy the full program into a differently named project:

XML
<PropertyGroup>
  <AssemblyName>Example</AssemblyName>
</PropertyGroup>
<ItemGroup>
  <PackageReference Include="RecursiveAI.OneGame.Core"
                    Version="2.0.0" />
</ItemGroup>
C#
using System.Numerics;
using OneGame.Core;
using OneGame.Host;
using OneGame.Physics;
using OneGame.UI;
 
using var game = GameInstance.Create(
    new OverlayGame(),
    new GameOptions
    { Headless = true, Gpu = GpuMode.Required });
var stage = (Render2DStage)game.Stage;
stage.Draw.DebugVisible = false;
game.Capture(320, 180);
var view = (ColliderView)game.World.Entities[0];
Console.WriteLine(
    $"Overlay off: {view.LineCount} lines");
stage.Draw.DebugVisible = true;
game.Capture(320, 180);
Console.WriteLine(
    $"Overlay on: {view.LineCount} lines");
 
[Entity]
public partial class ColliderView : Entity, IView2D
{
    private readonly PhysicsDebugShapes shapes =
        new();
    public int LineCount { get; private set; }
 
    [View]
    public void Draw(Draw2D draw)
    {
        LineCount = 0;
        if (!draw.DebugVisible ||
            PhysicsSpace2D.Of(World!) is not { } space)
            return;
        shapes.Clear();
        space.AddDebugShapes(shapes);
        foreach (var line in shapes.Lines)
        {
            draw.Line(
                Pixels(line.From), Pixels(line.To),
                Color.Rgba(line.Rgba), 1.5f, 9);
            LineCount++;
        }
    }
 
    [View]
    private static Vector2 Pixels(Vector2 p) =>
        new(160 + p.X * 32, 90 - p.Y * 32);
}
 
public sealed class OverlayGame : GameDefinition
{
    public override string Name => "Collider overlay";
    public override void RegisterEntities(
        EntityRegistry registry) =>
        OneGame.Generated.Example.EntityTypes.Register(
    registry);
    public override void RegisterSystems(
        World world) => PhysicsSpace2D.AddTo(world);
    public override void Setup(World world)
    {
        PhysicsSpace2D.Of(world)!.AddBox(
            new FVec2(2, 1), FVec2.Zero,
            new BodyOptions
            { Kind = BodyKind.Static });
        world.Add(new ColliderView());
    }
    public override void DefineScreens(
        ScreenSet screens) =>
        screens.Add("main", _ => Ui.Column());
}

dotnet run prints:

Text
Overlay off: 0 lines
Overlay on: 4 lines

The full program uses GpuMode.Required, which fails if a graphics device cannot be created.

The short answer

Collect a space’s debug geometry with AddDebugShapes, then draw its PhysicsDebugShapes.Lines from a view. Guard the work with Draw2D.DebugVisible. The small snippet above collects a box’s four outline lines; the full program connects that data to the actual 2D drawing stage and renders with visibility both off and on.

The physics guide describes the adapter as outlines of shapes plus contact points and normals, in world units. It draws geometry from the physics space, which is why it can reveal a mismatch between the collider and the visible artwork.

How do you draw 2D physics colliders for debugging?

Keep a PhysicsDebugShapes instance on the view side. Clear it before collecting the current frame, call space.AddDebugShapes(shapes), and send each returned segment to draw.Line. The full program’s Draw method is the working adapter. It converts both ends through the same pixel transform and uses the returned color for each line.

Physics uses Y-up coordinates. The example multiplies world coordinates by a pixel scale and negates Y, then offsets the result to the center of its capture. Use the same transform as your game artwork; otherwise the outlines can be correct physics geometry drawn in the wrong place. Thickness is a drawing choice too: this example uses a fixed pixel thickness rather than treating the returned world-unit width as pixels.

The adapter source defines separate colors for static, kinematic, awake, sleeping and trigger geometry. It adds contact normal lines and contact-point marks. Circles are represented by line segments; do not mistake the segmented debug outline for a change to the underlying collision shape.

Why is the physics debug overlay not showing?

First check DebugVisible. In the development host, F3 turns on the overlay; a scenario can use overlay: on. The guide says Release builds never set that flag. The console download sets it explicitly for two headless captures so its output is repeatable without keyboard input.

Next check whether this world has the space. Only the host world has physics; clients don’t, so clients cannot collect these outlines directly. The full view returns without drawing when PhysicsSpace2D.Of(World!) cannot find a space. To draw these outlines on a networked client, your game must send the geometry from the host.

Finally, check the rendering stage and its resources. Draw2D.Line must run during a rendered frame, not from OnTick or a random console callback. This example supplies a redistributable font and requests a GPU so the host uses its 2D stage. It calls Capture to render each frame; simply collecting outlines does not render a frame. The output distinguishes the zero lines submitted while hidden from the four box lines submitted while visible.

All onegame recipes