Unity: реализация динамичного передвижения в шутере

Unity: реализация динамичного передвижения в шутере
Скриншот «CREATURES»

Введение

Вот уже несколько месяцев я работаю над шутером от первого лица на Unity, вдохновлённым динамикой ULTRAKILL. Проект изначально задумывался как вызов себе и возможность практики, ведь шутеры — жанр, далёкий от моих привычек. За это время я освоил множество новых методов, которыми хочу поделиться.

В процессе разработки я столкнулся с интересными задачами: процедурная анимация смены оружия, выстрелы через Raycast и Projectile, создание эффектов дульного пламени и попаданий. В этой статье я расскажу о том, как организовал систему передвижения: надеюсь, моё решение окажется полезным в ваших проектах.

Референсы

Будучи скорее разработчиком, чем геймдизайнером, я опирался на проверенные примеры. В качестве эталона я взял движение из ULTRAKILL, убрав эффект скольжения. Оставил всё остальное: стремительный шаг без ускорения бега, высокий плавный прыжок, отскоки от стен, резкий спуск вниз и рывок.

Приятно было получить от пользователей комментарии в духе «мувсет ультраговнища» — значит, ощущение оригинала удалось воссоздать!

Итоговый список требований:

  • Перемещение по WASD с высокой скоростью без привычного спринта; на воздухе управление сохраняется, но с пониженным влиянием ввода.
  • Прыжок с переносом инерции.
  • Отскоки от стен с ограниченным количеством: после приземления счётчик сбрасывается. Отскок выполняется вверх и в сторону от поверхности.
  • Резкий спуск для ускоренного приземления в любой момент.
  • Рывок — мгновенное ускорение в направлении движения.

Существующие подходы

1. Character Controller

Компонент Character Controller позволяет управлять персонажем без Rigidbody. Методы Move и SimpleMove обрабатывают столкновения, но гравитацию и взаимодействие с физическими объектами придётся реализовывать вручную.

2. Изменение Transform.position

Непосредственное обновление Transform.position — фактически телепортация, что при высокой скорости приводит к «проскальзыванию» сквозь коллайдеры и нестабильному поведению.

3. AddForce

Добавление силы через AddForce даёт физически корректное движение, но снижает контроль и требует тщательной настройки физики и ForceMode.

4. Rigidbody.linearVelocity

Rigidbody.linearVelocity напрямую устанавливает скорость объекта, обеспечивая точный контроль и плавность. Я выбрал именно этот метод.

Реализация

Создаём объект с Collider и Rigidbody, добавляем скрипт PlayerMovement и вложенный объект CameraHolder с камерой. Тело поворачивается вокруг оси Y, а камера — вокруг X для наклона взгляда.

Повороты

private void CalculateView()
{
_newCameraRotation.x += _inputView.y * _playerSettings.verticalSensitivity *
Time.deltaTime * (_playerSettings.verticalInverted ? 1f : -1f);
_newCameraRotation.x = Mathf.Clamp(
    _newCameraRotation.x,
    _playerConfigs.cameraVerticalRotateMin,
    _playerConfigs.cameraVerticalRotateMax
);

_newCharacterRotation.y += _inputView.x * _playerSettings.horizontalSensitivity *
                           Time.deltaTime * (_playerSettings.horizontalInverted ? -1f : 1f);

_cameraHolder.localRotation = Quaternion.Euler(_newCameraRotation);
transform.localRotation = Quaternion.Euler(_newCharacterRotation);

}

Здесь _newCameraRotation — накопленный ввод мыши, коэффициенты чувствительности и ограничения углов берутся из ScriptableObject playerSettings и playerConfigs.

Движение

