LEARN X · ЗА 18 МИН

Unity

Основы Unity за 18 минут: GameObject, компоненты, MonoBehaviour, жизненный цикл, Transform, физика, корутины, ввод, векторы и движение игрока.

Unity — движок для 2D/3D игр на C#. Логику пишут в скриптах-компонентах. Этот тур — про сам ДВИЖОК (его концепции и API), а не про язык C#. Всё объяснение спрятано в комментариях рабочего кода: читай сверху вниз.

Концепции: GameObject, Component, Scene

Сцена (Scene) — это контейнер уровня. Всё в ней — GameObject. Поведение даёт не сам GameObject, а навешанные на него Component'ы.

// ИЕРАРХИЯ ПОНЯТИЙ Unity:
//
// Scene (сцена / уровень)
//  └─ GameObject (объект на сцене: игрок, враг, камера, свет)
//       └─ Component (компонент — кусочек поведения/данных)
//            ├─ Transform   // позиция, поворот, масштаб (есть ВСЕГДА)
//            ├─ MeshRenderer// как объект выглядит
//            ├─ Collider    // форма столкновений
//            ├─ Rigidbody   // физика
//            └─ ВашСкрипт   // ваша логика (наследник MonoBehaviour)
//
// ГЛАВНАЯ ИДЕЯ: GameObject сам по себе пустой.
// Всё, что он умеет, дают навешанные компоненты (композиция, не наследование).
//
// PREFAB (префаб) — заранее настроенный GameObject, сохранённый
// как ассет-шаблон. Из одного префаба создают много копий
// (пули, враги, монеты) — меняешь префаб, меняются все экземпляры.
//
// ИЕРАРХИЯ (Hierarchy) — дерево объектов: у объекта есть родитель
// и дети. Дочерние двигаются/вращаются вместе с родителем.

Скрипт — компонент MonoBehaviour

Свой скрипт — это класс, унаследованный от MonoBehaviour. Имя файла должно совпадать с именем класса.

using UnityEngine;   // основное пространство имён движка

// Класс ДОЛЖЕН наследовать MonoBehaviour, чтобы стать компонентом
// и попасть в жизненный цикл движка (Start/Update и т.д.).
// Имя файла = имя класса: PlayerController.cs
public class PlayerController : MonoBehaviour
{
    // Поля, методы жизненного цикла и своя логика — внутри.
    // this.gameObject  — GameObject, на котором висит скрипт
    // this.transform   — его Transform (ярлык к компоненту Transform)
    void Start()
    {
        // Debug.Log выводит сообщение в консоль Unity
        Debug.Log("Привет из компонента на объекте " + gameObject.name);
    }
}

Жизненный цикл (callback-методы)

Движок сам вызывает специальные методы в определённом порядке. Объявляешь метод с нужным именем — Unity его подхватит.

public class LifecycleDemo : MonoBehaviour
{
    // Вызывается ОДИН раз при создании объекта, ещё до Start.
    // Здесь обычно кэшируют ссылки на компоненты.
    void Awake() { }

    // Включение компонента (галочка в инспекторе / SetActive(true))
    void OnEnable() { }

    // ОДИН раз перед первым Update (но после всех Awake на сцене).
    // Здесь — стартовая инициализация, зависящая от других объектов.
    void Start() { }

    // КАЖДЫЙ кадр. Частота плавает (зависит от FPS).
    // Сюда — ввод, нефизическую логику. Умножай на Time.deltaTime!
    void Update() { }

    // С ФИКСИРОВАННЫМ шагом (по умолчанию 50 раз/сек).
    // Сюда — всю работу с физикой (Rigidbody, силы).
    void FixedUpdate() { }

    // КАЖДЫЙ кадр, но ПОСЛЕ всех Update.
    // Сюда — слежение камеры за игроком (чтоб игрок уже сдвинулся).
    void LateUpdate() { }

    void OnDisable() { } // выключение компонента
    void OnDestroy() { } // объект уничтожают
}

Transform — позиция, поворот, масштаб

Transform есть у каждого GameObject. Через него двигают и вращают объект.

public class TransformDemo : MonoBehaviour
{
    void Update()
    {
        // ЧТЕНИЕ/ЗАПИСЬ напрямую:
        transform.position = new Vector3(0f, 1f, 0f); // мировые координаты
        transform.localPosition = Vector3.zero;       // относительно родителя
        transform.localScale = new Vector3(2f, 2f, 2f);// масштаб (в 2 раза)
        transform.rotation = Quaternion.identity;     // поворот (без поворота)

        // Поворот в градусах вокруг осей (X, Y, Z):
        transform.eulerAngles = new Vector3(0f, 90f, 0f);

        // СДВИГ относительно текущей позиции (метры за кадр):
        // *Time.deltaTime делает движение независимым от FPS
        transform.Translate(Vector3.forward * 3f * Time.deltaTime);

        // ВРАЩЕНИЕ: 90 градусов в секунду вокруг оси Y
        transform.Rotate(Vector3.up, 90f * Time.deltaTime);

        // Развернуться лицом к точке (например, к цели):
        transform.LookAt(new Vector3(5f, 0f, 5f));
    }
}

