Recursive AI

Move a 3D character controller in C#

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

A character controller moves an upright capsule through collision geometry. You supply displacement; it does not add gravity or jumping for you.

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 space = new PhysicsSpace3D();
space.AddBox(new FVec3(40, 1, 40),
    new FVec3(0, -1, 0),
    new BodyOptions3D { Kind = BodyKind.Static });
var hero = space.AddCharacter(new FVec3(0, 3, 0));
CharacterMove3D move = default;
for (var tick = 0; tick < 120; tick++)
{
    move = space.Move(hero,
        new FVec3(space.StepSeconds,
            Fixed.FromRatio(-1, 10), 0));
    space.Step();
}
Console.WriteLine($"Position: {space.Position(hero)}");
Console.WriteLine($"Grounded: {move.Grounded}");

A removed character, vehicle or body reference causes StaleBodyException when used again.

Unzip the example, then dotnet run.

Text
Position: (1.999999881, 0.399999991, 0)
Grounded: True

Fixed and FVec3 are deterministic fixed-point scalar and 3D vector types: use Fixed.FromInt(2) or new FVec3(2, 0, 0) for integers, and convert a float with Fixed.FromDouble(value) outside sim code.

Variations

Move along Z instead of X. Replace the displacement in Move with:

C#
new FVec3(0, Fixed.FromRatio(-1, 10),
    space.StepSeconds)

With that change, the program prints:

Text
Position: (0, 0.399999991, 1.999999881)
Grounded: True

Add 3D rigid body physics in C# · Drive a 3D vehicle physics controller in C# · Raycast a 3D physics world in C#


The short answer

Use PhysicsSpace3D.AddCharacter to create an upright capsule, then call Move with a displacement for one fixed tick. The result reports grounded, ceiling and side contact. Unlike a falling rigid body, the character does not acquire a vertical displacement just because the space has gravity configured.

The console example moves along X while applying a downward displacement each tick. The capsule starts at Y = 3, reaches the static floor and reports Grounded: True; the downward motion comes from the supplied displacement, not automatic gravity.

How do I make a 3D character controller in C#?

Create a physics space, add collision geometry, and retain the CharacterRef returned by AddCharacter. The position you pass is the capsule’s center, not the bottom of its feet. CharacterOptions3D exposes radius, height, step offset, slope limit, push strength and layer. The slope limit is in radians.

Choose movement in simulation units. For speed measured in units per second, multiply by space.StepSeconds before passing the result to Move. Passing speed directly as displacement makes movement depend on tick count rather than elapsed simulation time. The example uses one unit per second along X and a fixed downward displacement each tick.

For a game-world integration, register the space with PhysicsSpace3D.AddTo(world) and issue movement from entity sim code before the world’s physics system steps. The download uses a standalone space to keep the example independent of input bindings, a game window and a source-generated entity registry.

Should I use a character controller or a rigid body?

Use a character when the game owns its intended movement and needs grounded, side and ceiling results. Use a dynamic rigid body when gravity, impulses and contact response should control its movement.

The 3D physics guide explicitly describes characters as displacement-driven. Keep vertical speed as synced entity state when implementing gravity and jumping. Update that speed according to your game’s rules and put the resulting vertical displacement into Move along with horizontal movement. Resetting or retaining vertical speed after a contact is also a game rule; the wrapper does not choose your jump behavior.

Read the movement result rather than inferring grounded state from a fixed height. A capsule can stand on geometry at different elevations, and side contact does not mean it is grounded. Grounded means contact with walkable ground; HitCeiling and HitSide identify other blocked directions.

The character reference and physics state are saved with the simulation. For a rendered or replicated character, mirror position and grounded state into synced entity properties. Views should consume those values rather than moving the capsule. This keeps the controller’s simulation independent of how often the screen redraws.

All onegame recipes