Персонаж 2D: ходьба, прыжок и стены
Законченный уровень C#: ввод, гравитация, MoveAndSlide и телепорт без ложного прыжка.
Запустить персонажа
Добавьте три файла ниже в C#-проект. Создайте отдельный уровень с корнем Entity2D, прикрепите CharacterLevel2D, соберите проект и нажмите F6. Окно — 640×420 или больше; физика — 60 шагов/с, гравитация 2D — 980 с направлением (0, 1). Корень и физическую ветвь оставьте без масштаба/поворота.
A/D или стрелки — ходьба, Space — прыжок с пола, R — перенос наверх. MotionBindings добавляет только отсутствующие действия Input Map; существующие привязки проекта сохраняются. Синий прямоугольник обозначает персонажа, физическая форма — капсула. Она стоит на полу и скользит вдоль правой стены.
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 — скорость в пикселях/с. Перед MoveAndSlide не умножайте её на delta: метод учитывает шаг сам. Ускорение GetGravity, напротив, добавляется к скорости с delta. CharacterBody не начинает падать автоматически только потому, что в проекте включена гравитация; это делает скрипт.
IsOnFloor/IsOnWall относятся к результату последнего MoveAndSlide. До первого движения пола может ещё не быть в состоянии тела. После остановки горизонтального движения флаг стены не обязан оставаться true: запрос больше не пытается двигаться в стену. Для постоянной проверки соседства нужен отдельный ray/shape-query.
В этой схеме UpDirection = Vector2.Up, прыжок имеет отрицательную Y-скорость. Она рассчитана на обычную вертикальную гравитацию. Для вращающейся гравитации нужно согласовать UpDirection, направление прыжка и горизонтальную ось, а не заменить только число Gravity.
Перенос и ограничения
RequestTeleport сохраняет мировую точку и применяется в _PhysicsProcess. После установки GlobalPosition скрипт очищает Velocity и вызывает ResetPhysicsInterpolation. На этом шаге он не использует прежний IsOnFloor для прыжка: пол до переноса уже не является основанием для действия в новой точке.
InputEnabled = false отключает управление, но тело продолжает падать и сталкиваться. Для паузы это другой сценарий. Пример не добавляет coyote time, буфер прыжка, ступени, лестницы или сетевую синхронизацию. Если персонаж не движется, проверьте имена/привязки действий, прямого родителя CollisionShape2D, Layer/Mask и тип корня. Если падает сквозь пол — начните с диагностики физики.