1_Объекты и компоненты

Урок 1 : Объекты и компоненты

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

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


1. GameObject

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

Каждый GameObject имеет несколько базовых свойств, доступных в коде:

// Основные свойства любого объекта
Debug.Log(gameObject.name);   // имя из Hierarchy
Debug.Log(gameObject.tag);    // тег (настраивается в Inspector, например "Player")
Debug.Log(gameObject.layer);  // слой - целое число (0–31)

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

Instantiate создаёт копию префаба или любого объекта. Это главный способ спавнить врагов, пули, эффекты во время игры:

public class Spawner : MonoBehaviour
{
    [SerializeField] private GameObject _enemyPrefab;
    [SerializeField] private Transform _spawnPoint;

    void SpawnEnemy()
    {
        // Простой спавн - позиция и вращение берутся из самого префаба
        GameObject enemy = Instantiate(_enemyPrefab);

        // Спавн с указанием позиции и вращения
        GameObject enemy2 = Instantiate(
            _enemyPrefab,
            _spawnPoint.position,
            _spawnPoint.rotation
        );

        // Уничтожить через 5 секунд (не мгновенно - в конце кадра или по таймеру)
        Destroy(enemy2, 5f);
    }
}
Почему Destroy не мгновенный

Destroy(obj) помечает объект к удалению, но реально уничтожает его в конце текущего кадра. Это позволяет другим скриптам в том же кадре ещё обратиться к объекту. Если нужно проверить - используй obj == null (Unity переопределяет оператор сравнения для уничтоженных объектов).

Включение и выключение объектов

SetActive позволяет «заморозить» объект: выключенный GameObject не обрабатывается движком - не отрисовывается, не выполняет Update, не участвует в физике. Это дешевле, чем Destroy + Instantiate, и используется для пулинга объектов.

// Включить/выключить объект
gameObject.SetActive(false); // выключить
gameObject.SetActive(true);  // включить

// activeSelf - собственное состояние объекта
// activeInHierarchy - реальное состояние с учётом родителя
// Если родитель выключен, activeInHierarchy = false, даже если activeSelf = true
bool isReallyActive = gameObject.activeInHierarchy;

3D-примитивы

Unity может создавать базовые геометрические объекты через меню GameObject → 3D Object. Это полноценные объекты с Mesh Renderer и коллайдером:

Примитив Особенности
Cube Единичный куб
Sphere Единичная сфера
Capsule Высота 2, радиус 0.5 - стандарт для персонажей
Cylinder Высота 2, радиус 0.5
Plane Плоскость 10×10 единиц, без дна
Quad Плоский прямоугольник 1×1, одна сторона

2. Иерархия объектов

Как работает parent-child

Иерархия в Unity - это дерево объектов: у каждого может быть один родитель и множество дочерних объектов. Дочерний объект наследует трансформ родителя: если сдвинуть родителя, все дочерние сдвинутся вместе с ним.

Представь руку робота: рука - дочерний объект плеча. Когда плечо поворачивается, рука движется вместе с ним. Это и есть иерархия.

Создать связь в редакторе просто - перетащи объект в Hierarchy на другой объект, и первый станет дочерним.

Управление иерархией через код

public class HierarchyExample : MonoBehaviour
{
    [SerializeField] private Transform _newParent;

    void Start()
    {
        // Узнать родителя
        Transform parent = transform.parent; // null, если объект в корне сцены

        // Задать нового родителя
        transform.SetParent(_newParent);

        // Отсоединиться от родителя (в корень сцены)
        transform.SetParent(null);

        // Найти прямого дочернего объекта по имени
        Transform weapon = transform.Find("WeaponHolder");

        // Найти по индексу (0 = первый дочерний)
        Transform firstChild = transform.GetChild(0);

        // Количество дочерних
        int childCount = transform.childCount;
    }
}

localPosition vs position

Это важное различие: дочерний объект имеет две системы координат.

