3_Input и базовый контроллер персонажа

Урок 3 : Input и базовый контроллер персонажа

О чём этот урок

Разбираем систему ввода Unity: как читать клавиши и оси, почему GetAxis и GetAxisRaw дают разный результат. Затем собираем полный контроллер персонажа, соединяя Input из этого урока с физикой Rigidbody из предыдущего.


1. Система ввода

Класс Input - классический Input Manager

Класс Input - статический, методы которого можно вызывать из любого скрипта без ссылки. Он опрашивает состояние устройств ввода (клавиатура, мышь, геймпад) и возвращает мгновенный снимок: что нажато, что отпущено, куда двигается аналоговая ось.

Весь ввод нужно обрабатывать в Update() - события захватываются один раз в кадр, и если опросить их в FixedUpdate, можно пропустить короткое нажатие.

Три состояния кнопки

void Update()
{
    // GetKey - true каждый кадр, пока кнопка зажата
    // Используй для: удержание кнопки, зажатый огонь
    if (Input.GetKey(KeyCode.Space))
        Debug.Log("Пробел зажат");

    // GetKeyDown - true ТОЛЬКО в первый кадр нажатия
    // Используй для: прыжок, одиночный выстрел, открыть дверь
    if (Input.GetKeyDown(KeyCode.Space))
    {
        Debug.Log("Пробел нажат (однократно)");
        Jump();
    }

    // GetKeyUp - true ТОЛЬКО в первый кадр отпускания
    // Используй для: заряженный выстрел (отпустил - выстрелил)
    if (Input.GetKeyUp(KeyCode.Space))
        Debug.Log("Пробел отпущен");
}

GetAxis и GetAxisRaw - в чём разница

Input.GetAxis читает виртуальные оси, настроенные в Project Settings → Input Manager. Стандартные оси "Horizontal" (A/D или ←/→) и "Vertical" (W/S или ↑/↓) возвращают значение от -1 до 1.

Разница между двумя методами принципиальная:

void Update()
{
    // GetAxis - плавное нарастание и убывание (Input Manager сглаживает значение)
    // Нажал D: 0 → 0.2 → 0.5 → 1.0 (за несколько кадров)
    // Отпустил: 1.0 → 0.5 → 0.1 → 0 (инерция)
    // Используй для: плавного движения персонажа, аналогового управления
    float smoothH = Input.GetAxis("Horizontal");

    // GetAxisRaw - дискретное: -1, 0 или 1, без сглаживания
    // Нажал D: мгновенно 1. Отпустил: мгновенно 0.
    // Используй для: отзывчивого управления, клеточного движения
    float rawH = Input.GetAxisRaw("Horizontal");
}
Когда что использовать

GetAxisRaw лучше для большинства геймплейных задач: отклик мгновенный, персонаж реагирует точно. GetAxis хорош там, где нужна инерция - например, вращение камеры или аналоговое управление машиной.

Мышь и касания

void Update()
{
    // Позиция мыши в экранных координатах (левый нижний угол = 0,0)
    Vector3 mousePos = Input.mousePosition;

    // Кнопки мыши: 0 = левая, 1 = правая, 2 = средняя
    if (Input.GetMouseButtonDown(0))
        Debug.Log("Левая кнопка нажата");

    // Движение мыши - дельта за кадр
    float mouseX = Input.GetAxis("Mouse X");
    float mouseY = Input.GetAxis("Mouse Y");
}

Новая система Input System

Input System - пакет нового поколения (устанавливается через Package Manager). Вместо опроса в Update используется модель событий: движок сам вызовет твой метод при нажатии кнопки. Поддерживает переназначение управления (rebinding) в runtime и все типы контроллеров из коробки.

Переключение в Project Settings → Player → Active Input Handling: выбери «Input System Package» или «Both» (работают оба одновременно).

Что использовать

Для учебных проектов классический Input проще и понятнее. Для релизного проекта с поддержкой геймпадов и ремаппингом - Input System. В этом курсе используем классический Input.


2. Разделение ответственности: InputSystem и MovementController

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

// InputSystem.cs - только читает ввод, живёт на отдельном объекте LOGIC/InputSystem
public class InputSystem : MonoBehaviour
{
    public float MoveHorizontalAxis { get; private set; }
    public float MoveVerticalAxis   { get; private set; }
    public bool  JumpPressed        { get; private set; }

    void Update()
    {
        MoveHorizontalAxis = Input.GetAxisRaw("Horizontal");
        MoveVerticalAxis   = Input.GetAxisRaw("Vertical");
        JumpPressed        = Input.GetKeyDown(KeyCode.Space);
    }
}

Такой скрипт легко заменить на новую Input System без переписывания логики движения - нужно изменить только InputSystem.cs.


3. Движение: Rigidbody vs Transform

Это один из главных выборов при создании контроллера персонажа. Ответ зависит от того, нужна ли тебе физика.

