Unity: Разработка системы перемещения для динамичного шутера. Часть 2
Реализация продвинутого мувсета в Unity: архитектура вызовов и обработка ввода
Основываясь на интересе к предыдущему материалу, в этой части мы углубимся в архитектуру системы перемещения. Мы разберем схему вызова методов, интеграцию современной системы ввода (Input System) и событийную модель, которая позволяет гибко настраивать визуальные и звуковые эффекты: от встряски камеры при приземлении до озвучки прыжков.
Для тех, кто пропустил первую часть, рекомендую ознакомиться с ней для понимания базовой логики. В данной статье мы сосредоточимся на том, как превратить набор разрозненных методов в стройную систему.
Архитектура методов перемещения
Ниже представлены ключевые функции, отвечающие за физику и логику движений. Мы разделим их на блоки ответственности.
Управление обзором и направлением
Метод CalculateView() отвечает за вращение камеры и корпуса персонажа, учитывая настройки чувствительности и инверсии.
private void CalculateView() { _newCameraRotation.x += _inputView.y * _playerSettings.verticalSensetivity * Time.deltaTime * (_playerSettings.verticalInverted ? 1f : -1f);_newCameraRotation.x = Mathf.Clamp(_newCameraRotation.x, _playerConfigs.cameraVerticalRotateMin, _playerConfigs.cameraVerticalRotateMax); _newCaracterRotation.y += _inputView.x * _playerSettings.horisontalSensetivity * Time.deltaTime * (_playerSettings.horisontalInverted ? -1f : 1f); _cameraHolder.localRotation = Quaternion.Euler(_newCameraRotation); transform.localRotation = Quaternion.Euler(_newCaracterRotation);}
Для определения вектора движения используется вспомогательный метод
GetMoveDirrection(), который ориентируется на положение камеры, но игнорирует вертикальную составляющую, чтобы игрок не «взлетал» при взгляде вверх.private Vector3 GetMoveDirrection() { Vector3 moveDirection = _cameraHolder.forward * _inputMovement.y + _cameraHolder.right * _inputMovement.x; moveDirection.y = 0; moveDirection.Normalize(); return moveDirection; }Логика движения и ускорения
Основной расчет скорости
CalculateMoveVelocity()разделяет поведение персонажа на земле и в воздухе, обеспечивая инерцию и контроль в полете.private void CalculateMoveVelocity() { if (_isDashNow) return;Vector3 moveDirection = GetMoveDirrection(); float currentAcceleration = _isGrounded ? _playerConfigs.acceleration : _playerConfigs.airAcceleration; if (!_isGrounded) { Vector3 currentVelocity = new Vector3(_rb.linearVelocity.x, 0, _rb.linearVelocity.z); float maxAirSpeed = _playerConfigs.walkSpeed; if (currentVelocity.magnitude > maxAirSpeed) currentVelocity = currentVelocity.normalized * maxAirSpeed; Vector3 airControl = moveDirection * (currentAcceleration * _playerConfigs.walkSpeedAirModif * Time.fixedDeltaTime); Vector3 newVelocity = currentVelocity + airControl; if (newVelocity.magnitude > maxAirSpeed) newVelocity = newVelocity.normalized * maxAirSpeed; newVelocity.y = _rb.linearVelocity.y; _rb.linearVelocity = newVelocity; } else { Vector3 targetVelocity = moveDirection * _playerConfigs.walkSpeed; Vector3 newVelocity = Vector3.MoveTowards( new Vector3(_rb.linearVelocity.x, 0, _rb.linearVelocity.z), targetVelocity, currentAcceleration * Time.fixedDeltaTime ); newVelocity.y = _rb.linearVelocity.y; _rb.linearVelocity = newVelocity; }}
Вертикальный геймплей: прыжки и рывки
Система поддерживает «койот-тайм» (возможность прыгнуть в течение доли секунды после схода с платформы) и прыжки от стен.
private void PerformJump() { bool canJump = (Time.time - _lastGroundedTime <= _coyoteTime) && (Time.time - _lastJumpTime >= _jumpCoyoteTime);if (canJump) Jump(); else if (CheckWallsAround(out Vector3 wallNormal) && _wallJumpWithoutGrounded < _playerConfigs.maxWallJumpsWithoutGrounded) JumpWall(wallNormal);}
private void Dash()
{
Vector3 dashVector = GetMoveDirrection();
if (dashVector.magnitude < 0.1f)
{
dashVector = _cameraHolder.transform.forward;
dashVector.y = 0;
}_dashStartTime = Time.time; _rb.linearVelocity = Vector3.zero; _rb.AddForce(dashVector * _playerConfigs.dashForce, ForceMode.Impulse);}
Система жизненного цикла и вызовов
Для корректной работы физики и плавности управления мы распределяем вызовы между
UpdateиFixedUpdate.private void FixedUpdate() { CheckGround(); HandleGravity(); }private void Update() { CalculateView(); CalculateDashStatus(); CalculateMoveVelocity(); }
Метод
CheckGround()не просто проверяет наличие коллизии под ногами, но и генерирует событие приземления, что важно для эффектов соударения.private void CheckGround() { bool wasGrounded = _isGrounded; _isGrounded = Physics.Raycast(transform.position, Vector3.down, _rayLength, _groundLayer);if (_isGrounded) { _lastGroundedTime = Time.time; _wallJumpWithoutGrounded = 0; } if (!wasGrounded && _isGrounded) landingEvent?.Invoke();}
Интеграция New Input System
Использование современной системы ввода позволяет абстрагироваться от конкретных клавиш и легко реализовать кроссплатформенность. Вместо проверки нажатия условной клавиши «Space», мы подписываемся на абстрактное действие
Jump.Настройка Action Maps
- Actions: Создаем действия для перемещения (Vector2), обзора (Vector2) и триггерные действия (Button) для прыжка и рывка.
- Bindings: Привязываем WASD к движению и Delta мыши к обзору.
Чтобы избежать дублирования кода, создадим единую точку доступа к вводу:
public class PlayerInputSystem : MonoBehaviour { private InputSystem_Actions _actions; public InputSystem_Actions Actions => _actions;private void Awake() { _actions = new InputSystem_Actions(); _actions.Enable(); }}
Связывание ввода с логикой
В методе
Startмы подписываем методы контроллера на события системы ввода:private void Start() { _rb = GetComponent<Rigidbody>();_playerInputSystem.Actions.Character.Movement.performed += ctx => _inputMovement = ctx.ReadValue<Vector2>(); _playerInputSystem.Actions.Character.Movement.canceled += ctx => _inputMovement = Vector2.zero; _playerInputSystem.Actions.Character.View.performed += ctx => _inputView = ctx.ReadValue<Vector2>(); _playerInputSystem.Actions.Character.View.canceled += ctx => _inputView = Vector2.zero; _playerInputSystem.Actions.Character.Jump.performed += ctx => PerformJump(); _playerInputSystem.Actions.Character.Dash.performed += ctx => PerformDash();}
Событийная модель (Events)
Для того чтобы система перемещения оставалась «чистой» и не содержала в себе ссылок на аудио-сорс или компоненты тряски камеры, мы используем
UnityEvent. Это позволяет дизайнерам настраивать эффекты прямо в инспекторе Unity.public UnityEvent jumpEvent; public UnityEvent dashEvent; public UnityEvent landingEvent; public UnityEvent wallJumpEvent;Такой подход делает систему модульной: вы можете добавить звук шагов или партиклы пыли, не меняя ни единой строчки в основном скрипте передвижения.
Заключение
Представленная архитектура обеспечивает баланс между производительностью и гибкостью. Разделение на логические блоки, использование New Input System и событийная модель позволяют создать надежную основу для динамичного шутера. Код легко расширяется новыми механиками, такими как скольжение по поверхностям или бег по стенам, сохраняя при этом читаемость и стабильность.
Obsidian представила первое дополнение для The Outer Worlds 2, которое выйдет уже скоро
Mass Effect ещё вернётся, а GTA VI будет работать при 30 fps даже на PS5 Pro — главное за 10 ноября
Двухмерную метроидванию Nine Sols в духе Sekiro перенесли на Android
На Prime Video стартовал третий сезон «Колец власти»
Издатель отреагировал на скандал после удаления You are Empty из Steam за нарушение авторских прав
Лучший сезон или худший? Вышли первые рецензии на третий сезон сериала «Властелин колец: Кольца власти»
Пятикратно пережёванное аниме и критика Хаяо Миядзаки в адрес нового поколения аниматоров
Ролевой экшен UnGodly от создателя Star Wars: Galaxy of Heroes вышел на смартфонах после двух переносов