---
title: "2D platformer character controller in C#"
url: https://recursiveai.org/onegame/cookbook/2d-character-controller/
description: "Move a capsule right through a 2D level. The full program adds gravity and a jump."
version: "2.0.0"
---

# 2D platformer 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.

[NuGet](https://www.nuget.org/packages/RecursiveAI.OneGame.Physics) · [GitHub](https://github.com/recursive-ai-oss/onegame) · [Docs](https://recursiveai.org/onegame/docs/) · [Download example (.zip)](https://recursiveai.org/onegame/cookbook/2d-character-controller/2d-character-controller.zip)

Move a capsule right through a 2D level. The full program adds gravity and a jump.

```shell
dotnet new console
dotnet add package RecursiveAI.OneGame.Physics
```

Requires [.NET 10](https://dotnet.microsoft.com/download/dotnet/10.0) (net10.0); .NET 9 and earlier aren’t supported.

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

```csharp
using OneGame.Core;
using OneGame.Physics;

var space = new PhysicsSpace2D();
var hero = space.AddCharacter(FVec2.Zero);
space.Move(hero, new FVec2(1, 0));
space.Step();
var position = space.Position(hero);
Console.WriteLine(position.X > 0);
```

Moves the capsule right and prints `True`.

```text
True
```

Unzip [the example](https://recursiveai.org/onegame/cookbook/2d-character-controller/2d-character-controller.zip), then `dotnet run`.

## The full program

An [entity](https://recursiveai.org/onegame/docs/first-game/) is a game object; its synced properties are saved and shared state, and its commands queue actions for a simulation tick.

The full program lands on a floor, queues a jump and lands again. `Hero` keeps its vertical speed, grounded flag and capsule reference in synced properties. Its generated command queues the jump for the next world tick.

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

```shell
dotnet add package RecursiveAI.OneGame.Core
```

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>
```

```csharp
using OneGame.Core;
using OneGame.Physics;

var registry = new EntityRegistry();
OneGame.Generated.Example.EntityTypes.Register(
    registry);
var world = new World(1, registry);
var space = PhysicsSpace2D.AddTo(world);
space.AddBox(new FVec2(40, 1), new FVec2(0, -1),
    new BodyOptions { Kind = BodyKind.Static });
var hero = world.Add(new Hero
    { Capsule = space.AddCharacter(
        new FVec2(0, 3)) });
for (var tick = 0; tick < 120; tick++)
    world.Step();
Console.WriteLine($"Grounded: {hero.Grounded}");
hero.Jump();
world.Step();
var jumping = hero.VerticalSpeed > Fixed.Zero;
Console.WriteLine($"Jumping: {jumping}");
for (var tick = 0; tick < 120; tick++)
    world.Step();
Console.WriteLine($"Landed again: {hero.Grounded}");

[Entity]
public partial class Hero : Entity
{
    [Synced]
    public partial CharacterRef Capsule { get; set; }
    [Synced]
    public partial Fixed VerticalSpeed { get; set; }
    [Synced]
    public partial bool Grounded { get; set; }
    [Synced]
    public partial FVec2 Position { get; set; }

    [Command]
    public void Jump()
    {
        if (Grounded) VerticalSpeed = 11;
    }

    protected override void OnTick()
    {
        var space = PhysicsSpace2D.Of(World!)!;
        var dt = space.StepSeconds;
        var vy = VerticalSpeed - 25 * dt;
        var move = space.Move(Capsule, new FVec2(
            2 * dt, vy * dt));
        if ((move.Grounded && vy < Fixed.Zero) ||
            (move.HitCeiling && vy > Fixed.Zero))
            vy = Fixed.Zero;
        VerticalSpeed = vy;
        Grounded = move.Grounded;
        Position = space.Position(Capsule);
    }
}
```

`dotnet run` prints:

```text
Grounded: True
Jumping: True
Landed again: True
```

Using a removed character reference throws `StaleBodyException`.

## Variations

### Respawn without sweeping through the level

```csharp
using OneGame.Core;
using OneGame.Physics;

var space = new PhysicsSpace2D();
var hero = space.AddCharacter(FVec2.Zero);
space.Teleport(hero, new FVec2(5, 3));
Console.WriteLine(space.Position(hero));
```

```text
(5, 3)
```

## Related recipes

- [Filter 2D physics collision layers in C#](https://recursiveai.org/onegame/cookbook/filter-physics-collision-layers/)
- [Apply 2D forces and impulses in C#](https://recursiveai.org/onegame/cookbook/apply-2d-forces-and-impulses/)
- [Create a 2D polygon collider in C#](https://recursiveai.org/onegame/cookbook/create-2d-polygon-colliders/)

## The short answer

Use `PhysicsSpace2D.AddCharacter` for the capsule and `Move` for each tick’s displacement. Read `Grounded` to decide whether a jump is allowed. Keep vertical speed as a synced `Fixed` value on the owning entity, not as a view-side animation value. The full program above includes that state and a generated jump command.

The controller is not a falling rigid body with a movement script attached. It is a kinematic capsule: the game asks it to move, and the physics step advances it. That distinction matters when choosing where to apply gravity and when to read its position. The [physics guide](https://recursiveai.org/onegame/docs/physics/) describes the capsule’s collide-and-slide behavior, step climbing, slope following, moving-ground support and dynamic-body pushing.

## How do you add gravity to a 2D character controller?

Subtract gravity times `StepSeconds` from vertical speed, then multiply that speed by `StepSeconds` to get the vertical displacement. The `OnTick` method above does both operations. `Move` expects a distance for this tick, not units per second. Passing a speed directly makes movement depend on how often the method is called.

Keep this calculation in simulation code. A view may run at a different cadence, and its floating-point drawing coordinates are not the source of the character’s physics position. In an attached space, commands run first, entities tick next, and the physics system steps afterward. Do not also call `space.Step()` from the entity: the registered system already owns that step.

Clear a negative vertical speed when the move reports walkable ground. Clear a positive vertical speed when it reports a ceiling. Otherwise a character can keep accumulating downward speed while standing still, or retain upward speed against a ceiling. The full program handles both cases; the small snippet at the top only demonstrates horizontal movement.

## How do you make a 2D character controller jump?

Treat jumping as a game rule. The example’s `Jump` command checks the synced grounded flag and sets an upward vertical speed. The following tick applies gravity and includes that speed in the displacement. Repeated calls while airborne do not create another jump because the handler checks `Grounded`.

The example uses a constant horizontal speed to keep the physics behavior visible without a keyboard or window. For [player steering](https://recursiveai.org/onegame/docs/input/), send a direction through a command and store it in a synced property, then use that value in the same displacement calculation. The public [physics example](https://github.com/recursive-ai-oss/onegame/blob/60d5573da204de68cafd5896dc8d41b16c72ff9d/examples/physics/Game.cs) shows that input pattern.

Read positions with the step order in mind. Inside `OnTick`, `space.Position(hero)` is the position after the last completed step. `CharacterMove.Position` is where the capsule stood when `Move` ran, not a promise about the upcoming step’s final position. Only the host world has physics; clients don’t, so copy the physics position into synced properties for client views. Use `Teleport` for a respawn, where sweeping from the previous location would be the wrong operation.
