Контроллер 3D-персонажа
Полный SquashPlayer.cs: движение, гравитация, прыжок, проверка контактов и сброс состояния.
Прикрепите Scripts/SquashPlayer.cs к корню игрока. Перед общей сборкой добавьте также SquashMob.cs, SquashGame.cs, SquashHud.cs и LandingPulse.cs из этого раздела: классы ссылаются друг на друга.
Полный SquashPlayer.cs
using Reckfell;
public partial class SquashPlayer : CharacterBody3D
{
[Signal] public delegate void HitEventHandler();
[Export] public float Speed { get; set; } = 5;
[Export] public float Gravity { get; set; } = 24;
[Export] public float JumpSpeed { get; set; } = 8;
[Export] public float BounceSpeed { get; set; } = 6;
public bool Active { get; set; } = true;
private Area3D _detector = null!;
private LandingPulse _visual = null!;
public override void _Ready()
{
_detector = GetEntity<Area3D>("MobDetector");
_visual = GetEntity<LandingPulse>("Visual");
}
public override void _PhysicsProcess(double delta)
{
if (!Active)
{
Velocity = Vector3.Zero;
return;
}
Vector3 velocity = Velocity;
if (!IsOnFloor()) velocity.Y -= Gravity * (float)delta;
if (IsOnFloor() && Input.IsActionJustPressed("jump")) velocity.Y = JumpSpeed;
Vector2 input = Input.GetVector("move_left", "move_right", "move_forward", "move_back");
velocity.X = input.X * Speed;
velocity.Z = input.Y * Speed;
bool falling = velocity.Y < 0;
Velocity = velocity;
MoveAndSlide();
Position = new Vector3(Mathf.Clamp(Position.X, -6, 6), Position.Y, Mathf.Clamp(Position.Z, -6, 6));
if (falling && IsOnFloor()) _visual.Pulse();
for (int i = 0; i < GetSlideCollisionCount(); i++)
{
KinematicCollision3D collision = GetSlideCollision(i);
if (falling && collision.GetNormal().Y > 0.5f &&
collision.GetCollider() is SquashMob mob && mob.TrySquash())
{
velocity = Velocity;
velocity.Y = BounceSpeed;
Velocity = velocity;
return;
}
}
if (IsOnFloor())
{
foreach (Entity3D body in _detector.GetOverlappingBodies())
{
if (body is SquashMob mob && !mob.IsQueuedForDeletion())
{
TakeHit();
break;
}
}
}
}
public void StartAt(Vector3 position)
{
Position = position;
Velocity = Vector3.Zero;
ResetPhysicsInterpolation();
_visual.ResetVisual();
Active = true;
}
public void TakeHit()
{
if (!Active) return;
Active = false;
Velocity = Vector3.Zero;
EmitSignal(SignalName.Hit);
}
}Скорость и шаг физики
_PhysicsProcess() обновляет Velocity в мировых единицах в секунду. MoveAndSlide() учитывает физический шаг; повторно умножать скорость движения на delta нельзя. Гравитация изменяет вертикальную скорость за время, поэтому Gravity * delta нужна именно там.
Input.GetVector() ограничивает длину направления. X движения становится мировой X, Y ввода — мировой Z. W соответствует отрицательной Z; управление не поворачивается вслед за камерой. Без нажатий X/Z скорости обнуляются.
Позиция X/Z ограничена диапазоном −6…6, чтобы персонаж оставался на площадке 14 × 14. Это правило прототипа, а не невидимые физические стены.
Состояние
Active = false явно блокирует управление и обнуляет скорость. StartAt() сбрасывает позицию, скорость, интерполяцию и визуальную анимацию перед новым раундом. Вызывайте его у уже готового игрока: ссылки на MobDetector и Visual заполняются в _Ready().
После MoveAndSlide() доступны контакты текущего перемещения. Приземление сверху и боковой контакт разбираются отдельно; это части того же полного класса, а не дополнительные скрипты.
Результат
Соберите все пять классов, запустите Arena.rscene и нажмите кнопку начала. Персонаж движется со скоростью 5 единиц/с, диагональ не быстрее, Space запускает прыжок только с пола. Если его нет на экране, проверьте камеру и меш; если он падает — формы пола и тела, а также маску 5 у игрока.