private void CalculateMoveVelocity()
{
if (_isDashNow) return;
Vector3 direction = GetMoveDirection();
float accel = _isGrounded
    ? _playerConfigs.acceleration
    : _playerConfigs.airAcceleration;

if (!_isGrounded)
{
    Vector3 horizVel = new Vector3(_rb.linearVelocity.x, 0, _rb.linearVelocity.z);
    float maxAirSpeed = _playerConfigs.walkSpeed;
    horizVel = horizVel.magnitude > maxAirSpeed
        ? horizVel.normalized * maxAirSpeed
        : horizVel;

    Vector3 airControl = direction * (_playerConfigs.walkSpeedAirModif * accel * Time.fixedDeltaTime);
    Vector3 newVel = horizVel + airControl;
    newVel = newVel.magnitude > maxAirSpeed
        ? newVel.normalized * maxAirSpeed
        : newVel;
    newVel.y = _rb.linearVelocity.y;
    _rb.linearVelocity = newVel;
}
else
{
    Vector3 targetVel = direction * _playerConfigs.walkSpeed;
    Vector3 newVel = Vector3.MoveTowards(
        new Vector3(_rb.linearVelocity.x, 0, _rb.linearVelocity.z),
        targetVel,
        accel * Time.fixedDeltaTime
    );
    newVel.y = _rb.linearVelocity.y;
    _rb.linearVelocity = newVel;
}

}

Метод GetMoveDirection():

private Vector3 GetMoveDirection()
{
    Vector3 dir = _cameraHolder.forward * _inputMovement.y + _cameraHolder.right * _inputMovement.x;
    dir.y = 0;
    return dir.normalized;
}

Прыжки

private void PerformJump()
{
bool canJump = Time.time - _lastGroundedTime <= _coyoteTime &&
Time.time - _lastJumpTime >= _jumpCoyoteTime;
if (canJump) Jump();
else if (CheckWallsAround(out Vector3 wallNormal) &&
         _wallJumpCount < _playerConfigs.maxWallJumpsWithoutGrounded)
{
    JumpWall(wallNormal);
}

}

Проверка стен:

private bool CheckWallsAround(out Vector3 wallNormal)
{
    Collider[] hits = Physics.OverlapSphere(transform.position, 0.6f, _wallLayer);
    foreach (var hit in hits)
    {
        if (Mathf.Abs(hit.transform.position.y - transform.position.y) < 0.5f) continue;
        wallNormal = (transform.position - hit.ClosestPoint(transform.position)).normalized;
        return true;
    }
    wallNormal = Vector3.zero;
    return false;
}

Методы прыжков:

private void Jump()
{
    Vector3 horizVel = new Vector3(_rb.linearVelocity.x, 0, _rb.linearVelocity.z);
    _rb.linearVelocity = horizVel + Vector3.up * _playerConfigs.jumpForce;
    _lastJumpTime = Time.time;
}

private void JumpWall(Vector3 jumpDir) { jumpDir.y = _playerConfigs.wallJumpY; Vector3 horizVel = new Vector3(_rb.linearVelocity.x, 0, _rb.linearVelocity.z); _rb.linearVelocity = horizVel + jumpDir * _playerConfigs.wallJumpForce; _wallJumpCount++; }

Рывок

private void Dash()
{
    Vector3 dir = GetMoveDirection();
    if (dir.magnitude < 0.1f)
    {
        dir = _cameraHolder.forward;
        dir.y = 0;
    }
    _dashStartTime = Time.time;
    _rb.linearVelocity = Vector3.zero;
    _rb.AddForce(dir * _playerConfigs.dashForce, ForceMode.Impulse);
}

Падение

private void Fall()
{
    _isDashNow = false;
    _rb.linearVelocity = Vector3.zero;
    _rb.AddForce(Physics.gravity * _playerConfigs.fallForce, ForceMode.Impulse);
}

Заключение

Эти методы можно адаптировать под любые проекты: подключайте New Input System или классический ввод, добавляйте проверку условий и расширяйте логику. Для полного кода контроллера оставляйте комментарии — возможно, я выпущу продолжение с детальным обзором системы ввода.

Следите за обновлениями в Telegram: https://t.me/UnityGameLab.

 

Источник

Читайте также