reckfellENGINE

Top-down 2D movement: input, collision and animation

A complete WASD actor with consistent diagonal speed and real wall collision.

On this page

Result

A WASD actor moves at 160 canvas units per second, slides against a wall and selects idle/walk from actual movement. Code generates four frames; no external images are required. This is top-down movement without jumping or gravity. A platformer needs a different movement mode.

Run in an empty level

  1. Create a C# project with Reckfell.NET.Sdk 1.0.0 and net10.0.
  2. Save the four files below in the project and run dotnet build.
  3. Create a level with an Entity2D root, attach TopDownExample.cs, save top_down.rscene and run that level.
  4. The actor starts at (96,96), the wall at (200,96). Hold D to move right and stop before the wall. Hold D+W to slide upward along it.

The script creates the actor, visuals, CollisionShape2D and wall. Do not add another set of shapes manually. Keep the root and physics bodies at scale (1,1); resize their Shape resources when different dimensions are needed.

ActorInput.cs
C#
using Reckfell;

public static class ActorInput
{
    public static void Ensure()
    {
        Add("actor_left", Key.A);
        Add("actor_right", Key.D);
        Add("actor_up", Key.W);
        Add("actor_down", Key.S);
    }

    private static void Add(string action, Key key)
    {
        if (InputMap.HasAction(action)) return;
        InputMap.AddAction(action);
        InputMap.ActionAddEvent(action, new InputEventKey { PhysicalKeycode = key });
    }
}
ActorFrames.cs
C#
using Reckfell;

public static class ActorFrames
{
    public static SpriteFrames Create()
    {
        using var image = Image.CreateEmpty(128, 32, false, Image.Format.Rgba8);
        image.Fill(Colors.Transparent);
        for (int i = 0; i < 4; i++)
        {
            int x = i * 32;
            int stride = i == 1 ? 3 : i == 3 ? -3 : 0;
            image.FillRect(new Rect2I(x + 9, 4, 14, 16), Colors.White);
            image.FillRect(new Rect2I(x + 19, 8, 3, 3), Colors.Black);
            image.FillRect(new Rect2I(x + 9 + stride, 20, 5, 8), new Color(0.25f, 0.45f, 1));
            image.FillRect(new Rect2I(x + 18 - stride, 20, 5, 8), new Color(0.25f, 0.45f, 1));
        }
        var atlas = ImageTexture.CreateFromImage(image);
        var frames = new SpriteFrames();
        frames.RemoveAnimation("default");
        frames.AddAnimation("idle");
        frames.SetAnimationLoopMode("idle", SpriteFrames.LoopMode.Linear);
        frames.SetAnimationSpeed("idle", 1);
        frames.AddAnimation("walk");
        frames.SetAnimationLoopMode("walk", SpriteFrames.LoopMode.Linear);
        frames.SetAnimationSpeed("walk", 8);
        for (int i = 0; i < 4; i++)
        {
            var region = new AtlasTexture { Atlas = atlas, Region = new Rect2(i * 32, 0, 32, 32) };
            frames.AddFrame("walk", region, i == 2 ? 2 : 1);
            if (i == 0) frames.AddFrame("idle", region);
        }
        return frames;
    }
}
TopDownActor.cs
C#
using Reckfell;

public partial class TopDownActor : CharacterBody2D
{
    [Export] public float Speed { get; set; } = 160;
    public AnimatedSprite2D Sprite { get; private set; } = null!;

    public override void _Ready()
    {
        ActorInput.Ensure();
        MotionMode = MotionModeEnum.Floating;
        AddChild(new CollisionShape2D { Shape = new RectangleShape2D { Size = new Vector2(18, 26) } });
        Sprite = new AnimatedSprite2D
        {
            SpriteFrames = ActorFrames.Create(),
            TextureFilter = TextureFilterEnum.Nearest
        };
        AddChild(Sprite);
        Sprite.Play("idle");
    }

    public override void _PhysicsProcess(double delta)
    {
        Vector2 direction = Input.GetVector("actor_left", "actor_right", "actor_up", "actor_down");
        float speed = float.IsFinite(Speed) && Speed > 0 ? Speed : 0;
        Velocity = direction * speed;
        MoveAndSlide();
        if (direction.X != 0) Sprite.FlipH = direction.X < 0;
        StringName animation = GetRealVelocity().LengthSquared() > 1 ? "walk" : "idle";
        if (Sprite.Animation != animation) Sprite.Play(animation);
    }
}
TopDownExample.cs
C#
using Reckfell;

public partial class TopDownExample : Entity2D
{
    public TopDownActor Actor { get; private set; } = null!;

    public override void _Ready()
    {
        Actor = new TopDownActor { Position = new Vector2(96, 96) };
        AddChild(Actor);
        var wall = new StaticBody2D { Position = new Vector2(200, 96) };
        AddChild(wall);
        wall.AddChild(new CollisionShape2D { Shape = new RectangleShape2D { Size = new Vector2(20, 160) } });
        wall.AddChild(new Polygon2D
        {
            Polygon = new[] { new Vector2(-10, -80), new Vector2(10, -80), new Vector2(10, 80), new Vector2(-10, 80) },
            Color = new Color(0.2f, 0.22f, 0.25f)
        });
        var hud = new CanvasLayer { Layer = 1 };
        AddChild(hud);
        hud.AddChild(new Label { Position = new Vector2(12, 190), Text = "W A S D" });
    }
}

Why movement works this way

Input.GetVector limits diagonal input to a length of one and applies a circular deadzone. Do not normalize its result again: doing so would discard analog input magnitude. ActorInput creates missing actions with physical keys in WASD positions and preserves existing bindings. These InputMap changes apply during execution. Save project Input Map bindings for persistent project configuration.

Velocity is speed, not displacement per frame. MoveAndSlide uses the physics timestep internally; do not multiply Velocity by delta before calling it. _PhysicsProcess recalculates desired speed each tick because collision may modify Velocity. Floating treats contacts as walls; IsOnFloor does not test for ground below this top-down actor.

Changing Position directly bypasses this movement path and does not replace MoveAndSlide for collision. A visible sprite is insufficient: both bodies need active CollisionShape2D/Shape resources and compatible collision layers/masks. The example uses their defaults in a clean project.

Animate the result

GetRealVelocity reports actual speed after movement. Speed above 1 selects walk; otherwise idle is selected. The actor therefore stops stepping when blocked by the wall. This threshold is a sample choice; adjust it for very slow actors.

FlipH changes only the sprite. The physics body and shape are not mirrored. The last horizontal facing direction survives stopping and vertical movement. This supplies two sides of one sprite, not complete four-direction or eight-direction animation.

Diagnose common failures

SymptomCheck
Faster diagonallyUse GetVector instead of adding unrestricted input axes
Barely movesRemove an extra delta multiplication from Velocity; use finite positive Speed
Passes through the wallCheck Shape, Disabled and collision layers/masks; do not move through Position
WASD does nothingFocus the game window and inspect existing actor_* bindings, which this sample preserves
Wrong visual sizeAtlasTexture.Region uses atlas pixels; do not scale the physics parent to resize artwork
Walking never stopsSelect state after MoveAndSlide using GetRealVelocity when animation should follow the outcome

Frame animation · Coordinates and camera

Reckfell Engine documentationBack to top ↑
QUICK LINKSGetting startedEntity hierarchyC# & .NET 10Scenes and prefabs
Reckfell guides and full API K
reckfellENGINE
RECKFELL ACCOUNT

Sign in

Sign in to your Reckfell account.

One account for the entire Reckfell platform.