0_Введение и основной цикл движка

Урок 0 : Введение и основной цикл движка

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

Знакомство с рабочей средой Unity: основные окна редактора, сцены и префабы как способ организовать игровой мир, инструменты отладки через консоль. В конце разбираем сердце движка - цикл MonoBehaviour и порядок вызова методов.


1. Интерфейс Unity

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

Scene - трёхмерный вьюпорт. Здесь ты двигаешь объекты, расставляешь их по сцене, смотришь на мир «глазами разработчика». Навигация: зажать правую кнопку мыши + WASD для полёта; Alt + ЛКМ для вращения вокруг точки; колёсико мыши - приближение.

Game - то, что увидит игрок в Play Mode. Это точный вид через игровую камеру. Пока игра не запущена, окно пустое. Нажми Ctrl+P (или кнопку Play) - Unity компилирует скрипты и запускает симуляцию.

Важно помнить

Любые изменения, сделанные в Play Mode (перемещение объектов, правка значений в Inspector), теряются при выходе. Это типичная ловушка новичков.

Hierarchy - дерево всех объектов текущей сцены. Слева от имени объекта - стрелка раскрытия дочерних объектов. Объект с дочерними - как папка с файлами.

Project - файловый менеджер ассетов: скрипты, текстуры, звуки, префабы. Структура папок здесь напрямую соответствует папкам на диске.

Inspector - панель свойств выбранного объекта или ассета. Именно здесь ты видишь все компоненты объекта и редактируешь их поля.

Console - вывод сообщений, предупреждений и ошибок. Без неё отладка невозможна - подробнее в разделе 3.

Горячие клавиши в Scene View

Клавиша Действие
W Move Tool - перемещение
E Rotate Tool - вращение
R Scale Tool - масштаб
Q Select / Hand Tool
F Сфокусировать вид на выбранном объекте
Ctrl+D Дублировать объект
Ctrl+Z Отменить действие
Настрой рабочее пространство под себя

Окна Unity можно перетаскивать, закреплять и отстыковывать. Встроенные лэйауты (Default, 2 by 3, Wide) доступны через меню Window → Layouts.


2. Сцены и префабы

Сцена

Сцена (Scene) - это контейнер, в котором живут все объекты: персонажи, декорации, камера, источники света. Файл сцены сохраняется с расширением .unity.

Когда игра загружает новую сцену, все объекты из предыдущей уничтожаются (если явно не указано DontDestroyOnLoad). Это как смена декораций в театре - старые убирают перед следующим актом.

Загрузить сцену можно командой SceneManager.LoadScene("SceneName");

Зачем делить игру на несколько сцен?

Префаб

Префаб (Prefab) - шаблон GameObject, сохранённый как ассет в папке Project. Думай о нём как о чертеже: из одного чертежа можно создать сколько угодно одинаковых объектов - и изменение чертежа обновит все копии сразу.

Создать префаб просто: перетащи объект из Hierarchy в папку в Project. Значок объекта в Hierarchy станет синим - признак того, что это экземпляр (instance) префаба.

Изменить префаб можно двумя способами:

  1. Двойной клик на файл префаба в Project - откроется режим редактирования префаба.
  2. Внести изменения в экземпляр, затем нажать Overrides → Apply All в Inspector.

Prefab Variant

Prefab Variant - разновидность, которая наследует базовый префаб, но переопределяет часть его свойств. Это как классы и наследование в коде, только визуально.

Пример: есть EnemyBase с компонентами движения и AI. Создаём варианты:

Изменение EnemyBase (например, новая анимация смерти) автоматически применяется к обоим вариантам. Но локальные переопределения (speed, health) не затрагиваются.


3. Консоль и Debug

Консоль Unity - главный инструмент отладки. Класс Debug позволяет выводить любую информацию прямо в редактор.

// Три уровня важности
Debug.Log("Игра запущена");          // белое сообщение - информация
Debug.LogWarning("Здоровье низкое"); // жёлтое с треугольником - предупреждение
Debug.LogError("Объект не найден");  // красное с кружком - ошибка

Клик на любое сообщение в Console показывает stack trace - цепочку вызовов, которая привела к этой строке. Клик на конкретную строку в stack trace открывает нужный файл в IDE на нужной строке. Это быстрее, чем искать вручную.

Форматирование вывода

Вместо конкатенации строк используй строковую интерполяцию - читается гораздо чище:

void Update()
{
    // Интерполяция: $ перед строкой, переменная в {}
    Debug.Log($"Позиция: {transform.position}");
    Debug.Log($"Кадр: {Time.frameCount}, время: {Time.time:F2}");
    
    // Можно передать второй аргумент - контекст (объект)
    // При клике на сообщение в Console объект выделится в Hierarchy
    Debug.Log($"Создан: {gameObject.name}", gameObject);
}

Кнопки панели Console

Кнопка Назначение
Clear Очистить все сообщения
Collapse Объединить дубликаты в одну строку с счётчиком
Clear on Play Очищать консоль при каждом нажатии Play
Error Pause Поставить игру на паузу при первой ошибке
Error Pause - твой лучший друг

Включи Error Pause во время разработки. Когда что-то пойдёт не так, игра остановится в точный момент ошибки - это намного информативнее, чем смотреть на замёрзшую игру и гадать, что произошло.


4. Основной игровой цикл MonoBehaviour