Доступ к компонентам: GetComponent

Чтобы управлять другим компонентом объекта, его надо сначала получить.

// RequireComponent гарантирует: при добавлении этого скрипта
// Rigidbody добавится автоматически и его нельзя будет удалить.
[RequireComponent(typeof(Rigidbody))]
public class ComponentAccess : MonoBehaviour
{
    private Rigidbody rb; // кэшируем ссылку, чтобы не искать каждый кадр

    void Awake()
    {
        // GetComponent<T>() ищет компонент типа T на ЭТОМ объекте.
        // ДОРОГО вызывать каждый кадр — берём один раз в Awake.
        rb = GetComponent<Rigidbody>();

        // Искать на детях / на родителе:
        var col = GetComponentInChildren<Collider>();
        var parentScript = GetComponentInParent<ComponentAccess>();

        // Попытка получить (без исключения, если нет):
        if (TryGetComponent<Rigidbody>(out var body))
        {
            body.mass = 2f;
        }
    }
}

Сериализованные поля в инспекторе

Поля, которые видны и редактируются в окне Inspector, можно настраивать без перекомпиляции.

public class InspectorFields : MonoBehaviour
{
    // public-поле видно и РЕДАКТИРУЕТСЯ в инспекторе:
    public float speed = 5f;

    // ЛУЧШЕ так: приватно для кода, но видно в инспекторе.
    // Инкапсуляция сохраняется, значение настраивается мышью.
    [SerializeField] private int health = 100;

    // Ползунок с диапазоном прямо в инспекторе:
    [SerializeField, Range(0f, 1f)] private float volume = 0.8f;

    // Подсказка при наведении в инспекторе:
    [Tooltip("Цель, за которой следит враг")]
    [SerializeField] private Transform target;

    // Скрыть public-поле из инспектора:
    [HideInInspector] public bool isAlive = true;
}

Ввод (Input)

Чтение клавиатуры, мыши и осей движения. Ввод обычно читают в Update.

public class InputDemo : MonoBehaviour
{
    void Update()
    {
        // ЗАЖАТА ли клавиша (каждый кадр, пока держат):
        if (Input.GetKey(KeyCode.W)) { /* идём вперёд */ }

        // НАЖАТА в этом кадре (один раз на нажатие) — для прыжка/выстрела:
        if (Input.GetKeyDown(KeyCode.Space)) { /* прыжок */ }

        // ОТПУЩЕНА в этом кадре:
        if (Input.GetKeyUp(KeyCode.Space)) { }

        // ОСИ движения: возвращают значение от -1 до 1 (плавно).
        // "Horizontal" = A/D и стрелки, "Vertical" = W/S.
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        Vector3 move = new Vector3(h, 0f, v);

        // Виртуальные кнопки (настраиваются в Project Settings > Input):
        if (Input.GetButtonDown("Jump")) { }
        if (Input.GetButtonDown("Fire1")) { /* ЛКМ / Ctrl */ }

        // Мышь: позиция курсора и движение мыши по осям:
        Vector3 mouse = Input.mousePosition;
        float mx = Input.GetAxis("Mouse X");
    }
    // НОВЫЙ Input System (пакет com.unity.inputsystem) — альтернатива:
    // действия задаются ассетом InputActions, читаются через
    // Keyboard.current[Key.W].isPressed или колбэки. Старый Input проще
    // для начала; новый — гибче и поддерживает геймпады из коробки.
}

Физика: Rigidbody и коллайдеры

Чтобы движок управлял объектом по законам физики, нужен Rigidbody. Forму столкновений задаёт Collider.

[RequireComponent(typeof(Rigidbody))]
public class PhysicsDemo : MonoBehaviour
{
    private Rigidbody rb;
    void Awake() { rb = GetComponent<Rigidbody>(); }

    // ВСЮ физику двигаем в FixedUpdate (фиксированный шаг):
    void FixedUpdate()
    {
        // AddForce — приложить силу (объект разгоняется плавно):
        rb.AddForce(Vector3.forward * 10f);

        // Импульс — мгновенный толчок (прыжок):
        rb.AddForce(Vector3.up * 5f, ForceMode.Impulse);

        // Прямая установка скорости:
        rb.velocity = new Vector3(0f, rb.velocity.y, 5f);
    }