// Пример: дочерний объект на позиции (1, 0, 0) от родителя
// Родитель находится в мировой позиции (5, 0, 0)
// Тогда:
// localPosition = (1, 0, 0)
// position      = (6, 0, 0) - (5 + 1)

Debug.Log(transform.localPosition); // смещение от родителя
Debug.Log(transform.position);      // абсолютная позиция в мире

3. Компоненты и MonoBehaviour

Компонентная архитектура

Компонент (Component) - модуль с данными или поведением, прикреплённый к GameObject. Это не наследование, а композиция: объект не является танком - он содержит компоненты, которые делают его танком. Такой подход гибче: можно взять компонент стрельбы и прикрепить к любому объекту.

MonoBehaviour - специальный базовый класс для компонентов, которые участвуют в игровом цикле (имеют Awake, Update и т.д.). Не каждый компонент - MonoBehaviour: например, Transform, Rigidbody, AudioSource - они наследуются от Component, не от MonoBehaviour.

GetComponent

Главный способ получить ссылку на компонент - GetComponent<T>(). Метод ищет компонент типа T на том же объекте:

public class PlayerController : MonoBehaviour
{
    private Rigidbody _rb;
    private AudioSource _audio;

    void Awake()
    {
        // Получаем нужные компоненты при старте, кэшируем в поля
        // Никогда не вызываем GetComponent в Update - это медленно
        _rb = GetComponent<Rigidbody>();
        _audio = GetComponent<AudioSource>();
    }

    void Update()
    {
        // Используем уже готовую ссылку - быстро
        if (Input.GetKeyDown(KeyCode.Space))
        {
            _rb.AddForce(Vector3.up * 5f, ForceMode.Impulse);
            _audio.Play();
        }
    }
}

Поиск в дочерних и родительских объектах

void Awake()
{
    // Ищет компонент на этом объекте И во всех дочерних (рекурсивно)
    var childRenderer = GetComponentInChildren<MeshRenderer>();

    // Ищет компонент на этом объекте И во всех родительских (вверх по иерархии)
    var parentCanvas = GetComponentInParent<Canvas>();

    // Добавить компонент прямо во время игры
    var newRb = gameObject.AddComponent<Rigidbody>();
}

Null-check - обязательная привычка

GetComponent возвращает null, если компонент не найден. Обращение к null - это NullReferenceException. Всегда проверяй:

void Awake()
{
    var rb = GetComponent<Rigidbody>();

    if (rb == null)
    {
        // Сообщаем разработчику что пошло не так - и где
        Debug.LogError($"Rigidbody не найден на объекте {gameObject.name}!", gameObject);
        return;
    }

    // Теперь безопасно работаем с rb
    rb.mass = 2f;
}
this.gameObject и this.transform

В MonoBehaviour свойства gameObject и transform - это сокращения, не вызовы GetComponent. Используй их напрямую, без кэширования.


4. Transform: позиция, вращение, масштаб

Система координат Unity

Unity использует левостороннюю систему координат:

Это важно знать при работе с направлениями: Vector3.forward = (0, 0, 1).

Базовые свойства Transform

void Start()
{
    // Позиция
    transform.position = new Vector3(0, 1, 5);     // мировая
    transform.localPosition = new Vector3(0, 1, 0); // относительно родителя

    // Вращение - Unity хранит как Quaternion (избегает gimbal lock)
    // Для чтения и задания в градусах используй eulerAngles
    transform.localEulerAngles = new Vector3(0, 90, 0); // повернуть на 90° по Y

    // Масштаб (только локальный - мировой только для чтения через lossyScale)
    transform.localScale = new Vector3(2, 2, 2); // увеличить вдвое
}

Методы перемещения и вращения

