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