Recursive AI

2D physics in a C# game

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

Drop a box under gravity with RecursiveAI.OneGame.Physics, stable since 1.24.0 and versioned with the engine.

Terminal
dotnet add package RecursiveAI.OneGame.Physics

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

C#
using OneGame.Core;
using OneGame.Physics;
 
var physics = new PhysicsSpace2D();
var body = physics.AddBox(
    new FVec2(1, 1), new FVec2(0, 4));
physics.Step();
Console.WriteLine(physics.Position(body).Y < 4);

Prints:

Text
True

Unzip the example, then dotnet run.

The full program

The snippet at the top advances a standalone physics space once and prints whether the box fell. The full program attaches a space to a game world, which steps it once per tick.

C#
using System.Globalization;
using OneGame.Core;
using OneGame.Physics;
 
CultureInfo.CurrentCulture = CultureInfo.InvariantCulture;
var world = new World(1, new EntityRegistry());
var physics = PhysicsSpace2D.AddTo(world);
physics.AddBox(new FVec2(20, 1),
    new FVec2(0, -Fixed.FromRatio(1, 2)),
    new BodyOptions { Kind = BodyKind.Static });
var crate = physics.AddBox(new FVec2(1, 1), new FVec2(0, 4));
for (int tick = 0; tick < 120; tick++) world.Step();
Console.WriteLine($"Landed: y={physics.Position(crate).Y.ToSingle():0.00}");
physics.ApplyImpulse(crate, new FVec2(4, 3));
for (int tick = 0; tick < 120; tick++) world.Step();
var position = physics.Position(crate);
Console.WriteLine($"After kick: x={position.X.ToSingle():0.00}, y={position.Y.ToSingle():0.00}");
Console.WriteLine($"Physics steps: {physics.StepCount}");

dotnet run prints:

Text
Landed: y=0.50
After kick: x=2.49, y=0.50
Physics steps: 240

Invalid physics options throw ArgumentOutOfRangeException; a physics save with another table format version is refused, and nothing is loaded half-way.

Variations

Keep a floor static

C#
using OneGame.Core;
using OneGame.Physics;
 
var physics = new PhysicsSpace2D();
var floor = physics.AddBox(new FVec2(20, 1),
    new FVec2(0, 0),
    new BodyOptions { Kind = BodyKind.Static });
physics.Step();
Console.WriteLine(physics.Position(floor).Y == 0);
Text
True

Record and replay · Pathfinding


The short answer

Add RecursiveAI.OneGame.Physics, create a PhysicsSpace2D, and add bodies with shapes. A standalone space advances with Step. A space added with PhysicsSpace2D.AddTo(world) advances automatically once per world tick, after the entities. Use one stepping model, not both at once.

The downloadable program uses the world-managed model. It drops a dynamic box onto a static floor, lets it settle, applies an impulse and lets it settle again. It prints the resulting coordinates without requiring a renderer or a window.

How do I use 2D physics in C#?

Use FVec2 for positions and sizes and Fixed for scalar inputs. Fixed is a fixed-point number; FVec2 contains two of them. Box sizes are full widths and heights, not half-extents. In the full program the floor is twenty units wide and one unit high, with its center half a unit below zero. Its top is therefore at zero.

The crate is one unit tall. Once it rests on that floor, its center is approximately half a unit above zero, which is why the program prints y=0.50. The default gravity points along negative Y. A screen coordinate system that points downward needs a conversion in view code; changing the display convention is different from changing gravity.

A world-managed space belongs in the game’s system registration, so a fresh world created to load a save also has the space. Bodies live in the space and entities can keep a synced BodyRef. Mirror the positions and angles needed by views into synced entity properties. The physics table is hidden authority state, not a table replicated directly to clients.

What is the difference between static and dynamic bodies?

A dynamic body responds to gravity and impulses. That is the default for a box when no body options override it. A static body is appropriate for this example’s floor: it provides collision geometry without falling along with the crate. The variation shows that its position remains unchanged after a step.

An impulse is a one-time push. Apply it when the action occurs, as the full program does after the first settling interval. Applying the same impulse every tick would be a repeated series of pushes rather than the single kick illustrated here.

The physics system steps after entity updates. If an entity copies a body’s position during its update, that copy describes the previous physics step. Keep this order in mind when comparing entity properties with the space’s current coordinates or drawing a debug overlay.

Which 2D physics features does the C# package support?

The package gives a game “2D and 3D rigid bodies, characters, vehicles, triggers and queries that behave the same on every node.” In 2D that means boxes, circles, capsules and polygons, characters with collide-and-slide movement, triggers, ray casts and collision layers. The 2D wrapper does not cover joints, chains and segments, shape casts and overlap queries yet. 3D spaces (PhysicsSpace3D) are a separate API. Check the feature you need before choosing this package for an existing game.

Physics is included in world saves, replay and state hashing. It is built on OnePhysics 1.0, whose 1.x promise it passes on: “Within 1.x, patch releases never change a result”, while “a minor release may change results”, and its changelog says so when one does. Physics saves and replays made with the preview packages before 1.24.0 are not promised to carry over; re-record them. An incompatible physics save is refused rather than partly loaded.

Do not mark a physics-reading command as predicted. A replica’s predicted world has no physics space. Treat physics as authority logic and send the view the mirrored information it needs. The physics guide explains the snapshot boundary and available operations in detail.

Source: the pinned library documentation.

All onegame recipes