    // СТОЛКНОВЕНИЯ (у обоих обычный Collider, есть Rigidbody):
    void OnCollisionEnter(Collision col)
    {
        // col.gameObject — с кем столкнулись
        if (col.gameObject.CompareTag("Enemy")) { /* урон */ }
    }
    void OnCollisionExit(Collision col) { }

    // ТРИГГЕРЫ (у коллайдера включён isTrigger — нет физ. отскока,
    // только событие пересечения): зоны, подбор предметов.
    void OnTriggerEnter(Collider other)
    {
        if (other.CompareTag("Coin")) { Destroy(other.gameObject); }
    }
    void OnTriggerExit(Collider other) { }
    // Для 2D — отдельные методы: OnCollisionEnter2D, OnTriggerEnter2D,
    // и компоненты Rigidbody2D / Collider2D.
}

Время

Кадры идут неравномерно. Чтобы скорость не зависела от FPS, всё умножают на Time.deltaTime.

public class TimeDemo : MonoBehaviour
{
    void Update()
    {
        // deltaTime — сколько СЕКУНД прошло с прошлого кадра.
        // speed (метров/сек) * deltaTime = метров за ЭТОТ кадр.
        float speed = 5f;
        transform.Translate(Vector3.forward * speed * Time.deltaTime);

        // time — сколько секунд прошло с запуска игры:
        float t = Time.time;

        // fixedDeltaTime — постоянный шаг для FixedUpdate (по умолч. 0.02):
        float fdt = Time.fixedDeltaTime;

        // Замедление/пауза времени (timeScale = 0 — полная пауза):
        Time.timeScale = 0.5f; // всё в 2 раза медленнее
    }
}

Создание и уничтожение объектов

Объекты порождают из префабов через Instantiate и убирают через Destroy.

public class SpawnDemo : MonoBehaviour
{
    [SerializeField] private GameObject bulletPrefab; // ссылка на префаб

    void Fire()
    {
        // Instantiate создаёт КОПИЮ префаба на сцене.
        // Аргументы: что, где (позиция), как повёрнут.
        GameObject bullet = Instantiate(
            bulletPrefab, transform.position, transform.rotation);

        // Destroy уничтожает объект. Второй аргумент — задержка в сек:
        Destroy(bullet, 3f);  // самоуничтожение через 3 секунды
        // Destroy(gameObject); // уничтожить сам объект со скриптом
    }

    // OBJECT POOLING (пул объектов) — оптимизация: вместо постоянных
    // Instantiate/Destroy (они нагружают сборщик мусора) заранее
    // создают запас объектов, прячут их (SetActive(false)) и
    // переиспользуют — показывают/прячут вместо создания/удаления.
    // В Unity есть готовый класс UnityEngine.Pool.ObjectPool<T>.
}

Корутины (Coroutine)

Корутина — метод, который умеет «ставить себя на паузу» и продолжать в следующих кадрах. Удобно для задержек и анимаций по времени.

using System.Collections; // нужно для IEnumerator
using UnityEngine;

public class CoroutineDemo : MonoBehaviour
{
    void Start()
    {
        // Запуск корутины:
        StartCoroutine(Explode());
    }

    // Тип возврата — IEnumerator. yield return ставит паузу.
    IEnumerator Explode()
    {
        Debug.Log("Отсчёт пошёл...");

        // Пауза на 2 секунды игрового времени:
        yield return new WaitForSeconds(2f);

        Debug.Log("Бабах!");

        // Подождать ДО следующего кадра:
        yield return null;

        // Подождать до конца кадра (после отрисовки):
        yield return new WaitForEndOfFrame();

        // Дождаться условия (пока true — ждём):
        yield return new WaitUntil(() => transform.position.y < 0f);
    }

    // Остановить: StopCoroutine(...) или StopAllCoroutines();
}

Векторы и кватернионы

Vector3/Vector2 — точки и направления. Quaternion — повороты (без «блокировки осей»).

public class VectorMath : MonoBehaviour
{
    void Demo()
    {
        // 3D-вектор (x, y, z) и удобные константы:
        Vector3 a = new Vector3(1f, 2f, 3f);
        Vector3 up = Vector3.up;       // (0,1,0)
        Vector3 fwd = Vector3.forward; // (0,0,1)

        // Арифметика векторов:
        Vector3 sum = a + up;          // сложение
        Vector3 scaled = a * 2f;       // умножение на число
        float len = a.magnitude;       // длина вектора
        Vector3 dir = a.normalized;    // длина = 1 (только направление)

        // Расстояние между точками:
        float dist = Vector3.Distance(a, up);

        // Скалярное произведение (косинус угла, если оба нормированы):
        float dot = Vector3.Dot(fwd, up);

        // Плавная интерполяция от A к B (t от 0 до 1):
        Vector3 mid = Vector3.Lerp(a, up, 0.5f);

        // 2D-вектор (для 2D-игр):
        Vector2 p = new Vector2(4f, 5f);

        // Quaternion — поворот. Из углов Эйлера:
        Quaternion rot = Quaternion.Euler(0f, 90f, 0f);

        // Плавный доворот к цели (Slerp для поворотов):
        transform.rotation = Quaternion.Slerp(
            transform.rotation, rot, Time.deltaTime);
    }
}