void Update()
{
    // Translate - перемещение относительно локальных осей (по умолчанию Space.Self)
    transform.Translate(Vector3.forward * Time.deltaTime * 3f);

    // Translate в мировых осях
    transform.Translate(Vector3.up * Time.deltaTime, Space.World);

    // Rotate - поворот на заданные углы в градусах
    transform.Rotate(new Vector3(0, 90, 0) * Time.deltaTime);

    // LookAt - мгновенно повернуть лицом к цели (Vector3 или Transform)
    transform.LookAt(Camera.main.transform);
}

Плавное перемещение: телепорт vs Lerp

Прямое присвоение transform.position - это телепорт: объект мгновенно оказывается в новой точке. Коллайдеры при этом не учитываются, и объект может «пронырнуть» сквозь стену.

Vector3.Lerp (Linear Interpolation) вычисляет промежуточную точку между двумя позициями. Параметр t от 0 до 1: при t = 0 возвращается точка a, при t = 1 - точка b.

public class SmoothMover : MonoBehaviour
{
    [SerializeField] private Transform _target;

    void Update()
    {
        // Телепорт - объект мгновенно оказывается в позиции target
        // transform.position = _target.position;

        // Lerp с Time.deltaTime * скорость - эффект «догоняния»:
        // чем ближе к цели, тем медленнее движется (экспоненциальное замедление)
        transform.position = Vector3.Lerp(
            transform.position,
            _target.position,
            Time.deltaTime * 5f
        );

        // Если нужна строго постоянная скорость - используй MoveTowards
        transform.position = Vector3.MoveTowards(
            transform.position,
            _target.position,
            speed: 3f * Time.deltaTime
        );
    }
}
Lerp - это не «постоянная скорость»

Lerp(a, b, t * speed) создаёт плавное экспоненциальное сближение - объект быстро летит к цели, но никогда не достигает её математически. Для строго постоянной скорости используй Vector3.MoveTowards.


5. Поиск объектов на сцене

FindObjectOfType и FindWithTag

Иногда нужно найти объект, на который нет прямой ссылки. Unity предоставляет несколько способов, но у каждого есть цена:

void Start()
{
    // FindObjectOfType - перебирает ВСЕ объекты на сцене
    // Медленно на большой сцене, но допустимо в Awake/Start
    var gameManager = FindObjectOfType<GameManager>();

    // FindWithTag - быстрее (использует индекс по тегу)
    // Тег "Player" нужно заранее создать в Tags & Layers
    GameObject player = GameObject.FindWithTag("Player");

    // НИКОГДА не делай это в Update - это убивает производительность
    // void Update() { FindObjectOfType<Enemy>(); } // плохо!
}
FindObjectOfType в Update

Вызов поисковых методов каждый кадр - одна из самых частых ошибок начинающих. Найди объект один раз в Start или Awake, сохрани в поле класса и используй поле.

Поиск в иерархии

transform.Find ищет только среди прямых дочерних объектов, не рекурсивно:

void Start()
{
    // Найти прямого дочернего по имени
    Transform healthBar = transform.Find("UI/HealthBar"); // поддерживает путь с /

    // Получить дочернего по индексу
    Transform firstWeapon = transform.GetChild(0);
}

Лучший способ: ссылки через Inspector

Все методы Find - это поиск во время выполнения. Лучший подход - передать ссылку заранее через Inspector. Это нулевая стоимость в runtime и явная зависимость:

public class PlayerController : MonoBehaviour
{
    // SerializeField делает приватное поле видимым в Inspector
    // Перетащи нужный объект прямо туда - никаких поисков в runtime
    [SerializeField] private Transform _cameraTransform;
    [SerializeField] private GameManager _gameManager;
    [SerializeField] private AudioSource _footstepAudio;

    void Update()
    {
        // Прямой доступ - быстро и надёжно
        var camForward = _cameraTransform.forward;
    }
}
SerializeField vs public

Используй [SerializeField] private вместо public. Оба поля видны в Inspector, но private с атрибутом не позволяет другим скриптам случайно изменить значение напрямую из кода - это лучше для архитектуры.


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