Методы Rigidbody (вызывать в FixedUpdate)

public class PhysicsController : MonoBehaviour
{
    [SerializeField] private float _speed = 5f;
    [SerializeField] private float _jumpForce = 7f;
    private Rigidbody _rb;

    void Awake()
    {
        _rb = GetComponent<Rigidbody>();
        _rb.constraints = RigidbodyConstraints.FreezeRotationX
                        | RigidbodyConstraints.FreezeRotationZ;
    }

    void FixedUpdate()
    {
        float h = Input.GetAxisRaw("Horizontal");
        float v = Input.GetAxisRaw("Vertical");

        // MovePosition - плавное кинематическое перемещение
        Vector3 targetPos = _rb.position + new Vector3(h, 0, v).normalized * _speed * Time.fixedDeltaTime;
        _rb.MovePosition(targetPos);
    }

    void Update()
    {
        // Прыжок - в Update, чтобы не пропустить нажатие
        if (Input.GetKeyDown(KeyCode.Space))
            _rb.AddForce(Vector3.up * _jumpForce, ForceMode.Impulse);
    }
}

Прямое управление velocity

void FixedUpdate()
{
    float h = Input.GetAxisRaw("Horizontal");

    // Прямое присвоение velocity - быстро, но «ломает» плавность физики
    // Сохраняем Y-скорость (гравитация), чтобы не отключить падение
    Vector3 vel = _rb.velocity;
    vel.x = h * _speed;
    _rb.velocity = vel;
}

Transform - телепорт (без физики)

void Update()
{
    float h = Input.GetAxisRaw("Horizontal");
    float v = Input.GetAxisRaw("Vertical");

    // Transform.Translate перемещает объект напрямую
    // Коллайдеры не участвуют - объект может пройти сквозь стену
    Vector3 direction = new Vector3(h, 0, v).normalized;
    transform.Translate(direction * _speed * Time.deltaTime, Space.World);
}

Сравнительная таблица

Метод Где вызывать Физика Коллизии Когда использовать
rb.MovePosition FixedUpdate Да Корректно Основное движение персонажа с физикой
rb.AddForce FixedUpdate Да Корректно Прыжок, взрыв, применение сил
rb.velocity = v FixedUpdate Частично Корректно Быстрый контроль горизонтальной скорости
transform.position = p Update Нет Игнорируются Спавн, телепорт, объекты без физики
transform.Translate Update Нет Игнорируются Визуальные объекты, UI-элементы

4. Полный контроллер персонажа

Собираем всё вместе - простой, но рабочий контроллер:

public class CharacterController3D : MonoBehaviour
{
    [Header("Движение")]
    [SerializeField] private float _moveSpeed = 5f;
    [SerializeField] private float _jumpForce = 6f;

    [Header("Проверка земли")]
    [SerializeField] private Transform _groundCheck;
    [SerializeField] private float _groundRadius = 0.2f;
    [SerializeField] private LayerMask _groundLayer;

    private Rigidbody _rb;
    private InputSystem _input;
    private bool _isGrounded;
    private Vector3 _moveInput;

    void Awake()
    {
        _rb = GetComponent<Rigidbody>();
        _rb.constraints = RigidbodyConstraints.FreezeRotationX
                        | RigidbodyConstraints.FreezeRotationZ;
        _input = FindObjectOfType<InputSystem>();

        if (_input == null)
            Debug.LogError("InputSystem не найден на сцене!");
    }

    void Update()
    {
        // Читаем ввод через InputSystem
        _moveInput = new Vector3(_input.MoveHorizontalAxis, 0, _input.MoveVerticalAxis).normalized;

        // Проверяем землю - CheckSphere в точке под персонажем
        _isGrounded = Physics.CheckSphere(
            _groundCheck.position,
            _groundRadius,
            _groundLayer
        );

        if (_input.JumpPressed && _isGrounded)
        {
            // Сбрасываем вертикальную скорость перед прыжком
            _rb.velocity = new Vector3(_rb.velocity.x, 0, _rb.velocity.z);
            _rb.AddForce(Vector3.up * _jumpForce, ForceMode.Impulse);
        }
    }

    void FixedUpdate()
    {
        // Применяем движение в FixedUpdate
        Vector3 targetVelocity = _moveInput * _moveSpeed;
        targetVelocity.y = _rb.velocity.y; // сохраняем вертикальную скорость (гравитация)
        _rb.velocity = targetVelocity;
    }
}
Как добавить Ground Check

Создай пустой GameObject, сделай его дочерним для персонажа и опусти к ногам (позиция 0, -1, 0 для капсулы высотой 2). Это и будет _groundCheck. Создай слой Ground и назначь его всем поверхностям. Назначь этот слой в поле _groundLayer через Inspector.


📋 Перейти к практике урока 3