Recursive AI

Create a 2D polygon collider in C#

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

Create a convex 2D collider with at most 8 hull vertices; concave outlines become convex hulls.

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();
FVec2[] points = [new(-1, -1),
    new(1, -1), new(0, 1)];
var body = space.AddPolygon(
    points, FVec2.Zero);
Console.WriteLine(space.Exists(body));

Creates a triangular dynamic body and prints True.

Text
True

Unzip the example, then dotnet run.

The full program

The full program drops a triangle onto a static floor, watches for a contact and checks that it stops falling. All geometry is defined in the program; no image or mesh input is needed.

C#
using OneGame.Core;
using OneGame.Physics;
 
var space = new PhysicsSpace2D();
space.AddBox(new FVec2(20, 1), new FVec2(0, -1),
    new BodyOptions { Kind = BodyKind.Static });
FVec2[] vertices =
    [new(-1, -1), new(1, -1), new(0, 1)];
var triangle = space.AddPolygon(
    vertices, new FVec2(0, 4),
    new BodyOptions { FixedRotation = true });
var touchedFloor = false;
for (var tick = 0; tick < 180; tick++)
{
    space.Step();
    foreach (var contact in space.Events)
        if (contact.Kind ==
            PhysicsEventKind.ContactBegin)
            touchedFloor = true;
}
Console.WriteLine($"Touched floor: {touchedFloor}");
var stopped = space.Velocity(triangle).Y ==
    Fixed.Zero;
Console.WriteLine($"Stopped falling: {stopped}");

dotnet run prints:

Text
Touched floor: True
Stopped falling: True

Invalid polygon geometry is rejected; if polygon creation fails after allocating a body, the physics library destroys that body before rethrowing.

Variations

Make the polygon static level geometry

C#
using OneGame.Core;
using OneGame.Physics;
 
var space = new PhysicsSpace2D();
FVec2[] points = [new(-2, 0), new(2, 0), new(0, 1)];
var ramp = space.AddPolygon(points, FVec2.Zero,
    new BodyOptions { Kind = BodyKind.Static });
space.Step();
Console.WriteLine(space.Position(ramp));
Text
(0, 0)

The short answer

Pass local FVec2 points and a world position to PhysicsSpace2D.AddPolygon. The method returns a BodyRef, like AddBox and AddCircle, so the resulting collider participates in the same position, velocity, layer and event APIs. Use BodyOptions to choose a static body for level geometry or a dynamic body for a falling object.

The important geometry rule is that this API creates a convex hull. It does not trace sprite transparency, preserve inward notches or create a body with holes. The published OnePhysics package, used by OneGame 2.0.0, documents the underlying Shape2D.CreatePolygon behavior. The OneGame polygon method converts the local points and delegates polygon creation to that solver.

How do you tell if a polygon is convex or concave?

A polygon is convex if every line segment joining two points inside it stays inside; an inward notch makes it concave.

For choosing this collider API, the practical question is whether your intended solid has an inward notch. A convex hull covers that notch. If a player must be able to enter a recess in the outline, supplying the recess’s vertices to one AddPolygon call will not preserve that empty space.

Start with a small convex shape whose local coordinates you can inspect. The triangle above is centered around the body frame, then placed above the floor by AddPolygon’s second argument, at: the body origin’s world position, not another vertex. Moving the body position and adding the same offset to every local vertex are different operations; doing both offsets the shape twice. Keep the world placement separate from the point list.

The underlying API accepts between 3 and 65,536 input points but limits the resulting convex hull to 8 vertices. Duplicate and nearly duplicate points, and points close to hull edges within its documented tolerance, are dropped. A large input list is therefore not a promise of a many-sided output collider. Collinear or too-small point sets are invalid; merely passing three points does not ensure they enclose a usable solid.

How do you create a 2D polygon collider in code?

Build the point list in simulation units, pass it to AddPolygon, then step the space. The full program uses fixed rotation to keep the falling triangle upright and checks contact events instead of guessing that a particular number of ticks guarantees a landing. The floor is a static box, not a second falling object.

Read the point coordinates as local geometry. Body options supply angle, density, friction, restitution, layer and other body settings. The physics library converts each point through its checked physics conversion, creates the body and requests the polygon shape. If shape creation throws, it destroys the newly allocated body before rethrowing, so that failure does not leave that body in the space.

For level pieces that do not move, use BodyKind.Static as in the variation. For a moving polygon, retain its returned reference and read position and angle from the physics space after stepping. Only the host world has physics; clients don’t, so copy those values into synced properties for client views. The physics library does not expose chains, segments or 2D shape casts; those are separate geometry operations, not alternative names for this polygon method.

All onegame recipes