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(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
Это важное различие: дочерний объект имеет две системы координат.
transform.position- координаты в мировой системе: (0, 0, 0) - это центр мира.transform.localPosition- координаты относительно родителя: (0, 0, 0) - это центр родителя.
// Пример: дочерний объект на позиции (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;
}
В MonoBehaviour свойства gameObject и transform - это сокращения, не вызовы GetComponent. Используй их напрямую, без кэширования.
4. Transform: позиция, вращение, масштаб
Система координат Unity
Unity использует левостороннюю систему координат:
- X - вправо
- Y - вверх
- Z - вперёд
Это важно знать при работе с направлениями: 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(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>(); } // плохо!
}
Вызов поисковых методов каждый кадр - одна из самых частых ошибок начинающих. Найди объект один раз в 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] private вместо public. Оба поля видны в Inspector, но private с атрибутом не позволяет другим скриптам случайно изменить значение напрямую из кода - это лучше для архитектуры.