
Введение
Вот уже несколько месяцев я работаю над шутером от первого лица на 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.



