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);
}
}