Что такое MonoBehaviour

MonoBehaviour - базовый класс для всех скриптов, которые ты прикрепляешь к GameObject. Именно он подключает твой код к игровому циклу Unity: движок сам вызывает нужные методы в строго определённом порядке - ты просто пишешь их тела.

Это не параллельные потоки и не случайный порядок - это детерминированная последовательность, одинаковая при каждом запуске.

Схема порядка вызовов

═══════════════════════════════════════════════
         ЖИЗНЕННЫЙ ЦИКЛ MONOBEHAVIOUR
═══════════════════════════════════════════════

ИНИЦИАЛИЗАЦИЯ (один раз при создании объекта)
─────────────────────────────────────────────
  Awake()         ← первым, даже если компонент выключен
      ↓
  OnEnable()      ← при включении объекта/компонента
      ↓
  Start()         ← после всех Awake() на сцене

═══════════════════════════════════════════════

ОСНОВНОЙ ЦИКЛ (повторяется)
─────────────────────────────────────────────
  FixedUpdate()   ← фиксированный шаг (~50 раз/сек, для физики)
      ↓
  Update()        ← каждый кадр (для логики и ввода)
      ↓
  LateUpdate()    ← после всех Update (для камеры)

═══════════════════════════════════════════════

ЗАВЕРШЕНИЕ
─────────────────────────────────────────────
  OnDisable()     ← при выключении компонента/объекта
      ↓
  OnDestroy()     ← при уничтожении объекта

═══════════════════════════════════════════════

Инициализация: Awake, OnEnable, Start

Три метода инициализации выглядят похожими, но у каждого своя роль:

Awake() вызывается самым первым - даже если компонент отключён в Inspector. Используй его для инициализации собственных переменных, которые не зависят от других объектов.

OnEnable() вызывается каждый раз, когда компонент включается. Удобно для подписки на события.

Start() вызывается один раз, но уже после того, как все Awake() на сцене отработали. Используй его когда нужно обратиться к уже инициализированным компонентам других объектов.

public class Player : MonoBehaviour
{
    [SerializeField] private int _health;
    private Rigidbody _rb;

    void Awake()
    {
        // Инициализируем собственные поля - Rigidbody точно на этом же объекте
        _rb = GetComponent<Rigidbody>();
        _health = 100;
        Debug.Log($"Awake: {gameObject.name}");
    }

    void Start()
    {
        // Start - безопасно обращаться к другим объектам:
        // их Awake() уже выполнился
        var enemy = FindObjectOfType<Enemy>();
        Debug.Log($"Start: враг найден - {enemy != null}");
    }
}

Основной цикл: FixedUpdate, Update, LateUpdate

FixedUpdate() работает с фиксированным шагом времени - по умолчанию 0.02 секунды (50 вызовов в секунду). Этот шаг не зависит от FPS. Именно поэтому весь код физики (применение сил, работа с Rigidbody) должен быть здесь - физический движок Unity синхронизирован с этим шагом.

Update() вызывается каждый кадр. Время между кадрами непостоянно: на мощном ПК кадр длится 4ms, на слабом - 30ms. Если двигать объект на фиксированное расстояние в Update, на разных машинах персонаж будет двигаться с разной скоростью.

Решение - Time.deltaTime: время, прошедшее с предыдущего кадра, в секундах. Умножение скорости на deltaTime делает движение независимым от FPS:

public class Movement : MonoBehaviour
{
    [SerializeField] private float _speed = 5f; // юниты в секунду

    void Update()
    {
        // НЕПРАВИЛЬНО: на 60fps переместится на 0.1 * 60 = 6 ед/сек
        //              на 30fps переместится на 0.1 * 30 = 3 ед/сек (вдвое медленнее!)
        // transform.Translate(Vector3.forward * 0.1f);

        // ПРАВИЛЬНО: _speed ед/сек на любом FPS
        // На 60fps: 5 * 0.0167 ≈ 0.083 ед/кадр
        // На 30fps: 5 * 0.0333 ≈ 0.167 ед/кадр - итого те же 5 ед/сек
        transform.Translate(Vector3.forward * _speed * Time.deltaTime);
    }
}

LateUpdate() выполняется после того, как все Update() на сцене отработали. Идеальное место для камеры: к моменту LateUpdate игрок уже переместился, и камера может корректно встать за ним без дрожания.

public class CameraFollow : MonoBehaviour
{
    [SerializeField] private Transform _target;
    [SerializeField] private Vector3 _offset = new Vector3(0, 5, -7);

    void LateUpdate()
    {
        // Игрок уже сдвинулся в Update - камера плавно следует
        transform.position = Vector3.Lerp(
            transform.position,
            _target.position + _offset,
            Time.deltaTime * 5f
        );
        transform.LookAt(_target);
    }
}

Завершение: OnDisable и OnDestroy

OnDisable() вызывается при выключении компонента или объекта (через SetActive(false) или снятием галочки). Используй для отписки от событий - это предотвращает утечки памяти.

OnDestroy() вызывается перед уничтожением объекта (через Destroy()). Здесь нужно освободить ресурсы, которые не управляются Unity автоматически.

Полный цикл в одном скрипте

На практике ты редко пишешь все методы в одном классе - только те, что нужны для конкретного компонента. Если метода нет, Unity не вызывает его вообще, так что нет смысла держать пустые заглушки.


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