2D character: walking, jumping and walls
A complete C# level: input, gravity, MoveAndSlide and teleporting without a stale-floor jump.
Run the character
Add the three files below to a C# project. Create a separate level with an Entity2D root, attach CharacterLevel2D, build and press F6. Use a 640×420 or larger window, 60 physics ticks/s and 2D gravity 980 in direction (0, 1). Keep the root and physics branch unscaled/unrotated.
A/D or arrows walk, Space jumps from the floor, and R teleports above. MotionBindings adds only missing Input Map actions and preserves existing project bindings. A blue rectangle represents the character; its actual physics shape is a capsule. It stands on the floor and slides along the right wall.
MotionBindings.cs
using Reckfell;
public static class MotionBindings
{
public static void Ensure()
{
Bind("move_left", Key.A, Key.Left); Bind("move_right", Key.D, Key.Right);
Bind("move_forward", Key.W, Key.Up); Bind("move_back", Key.S, Key.Down);
Bind("jump", Key.Space);
}
private static void Bind(string action, params Key[] keys)
{
if (InputMap.HasAction(action)) return;
InputMap.AddAction(action);
foreach (Key key in keys) InputMap.ActionAddEvent(action, new InputEventKey { PhysicalKeycode = key });
}
}CharacterMotor2D.cs
using System;
using Reckfell;
public partial class CharacterMotor2D : CharacterBody2D
{
[Export] public float Speed { get; set; } = 240;
[Export] public float JumpSpeed { get; set; } = 420;
public bool InputEnabled { get; set; } = true;
public int Teleports { get; private set; }
private Vector2? _teleport;
public override void _Ready()
{
MotionBindings.Ensure(); UpDirection = Vector2.Up; FloorSnapLength = 6;
}
public void RequestTeleport(Vector2 worldPosition)
{
if (!worldPosition.IsFinite()) throw new ArgumentOutOfRangeException(nameof(worldPosition));
_teleport = worldPosition;
}
public override void _PhysicsProcess(double delta)
{
bool teleported = false;
if (_teleport is Vector2 target)
{
GlobalPosition = target; Velocity = Vector2.Zero; _teleport = null;
ResetPhysicsInterpolation(); Teleports++; teleported = true;
}
Vector2 velocity = Velocity;
if (teleported || !IsOnFloor()) velocity += GetGravity() * (float)delta;
if (!teleported && InputEnabled && IsOnFloor() && Input.IsActionJustPressed("jump")) velocity.Y = -JumpSpeed;
velocity.X = InputEnabled ? Input.GetAxis("move_left", "move_right") * Speed : 0;
Velocity = velocity;
MoveAndSlide();
}
}CharacterLevel2D.cs
using Reckfell;
public partial class CharacterLevel2D : Entity2D
{
public CharacterMotor2D Player { get; private set; } = null!;
public override void _Ready()
{
AddBox(new Vector2(320, 320), new Vector2(640, 40));
AddBox(new Vector2(560, 180), new Vector2(40, 240));
Player = new CharacterMotor2D { Position = new Vector2(100, 280), CollisionLayer = 2, CollisionMask = 1 };
Player.AddChild(new CollisionShape2D { Shape = new CapsuleShape2D { Radius = 12, Height = 40 } });
Player.AddChild(new Polygon2D { Polygon = new[] { new Vector2(-12,-20), new Vector2(12,-20), new Vector2(12,20), new Vector2(-12,20) }, Color = new Color(.25f,.45f,1) });
AddChild(Player);
}
private void AddBox(Vector2 position, Vector2 size)
{
var body = new StaticBody2D { Position = position, CollisionLayer = 1, CollisionMask = 0 };
body.AddChild(new CollisionShape2D { Shape = new RectangleShape2D { Size = size } });
Vector2 half = size / 2;
body.AddChild(new Polygon2D { Polygon = new[] { -half, new Vector2(half.X,-half.Y), half, new Vector2(-half.X,half.Y) }, Color = new Color(.72f,.72f,.76f) });
AddChild(body);
}
public override void _UnhandledKeyInput(InputEvent @event)
{
if (@event is InputEventKey { Pressed: true, Echo: false, PhysicalKeycode: Key.R })
Player.RequestTeleport(ToGlobal(new Vector2(100,100)));
}
}Velocity and gravity
Velocity is measured in pixels/s. Do not multiply it by delta before MoveAndSlide; the method accounts for the step. Acceleration from GetGravity, however, is added to velocity with delta. A CharacterBody does not start falling merely because project gravity is configured; the script implements that.
IsOnFloor/IsOnWall describe the last MoveAndSlide result. Floor state can still be absent before the first movement. After horizontal input stops, the wall flag need not remain true: the query is no longer trying to move into the wall. Use a separate ray/shape query for persistent proximity checks.
This controller uses UpDirection = Vector2.Up and negative Y jump velocity. It assumes ordinary vertical gravity. Rotating gravity requires coordinating UpDirection, jump direction and the horizontal axis, rather than changing only a Gravity number.
Teleporting and limits
RequestTeleport stores a world point and applies it in _PhysicsProcess. After assigning GlobalPosition, the script clears Velocity and calls ResetPhysicsInterpolation. It deliberately does not use the old IsOnFloor to allow a jump that tick: the floor before teleporting no longer authorizes an action at the new position.
InputEnabled = false disables control while falling/collisions continue. Pausing is a separate scenario. The example does not add coyote time, jump buffering, stairs, ladders or network synchronization. For no movement, check action names/bindings, the CollisionShape2D direct parent, Layer/Mask and root type. For falling through the floor, begin with physics diagnostics.