reckfellENGINE

Движение 2D сверху: ввод, столкновения и анимация

Готовый персонаж с WASD, нормальной скоростью по диагонали и остановкой у стены.

На этой странице

Что получится

Персонаж перемещается по WASD со скоростью 160 единиц canvas в секунду, скользит вдоль стены и переключает idle/walk по фактическому движению. Четыре кадра создаются кодом: внешние изображения не нужны. Это вид сверху, без прыжка и гравитации; для платформера нужен другой режим движения.

Запустить в пустом уровне

  1. Создайте C#-проект с Reckfell.NET.Sdk 1.0.0 и net10.0.
  2. Сохраните четыре файла ниже в проект, выполните dotnet build.
  3. Создайте уровень с корнем Entity2D, прикрепите TopDownExample.cs, сохраните как top_down.rscene и запустите этот уровень.
  4. Персонаж появляется в (96,96), стена — в (200,96). Нажмите D: персонаж движется вправо и останавливается перед стеной. D+W позволяет двигаться вдоль неё вверх.

Скрипт сам создаёт персонажа, графику, CollisionShape2D и стену. Не добавляйте ещё один набор форм вручную. Используйте масштаб корня и физических тел (1,1); меняйте размер самих Shape, если нужны другие габариты.

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" });
    }
}

Почему движение устроено так

Input.GetVector ограничивает длину диагонального ввода единицей и обрабатывает круговую deadzone. Дополнительно нормализовать полученный вектор не нужно: это уничтожило бы величину аналогового ввода. ActorInput создаёт отсутствующие действия с физическими клавишами в позициях WASD и сохраняет уже существующие назначения. Изменения InputMap в этом примере действуют во время запуска; для постоянных настроек проекта сохраните назначения в Input Map.

Velocity — скорость, не смещение за кадр. MoveAndSlide сам использует шаг физики; не умножайте Velocity на delta перед вызовом. _PhysicsProcess пересчитывает желаемую скорость на каждом шаге, поскольку столкновение может изменить Velocity. Floating трактует контакты как стены; IsOnFloor здесь не проверяет наличие пола под персонажем.

Прямое изменение Position обходит этот путь движения и не заменяет MoveAndSlide для столкновений. Получить видимую картинку недостаточно: нужна активная CollisionShape2D с Shape и совместимые collision layer/mask у обоих тел. В чистом проекте пример использует их стандартные значения.

Связать анимацию с результатом

GetRealVelocity показывает фактическую скорость после перемещения. При величине больше 1 выбирается walk, иначе idle. Поэтому при упоре в стену персонаж не продолжает шагать на месте. Порог — решение примера; для очень медленного персонажа настройте его под свою игру.

FlipH меняет только спрайт. Физическое тело и форма не зеркалятся. Последнее горизонтальное направление сохраняется при остановке и вертикальном движении. Это две стороны одного спрайта, а не готовый набор анимаций для четырёх или восьми направлений.

Исправить типичные ошибки

СимптомПроверка
По диагонали быстрееНе складывайте ненормированные оси вместо GetVector
Почти не движетсяУберите лишний delta из Velocity; задайте конечный положительный Speed
Проходит сквозь стенуПроверьте Shape, Disabled и collision layer/mask; не перемещайте тело через Position
Нет реакции на WASDДайте окну игры фокус; проверьте существующие actor_* назначения, которые пример не перезаписывает
Слишком большая/маленькая картинкаAtlasTexture.Region задаётся в пикселях атласа; не масштабируйте физический родитель ради картинки
Ходьба не останавливаетсяВыбирайте состояние после MoveAndSlide по GetRealVelocity, если нужна связь с результатом

Покадровая анимация · Координаты и камера

Документация Reckfell EngineНаверх ↑
БЫСТРЫЙ ПЕРЕХОДНачало работыEntity и иерархияC# & .NET 10Сцены и префабы
Руководства и полный API Reckfell K
reckfellENGINE
RECKFELL ACCOUNT

Вход в аккаунт

Войдите в аккаунт Reckfell.

Один аккаунт для всей платформы Reckfell.