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&lt;Vector2&gt;();
_playerInputSystem.Actions.Character.Movement.canceled += ctx => _inputMovement = Vector2.zero;

_playerInputSystem.Actions.Character.View.performed += ctx => _inputView = ctx.ReadValue&lt;Vector2&gt;();
_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 и событийная модель позволяют создать надежную основу для динамичного шутера. Код легко расширяется новыми механиками, такими как скольжение по поверхностям или бег по стенам, сохраняя при этом читаемость и стабильность.

 

Источник

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