Поиск объектов, теги и слои

Объекты находят по типу, тегу или имени. Теги и слои — способ помечать и группировать объекты.

public class FindDemo : MonoBehaviour
{
    void Start()
    {
        // По ТЕГУ (тег задаётся в инспекторе сверху объекта):
        GameObject player = GameObject.FindWithTag("Player");

        // По ИМЕНИ (медленно, избегай в Update):
        GameObject boss = GameObject.Find("Boss");

        // Все объекты с тегом:
        GameObject[] enemies = GameObject.FindGameObjectsWithTag("Enemy");

        // Найти компонент нужного типа на сцене (Unity 2023+):
        var spawner = Object.FindFirstObjectByType<SpawnDemo>();

        // Проверка тега у конкретного объекта:
        if (player != null && player.CompareTag("Player")) { }

        // СЛОИ (Layer) — для масок: по каким слоям бьёт луч/физика.
        // Например, луч (Raycast) только по слою "Ground":
        int mask = LayerMask.GetMask("Ground");
        if (Physics.Raycast(transform.position, Vector3.down,
                            out RaycastHit hit, 5f, mask))
        {
            Debug.Log("Под нами земля: " + hit.collider.name);
        }
    }
}

UI и звук (кратко)

Интерфейс рисуется на Canvas, звук проигрывает AudioSource.

using UnityEngine;
using UnityEngine.UI;   // Button, Image (классический UI)
using TMPro;            // TextMeshProUGUI — текст (стандарт в Unity)

public class UiAudioDemo : MonoBehaviour
{
    // UI ВСЕГДА лежит внутри объекта Canvas в иерархии.
    [SerializeField] private Button startButton;
    [SerializeField] private TextMeshProUGUI scoreText;
    [SerializeField] private AudioSource audioSource; // проигрыватель звука
    [SerializeField] private AudioClip coinClip;      // звуковой файл

    void Start()
    {
        // Подписка на клик кнопки:
        startButton.onClick.AddListener(OnStart);
        scoreText.text = "Счёт: 0";
    }

    void OnStart()
    {
        // Проиграть звук один раз (поверх текущего):
        audioSource.PlayOneShot(coinClip);
    }
}

Итог: контроллер движения игрока

Собираем изученное: ввод в Update, физику в FixedUpdate, прыжок через триггерную проверку земли.

using UnityEngine;

[RequireComponent(typeof(Rigidbody))] // физика обязательна
public class PlayerMovement : MonoBehaviour
{
    [SerializeField] private float speed = 6f;     // скорость, м/с
    [SerializeField] private float jumpForce = 5f; // сила прыжка
    [SerializeField] private LayerMask groundMask; // что считать землёй

    private Rigidbody rb;
    private Vector3 input;     // желаемое направление от игрока
    private bool jumpQueued;   // запомнили нажатие прыжка

    void Awake()
    {
        // Кэшируем Rigidbody один раз — GetComponent дорогой:
        rb = GetComponent<Rigidbody>();
    }

    void Update()
    {
        // ВВОД читаем в Update (он точнее ловит нажатия):
        float h = Input.GetAxis("Horizontal");
        float v = Input.GetAxis("Vertical");
        input = new Vector3(h, 0f, v).normalized; // не быстрее по диагонали

        // Прыжок — только если стоим на земле:
        if (Input.GetButtonDown("Jump") && IsGrounded())
        {
            jumpQueued = true; // применим в FixedUpdate
        }
    }

    void FixedUpdate()
    {
        // ДВИЖЕНИЕ через физику, с учётом постоянного шага:
        Vector3 step = input * speed * Time.fixedDeltaTime;
        rb.MovePosition(rb.position + step);

        // Применяем отложенный прыжок импульсом вверх:
        if (jumpQueued)
        {
            rb.AddForce(Vector3.up * jumpForce, ForceMode.Impulse);
            jumpQueued = false;
        }
    }

    // Луч вниз: есть ли земля под ногами (диапазон 1.1 м):
    bool IsGrounded()
    {
        return Physics.Raycast(
            transform.position, Vector3.down, 1.1f, groundMask);
    }
}
Поддержать проект