CharacterBody: скольжение, столкновение и отражение
Сравнение MoveAndSlide и MoveAndCollide, полный персонаж 3D и ограниченный цикл отражений 2D.
Выберите метод движения
| Метод | Передать | Получить |
|---|---|---|
| MoveAndSlide | Заранее задать Velocity в единицах/с | bool наличия столкновения; состояние пола/стены/потолка и список slide-контактов |
| MoveAndCollide | Перемещение за этот вызов, обычно velocity × delta | KinematicCollision либо null; путь, остаток и нормаль контакта |
MoveAndSlide умеет скользить и корректирует Velocity. Он не добавляет гравитацию или правила управления сам. MoveAndCollide останавливает движение при столкновении; дальнейшее скольжение/отражение задаёт ваш код. testOnly проверяет движение без перемещения тела. Не применяйте оба метода для одной и той же полной скорости за один шаг: получится лишнее движение.
После MoveAndSlide читайте GetSlideCollisionCount/GetSlideCollision. Эти контакты относятся к последнему вызову, а не ко всем соседям. Для MoveAndCollide используйте возвращённый объект. Различайте GetTravel и GetRemainder; остаток нужен для продолжения движения после контакта, а не повторного применения исходного отрезка.
Пол, склон и платформа
Grounded различает пол, стены и потолок относительно UpDirection и FloorMaxAngle. Угол API задаётся в радианах, например Mathf.DegToRad(45). Floating подходит движению без понятия пола. FloorSnapLength помогает удерживаться на поверхности в подходящем направлении; snap не является разрешением прыгать и не должен отменять движение вверх. FloorStopOnSlope, FloorConstantSpeed, SafeMargin и MaxSlides решают разные задачи. Не исправляйте неверный масштаб/форму случайным увеличением margin.
Платформы требуют согласованного физического движения, фильтров platform layers и политики ухода. GetPlatformVelocity и фактическая скорость движения отличаются от запрошенной Velocity. Этот пример проверяет плоский пол и стену; он не сертифицирует все склоны, вращающиеся платформы или автоматический подъём по ступеням.
Законченный персонаж 3D
Добавьте три файла ниже. Прикрепите CharacterLevel3D к корню Entity3D отдельного уровня, соберите и запустите F6. Настройки: 60 шагов/с, гравитация 9.8 вниз; единичный масштаб. WASD/стрелки двигают по мировым X/Z, Space прыгает, R переносит наверх. Направление пока не зависит от камеры. GetVector ограничивает диагональ, чтобы она не давала лишнюю скорость. Пример 2D с теми же правилами: ходьба и прыжок.
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 });
}
}CharacterMotor3D.cs
using System;
using Reckfell;
public partial class CharacterMotor3D : CharacterBody3D
{
[Export] public float Speed { get; set; } = 5;
[Export] public float JumpSpeed { get; set; } = 5;
public bool InputEnabled { get; set; } = true;
public int Teleports { get; private set; }
private Vector3? _teleport;
public override void _Ready()
{
MotionBindings.Ensure(); UpDirection = Vector3.Up; FloorSnapLength = .2f;
}
public void RequestTeleport(Vector3 worldPosition)
{
if (!worldPosition.IsFinite()) throw new ArgumentOutOfRangeException(nameof(worldPosition));
_teleport = worldPosition;
}
public override void _PhysicsProcess(double delta)
{
bool teleported = false;
if (_teleport is Vector3 target)
{
GlobalPosition = target; Velocity = Vector3.Zero; _teleport = null;
ResetPhysicsInterpolation(); Teleports++; teleported = true;
}
Vector3 velocity = Velocity;
if (teleported || !IsOnFloor()) velocity += GetGravity() * (float)delta;
if (!teleported && InputEnabled && IsOnFloor() && Input.IsActionJustPressed("jump")) velocity.Y = JumpSpeed;
Vector2 input = InputEnabled ? Input.GetVector("move_left", "move_right", "move_forward", "move_back") : Vector2.Zero;
velocity.X = input.X * Speed; velocity.Z = input.Y * Speed;
Velocity = velocity;
MoveAndSlide();
}
}CharacterLevel3D.cs
using Reckfell;
public partial class CharacterLevel3D : Entity3D
{
public CharacterMotor3D Player { get; private set; } = null!;
public override void _Ready()
{
AddBox(new Vector3(0,-.25f,0),new Vector3(16,.5f,12));
AddBox(new Vector3(4,2,0),new Vector3(.5f,4,8));
Player = new CharacterMotor3D { Position = new Vector3(0,.9f,0), CollisionLayer = 2, CollisionMask = 1 };
Player.AddChild(new CollisionShape3D { Shape = new CapsuleShape3D { Radius = .35f, Height = 1.8f } });
Player.AddChild(new MeshInstance3D { Mesh = new CapsuleMesh { Radius = .35f, Height = 1.8f }, MaterialOverride = Material(new Color(.25f,.45f,1)) });
AddChild(Player);
var camera = new Camera3D { Position = new Vector3(7,6,9), Current = true }; AddChild(camera); camera.LookAt(ToGlobal(new Vector3(0,1,0)));
}
private static StandardMaterial3D Material(Color color) => new() { AlbedoColor = color, ShadingMode = BaseMaterial3D.ShadingModeEnum.Unshaded };
private void AddBox(Vector3 position,Vector3 size)
{
var body = new StaticBody3D { Position = position, CollisionLayer = 1, CollisionMask = 0 };
body.AddChild(new CollisionShape3D { Shape = new BoxShape3D { Size = size } });
body.AddChild(new MeshInstance3D { Mesh = new BoxMesh { Size = size }, MaterialOverride = Material(new Color(.55f,.55f,.6f)) }); AddChild(body);
}
public override void _UnhandledKeyInput(InputEvent @event)
{
if (@event is InputEventKey { Pressed: true, Echo: false, PhysicalKeycode: Key.R })
Player.RequestTeleport(ToGlobal(new Vector3(0,3,0)));
}
}Отражение через MoveAndCollide
Для отдельного движущегося предмета создайте CharacterBody2D с дочерним CollisionShape2D/CircleShape2D и прикрепите BounceMover2D. Стены — StaticBody2D с включёнными формами; у предмета Mask должен видеть их Layer. Класс не использует гравитацию и ввод. Его можно проверить в отдельном уровне либо добавить рядом с персонажем. Для воспроизведения задайте CircleShape2D.Radius = 12. Добавьте видимый Sprite2D/Polygon2D или включите Visible Collision Shapes в отладочном меню редактора: сама физическая форма не рисуется в игре.
BounceMover2D.cs
using Reckfell;
public partial class BounceMover2D : CharacterBody2D
{
[Export] public Vector2 InitialVelocity { get; set; } = new(220,150);
public int Bounces { get; private set; }
public override void _Ready() { MotionMode = MotionModeEnum.Floating; Velocity = InitialVelocity; }
public override void _PhysicsProcess(double delta)
{
Vector2 remaining = Velocity * (float)delta;
for (int i = 0; i < 4 && remaining.LengthSquared() > .000001f; i++)
{
KinematicCollision2D? hit = MoveAndCollide(remaining);
if (hit is null) break;
Vector2 normal = hit.GetNormal();
if (normal.IsZeroApprox()) break;
Velocity = Velocity.Bounce(normal);
remaining = hit.GetRemainder().Bounce(normal);
Bounces++;
}
}
}Код отражает и скорость, и оставшийся путь. Цикл ограничен четырьмя столкновениями на шаг; непотраченный остаток после лимита отбрасывается. Это явный компромисс для примера, а не универсальный высокоскоростной solver. Нулевая нормаль прекращает цикл. Для снаряда с особыми правилами попадания используйте отдельную логику запросов и проверяйте нужные скорости/формы.
Если на месте ожидаемого скольжения происходит остановка — проверьте выбранный метод и реакцию на остаток. Если движение слишком медленное — нет ли delta дважды. Если слишком быстрое — не передана ли скорость напрямую в MoveAndCollide. Статус пола после телепорта должен обновиться новым движением; прежнее значение нельзя использовать для лишнего прыжка.