C# для разработки игр в Unity

Курс предназначен для начинающих разработчиков и охватывает ключевые аспекты программирования на C# внутри движка Unity. Вы изучите синтаксис языка, работу с API Unity, физику и основы архитектуры игровых скриптов.

1. Введение в C# и структура скриптов в Unity

Введение в C# и структура скриптов в Unity

Добро пожаловать в курс «C# для разработки игр в Unity». Это первая статья, и она станет фундаментом для всего вашего дальнейшего пути в создании игр. Мы не просто будем учить язык программирования; мы будем учиться «думать» на языке игрового движка.

Многие новички считают, что создание игр — это магия. На самом деле, это логика, упакованная в структуру. Unity предоставляет вам тело игры (графику, физику, звук), а C# — это её мозг. Без скриптов ваши персонажи будут просто статичными манекенами, а мир — безжизненной декорацией.

В этой статье мы разберем, как C# интегрирован в Unity, из чего состоит стандартный скрипт и как работает жизненный цикл игрового объекта.

Почему именно C#?

Unity использует C# (читается как «си-шарп») в качестве основного языка сценариев. Это объектно-ориентированный язык программирования, разработанный компанией Microsoft. Для нас, как для разработчиков игр, важны следующие его преимущества:

* Строгая типизация: Язык помогает избегать глупых ошибок, требуя четкого определения типов данных. * Управление памятью: C# берет на себя большую часть работы по очистке оперативной памяти (Garbage Collection), позволяя вам сосредоточиться на геймплее. * Огромное сообщество: Если вы столкнулись с проблемой, скорее всего, кто-то уже решил её и выложил решение в интернет.

Архитектура Unity: GameObject и Компоненты

Прежде чем писать код, нужно понять философию Unity. Вся сцена в Unity состоит из объектов, называемых GameObjects. Сам по себе GameObject — это просто пустой контейнер, точка в пространстве. Чтобы он стал камерой, светом, персонажем или стеной, к нему нужно добавить Компоненты.

Скрипт на C#, который вы пишете, — это тоже компонент. Когда вы создаете скрипт и «вешаете» его на объект, вы создаете новый тип поведения для этого объекта.

!Схема, показывающая, что GameObject является контейнером для различных компонентов, включая скрипты.

Создание первого скрипта

В интерфейсе Unity создание скрипта происходит через окно Project. Вы нажимаете правую кнопку мыши, выбираете Create -> C# Script и даете ему имя.

> Важное правило: Имя файла скрипта должно в точности совпадать с именем класса внутри этого файла. Если вы назвали файл PlayerController, то и класс внутри должен называться PlayerController. Если они отличаются, Unity не сможет подключить этот скрипт к объекту.

Давайте рассмотрим структуру стандартного скрипта, который Unity генерирует автоматически.

Разберем этот код построчно. Понимание каждой строки критически важно для дальнейшей работы.

1. Пространства имен (Namespaces)

В самом верху файла мы видим строки, начинающиеся с ключевого слова using:

Это подключение библиотек. Представьте, что вы столяр. Чтобы работать, вам нужен ящик с инструментами. using UnityEngine; говорит программе: «Открой ящик с инструментами Unity». Это дает нам доступ к таким классам, как GameObject, Transform, Debug и многим другим. Без этой строки компьютер не поймет команды, специфичные для движка.

2. Объявление класса

* public: Означает, что этот класс доступен для других частей программы (и для редактора Unity). * class MyFirstScript: Мы объявляем чертеж (класс) с именем MyFirstScript. * : MonoBehaviour: Это самая важная часть. Двоеточие означает наследование. Мы говорим, что наш скрипт — это не просто какой-то код, а наследник MonoBehaviour. Все скрипты, которые должны прикрепляться к объектам в Unity, обязаны наследоваться от MonoBehaviour. Именно это превращает обычный класс C# в компонент Unity.

3. Тело класса

Всё, что находится между фигурными скобками { и }, является телом класса. Здесь живут переменные (данные) и методы (действия).

Методы жизненного цикла: Start и Update

Unity работает по принципу игрового цикла (Game Loop). В отличие от обычных программ, которые выполняют задачу и закрываются, игра работает в бесконечном цикле, отрисовывая кадры десятки раз в секунду. MonoBehaviour предоставляет нам специальные методы, которые Unity вызывает автоматически в определенные моменты времени.

!Визуализация жизненного цикла скрипта: Start выполняется один раз при инициализации, а Update повторяется каждый кадр.

Метод Start()

Метод Start вызывается один раз в момент, когда объект с этим скриптом становится активным в сцене, но перед тем, как будет отрисован первый кадр.

Для чего используется: * Настройка начального количества жизней. * Получение ссылок на другие компоненты. * Установка начальной позиции.

Метод Update()

Метод Update вызывается каждый кадр. Если ваша игра работает с частотой 60 кадров в секунду (60 FPS), то код внутри Update выполнится 60 раз за одну секунду.

Для чего используется: * Считывание ввода игрока (нажатие клавиш). * Перемещение персонажа. * Таймеры и кулдауны.

> Важно понимать: время между вызовами Update не является константой. Оно зависит от того, насколько быстро компьютер может обработать текущий кадр. Это время называется DeltaTime.

Если мы хотим выразить зависимость частоты кадров математически, мы можем использовать простую формулу:

где — количество кадров в секунду (Frames Per Second), а — время, затраченное на обработку одного кадра (Delta Time). Чем меньше времени тратится на кадр, тем выше FPS.

Консоль и отладка

Как узнать, что ваш код работает, если визуально ничего не происходит? Для этого используется Консоль. В C# для Unity есть команда, которая позволяет выводить текстовые сообщения в окно Console редактора.

Давайте напишем наш первый рабочий код. Изменим стандартный шаблон следующим образом:

Если вы создадите такой скрипт, прикрепите его к любому объекту на сцене и нажмете кнопку Play, в консоли появится сообщение «Я родился! Этот код сработал один раз.».

Синтаксис: на что обратить внимание

C# — язык строгий к пунктуации. Новички часто совершают одни и те же ошибки:

  • Точка с запятой (;): Каждая команда должна заканчиваться точкой с запятой. Это как точка в конце предложения в русском языке.
  • Правильно*: Debug.Log("Test"); Ошибка*: Debug.Log("Test")
  • Фигурные скобки {}: Они обозначают блоки кода. У каждого открывающего { должен быть закрывающий }. Если вы потеряете скобку, скрипт не скомпилируется.
  • Регистр букв: C# чувствителен к регистру. Start, start и START — это три разных слова для компьютера. Методы Unity всегда пишутся с большой буквы (Start, Update), а ключевые слова языка — с маленькой (void, class, public).
  • Заключение

    Мы рассмотрели структуру базового скрипта в Unity. Теперь вы знаете, что скрипт — это компонент, наследуемый от MonoBehaviour, который имеет доступ к инструментам UnityEngine. Вы также узнали разницу между Start (инициализация) и Update (цикл) и научились выводить сообщения в консоль.

    В следующих статьях мы начнем наполнять эти методы реальной логикой: научимся создавать переменные, управлять ими и заставим объекты двигаться.

    2. Жизненный цикл MonoBehaviour и манипуляция объектами GameObject

    Жизненный цикл MonoBehaviour и манипуляция объектами GameObject

    В предыдущей статье мы познакомились с основами скриптов в Unity, узнали, что такое MonoBehaviour, и написали свой первый код, выводящий сообщение в консоль. Мы выяснили, что методы Start и Update вызываются движком автоматически. Но это лишь верхушка айсберга.

    Чтобы создавать сложные игровые механики — от стрельбы и передвижения до спавна врагов и управления инвентарем — нам нужно глубже понять, когда именно Unity вызывает методы и как мы можем управлять объектами на сцене через код. Сегодня мы превратим наши скрипты из пассивных наблюдателей в активных кукловодов.

    Расширенный жизненный цикл скрипта

    Жизнь скрипта не ограничивается методами Start и Update. Unity предоставляет целую цепочку событий, которые происходят в строго определенном порядке. Понимание этого порядка (Execution Order) критически важно, чтобы избежать ошибок, когда один скрипт пытается обратиться к данным другого, который еще не успел инициализироваться.

    !Схема порядка выполнения основных методов жизненного цикла MonoBehaviour.

    Этап инициализации: Awake, OnEnable, Start

    Часто новички пишут весь код настройки в Start. Однако иногда переменные нужно настроить до того, как игра начнется. Для этого существуют Awake и OnEnable.

  • Awake(): Вызывается самым первым, как только объект создан или сцена загружена. Этот метод срабатывает, даже если сам скрипт (компонент) выключен (галочка снята), но объект активен.
  • Использование*: Инициализация собственных переменных, настройка ссылок на компоненты внутри того же объекта (Singleton паттерн часто живет здесь).
  • OnEnable(): Вызывается каждый раз, когда объект или скрипт становится активным.
  • Использование*: Подписка на события (Events), сброс параметров при повторном включении врага.
  • Start(): Вызывается перед первым кадром, если скрипт активен.
  • Использование: Получение ссылок на другие* объекты, сложная логика, требующая, чтобы все остальные объекты уже прошли стадию Awake.

    > Золотое правило инициализации: Настраивайте себя в Awake, а связи с другими — в Start. Это гарантирует, что когда вы обратитесь к соседу в Start, он уже будет готов (так как его Awake точно прошел).

    Этап обновлений: Update, FixedUpdate, LateUpdate

    Мы уже знаем про Update, который зависит от частоты кадров (FPS). Но для физики и камеры нужны другие подходы.

  • FixedUpdate(): Вызывается с фиксированным интервалом времени (по умолчанию 0.02 секунды, то есть 50 раз в секунду). Этот интервал не зависит от FPS.
  • Использование*: Любые манипуляции с физикой (Rigidbody). Если двигать физическое тело в обычном Update, оно будет дергаться или проваливаться сквозь стены при лагах.
  • Update(): Вызывается каждый кадр.
  • Использование*: Ввод игрока (Input), таймеры, простая логика перемещения без физики.
  • LateUpdate(): Вызывается каждый кадр, но строго после того, как отработали все Update во всех скриптах.
  • Использование*: Камера, следующая за игроком. Мы должны быть уверены, что игрок уже закончил движение в Update, прежде чем двигать камеру к его новой позиции, иначе камера будет дрожать.

    Этап завершения: OnDisable, OnDestroy

    Когда объект уходит со сцены, нам нужно прибраться за собой.

  • OnDisable(): Вызывается, когда объект выключается.
  • Использование*: Отписка от событий, остановка звуков.
  • OnDestroy(): Вызывается перед полным удалением объекта или при выгрузке сцены.
  • Использование*: Очистка памяти, сохранение данных перед смертью персонажа.

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

    GameObject — это контейнер. Чтобы скрипт мог управлять, например, физикой объекта, ему нужно получить доступ к компоненту Rigidbody, который лежит в том же контейнере.

    Для этого используется метод GetComponent<Тип>().

    Важно о производительности: Метод GetComponent — «дорогой» для процессора. Unity приходится перебирать список компонентов, чтобы найти нужный. Никогда не вызывайте GetComponent внутри методов Update или FixedUpdate. Лучшая практика — найти компонент один раз в Start или Awake, сохранить его в переменную (как в примере выше) и использовать эту переменную. Этот процесс называется кэшированием компонентов.

    Манипуляция трансформацией (Transform)

    Каждый GameObject обязательно имеет компонент Transform. Он отвечает за позицию, поворот и масштаб. Поскольку он есть всегда, Unity предоставляет к нему быстрый доступ через свойство transform (с маленькой буквы).

    Позиция и Векторы

    Позиция в 3D-пространстве описывается с помощью структуры Vector3, которая содержит три координаты: , , .

    Чтобы переместить объект, мы можем изменить его координаты. Допустим, мы хотим двигать объект вперед.

    Здесь мы видим использование Time.deltaTime. Давайте разберем математику этого процесса. Если мы просто будем прибавлять значение к позиции каждый кадр, скорость объекта будет зависеть от FPS. На мощном компьютере (120 FPS) объект улетит в два раза дальше, чем на слабом (60 FPS).

    Формула корректного перемещения выглядит так:

    Где — новая позиция объекта, — текущая позиция, — вектор скорости (направление * величина), а — время, прошедшее с последнего кадра (Time.deltaTime).

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

    Вращение и Масштаб

    * Вращение (Rotation): Хранится в кватернионах (Quaternion), сложной математической структуре, избегающей проблем с углами Эйлера. Однако для простых операций мы можем использовать методы transform.Rotate(Vector3) или обращаться к углам Эйлера через transform.eulerAngles. * Масштаб (Scale): Доступен через transform.localScale. Изменение этого вектора позволяет сплющивать или растягивать объекты.

    Управление объектами: Создание и Уничтожение

    В играх объекты постоянно появляются (пули, враги, монеты) и исчезают. Unity предоставляет для этого два мощных метода.

    Instantiate (Создание)

    Метод Instantiate создает копию любого объекта. Чаще всего мы копируем Префабы (заранее настроенные шаблоны объектов).

    Destroy (Уничтожение)

    Метод Destroy удаляет объект из сцены и очищает память.

    > Обратите внимание: gameObject (с маленькой буквы) — это ссылка на сам объект, к которому прикреплен скрипт. this — это ссылка на текущий экземпляр класса скрипта (компонент).

    Активация и Деактивация

    Иногда объект не нужно удалять, а нужно просто скрыть (например, меню паузы или инвентарь). Для этого используется метод SetActive.

    Проверить, активен ли объект в данный момент, можно через свойство activeSelf (локальная активность) или activeInHierarchy (активность с учетом родителей).

    Поиск объектов (и почему этого стоит избегать)

    Иногда скрипту нужно найти другой объект на сцене, не имея прямой ссылки. Для этого существует GameObject.Find.

    Этот метод ищет объект по имени. Также есть FindWithTag, который ищет по тегу.

    Предостережение: Операция поиска по строке очень медленная. Unity перебирает все объекты в сцене, сравнивая их имена. Если вы будете делать это в Update (каждый кадр), ваша игра начнет тормозить. Используйте поиск только в крайних случаях и только в методах инициализации (Start/Awake), сохраняя результат в переменную.

    Практический пример: Собираемый предмет

    Давайте объединим полученные знания и напишем скрипт для монетки, которая вращается, а при сборе исчезает.

    В этом небольшом скрипте мы использовали: * Update для анимации вращения. * Vector3 и Time.deltaTime для плавности. * Событие физики OnTriggerEnter (о нем подробнее в следующих статьях). * Destroy для удаления объекта.

    Заключение

    Теперь вы понимаете, как бьется сердце скрипта Unity. Вы знаете, что Awake готовит объект, Start налаживает связи, Update обрабатывает логику, а FixedUpdate считает физику. Вы научились манипулировать пространством через Transform, создавать новые миры через Instantiate и разрушать их через Destroy.

    Эти инструменты — база для любого взаимодействия в игре. В следующей статье мы углубимся в тему, которую затронули лишь вскользь — Физика и столкновения, чтобы наши объекты обрели вес и плотность.

    3. Работа с физикой, коллизиями и системой ввода Input System

    Работа с физикой, коллизиями и системой ввода Input System

    В предыдущих статьях мы научились создавать скрипты и перемещать объекты, изменяя их координаты через компонент Transform. Это отличный способ для движения платформ или камер, но когда дело доходит до персонажей, мячей или падающих ящиков, прямое изменение координат выглядит неестественно. Объекты проходят сквозь стены, не имеют инерции и не падают вниз.

    Чтобы оживить мир, нам нужна физика. Unity использует мощный физический движок (PhysX для 3D и Box2D для 2D), который берет на себя сложные математические расчеты столкновений и гравитации. В этой статье мы разберем три кита интерактивности: Rigidbody (физическое тело), Colliders (формы столкновений) и Input (управление).

    Rigidbody: Добавляем вес и гравитацию

    Чтобы объект начал подчиняться законам физики Ньютона, к нему нужно добавить компонент Rigidbody (Твердое тело). Как только вы это сделаете, объект обретет массу и начнет падать под действием гравитации.

    Основные свойства Rigidbody

    * Mass (Масса): Вес объекта в килограммах. Тяжелые объекты сложнее сдвинуть с места, но падают они с той же скоростью, что и легкие (если не учитывать сопротивление воздуха). * Drag (Сопротивление): Сопротивление воздуха. Чем выше значение, тем быстрее объект замедляется при движении. * Use Gravity: Если галочка стоит, объект будет падать. * Is Kinematic: Очень важная настройка. Если включить эту опцию, объект перестанет реагировать на физические силы и гравитацию, но сам сможет толкать другие объекты. Это полезно для движущихся платформ: они должны быть твердыми, но не должны падать.

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

    Управлять физическим объектом через transform.position — плохая идея. Это телепортация, которая ломает физические расчеты. Вместо этого мы должны прикладывать к объекту силы.

    Вспомним второй закон Ньютона:

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

    > Важное напоминание: Постоянные физические силы (например, двигатель машины) нужно применять в методе FixedUpdate, так как физический движок работает с фиксированным тактом времени. Однократные импульсы (прыжок) допустимо вызывать в Update при нажатии кнопки.

    Коллизии и Триггеры

    Чтобы объекты не проваливались сквозь пол, им нужны Коллайдеры (Colliders). Это невидимые сетки, определяющие физическую форму объекта. Самые простые и эффективные коллайдеры — примитивы: BoxCollider (куб), SphereCollider (сфера), CapsuleCollider (капсула).

    !Сравнение твердого столкновения и триггерной зоны.

    Два режима работы коллайдера

  • Collision (Столкновение): Стандартный режим. Объекты ударяются, отскакивают, трутся друг о друга. Используется для стен, пола, твердых предметов.
  • Trigger (Триггер): Если в настройках коллайдера поставить галочку Is Trigger, он станет проницаемым. Объекты будут проходить сквозь него, но Unity сообщит нам об этом событии. Используется для зон сбора монет, автоматических дверей, чекпоинтов.
  • Обработка событий в коде

    Unity вызывает специальные методы, когда происходит взаимодействие коллайдеров. Для их работы хотя бы один из объектов должен иметь компонент Rigidbody.

    #### Для твердых столкновений (Collision):

    #### Для триггеров (Trigger):

    Обратите внимание на разницу в аргументах: OnCollisionEnter получает объект типа Collision (содержит информацию о точке удара, силе), а OnTriggerEnter получает просто Collider (ссылку на того, кто вошел).

    Система ввода (Input System)

    Чтобы игрок мог взаимодействовать с физикой, нам нужно считывать нажатия клавиш, движения мыши или геймпада. В Unity существуют две системы ввода: классический Input Manager и новый пакет Input System. В рамках базового курса мы сосредоточимся на классическом классе Input, так как он проще для понимания логики C#.

    Считывание клавиатуры

    Есть три состояния нажатия кнопки:

  • Input.GetKeyDown(KeyCode.Space) — возвращает true только в первый кадр нажатия. Идеально для прыжка, выстрела, открытия меню.
  • Input.GetKey(KeyCode.Space) — возвращает true все время, пока кнопка удерживается. Подходит для автоматической стрельбы или спринта.
  • Input.GetKeyUp(KeyCode.Space) — срабатывает один раз при отпускании кнопки.
  • Оси управления (Axes)

    Вместо того чтобы проверять конкретные клавиши (W, A, S, D), лучше использовать Оси. Это абстракция, которая позволяет управлять персонажем и с клавиатуры, и с геймпада без переписывания кода.

    Метод Input.GetAxis("Name") возвращает число от -1.0 до 1.0.

    * Horizontal: A / Стрелка влево = -1.0; D / Стрелка вправо = 1.0; Ничего не нажато = 0. * Vertical: S / Стрелка вниз = -1.0; W / Стрелка вверх = 1.0.

    Особенность GetAxis в том, что значение меняется плавно (симуляция аналогового стика). Если вам нужны мгновенные значения (-1, 0, 1), используйте Input.GetAxisRaw.

    Практика: Физическое движение персонажа

    Давайте объединим знания и напишем скрипт для движения шара физикой.

    Raycasting (Бросание лучей)

    Еще одна важная часть физики — Рейкастинг. Это способ «посмотреть» или «выстрелить» невидимым лучом из точки А в направлении Б и узнать, во что он попал.

    Это используется для стрельбы в шутерах, проверки земли под ногами или взаимодействия с предметами (взгляд на кнопку).

    Здесь используется ключевое слово out. Оно означает, что метод Raycast не только возвращает true/false (попал/не попал), но и записывает подробные данные в переменную hit.

    Заключение

    Физика в Unity — это сочетание компонентов (Rigidbody, Collider) и кода (AddForce, OnCollisionEnter). Использование физики делает игровой процесс непредсказуемым и живым, но требует аккуратности: не забывайте использовать FixedUpdate для постоянных сил и оптимизировать коллайдеры (использовать простые формы вместо сложных мешей).

    В следующей статье мы отойдем от механик и займемся визуальной частью и интерфейсом — созданием UI, чтобы игрок мог видеть счет, здоровье и меню.

    4. Игровая логика: префабы, корутины и создание пользовательского интерфейса

    Игровая логика: префабы, корутины и создание пользовательского интерфейса

    Мы уже прошли большой путь: разобрались со структурой скриптов, научились управлять объектами и даже внедрили физику. Но пока наши «игры» больше похожи на физические песочницы. В них нет структуры, нет таймеров, нет возможности проиграть или выиграть, и, самое главное, игрок не видит никакой информации о состоянии игры, кроме того, что происходит на сцене.

    В этой статье мы превратим набор механик в полноценный игровой цикл. Мы разберем три фундаментальные концепции:

  • Префабы (Prefabs): Как создавать шаблоны объектов для их массового использования.
  • Корутины (Coroutines): Как выполнять действия с задержкой во времени, не замораживая игру.
  • UI (User Interface): Как создать интерфейс и связать его с кодом.
  • Префабы: Клонирование и шаблонизация

    Представьте, что вы делаете стратегию, и вам нужно создать 100 одинаковых солдат. Если вы будете создавать каждого солдата вручную на сцене, а потом решите изменить им цвет шлема, вам придется менять его 100 раз. Это неэффективно.

    В Unity для этого существуют Префабы. Префаб — это сохраненная копия объекта, хранящаяся в файлах проекта, которая служит шаблоном (чертежом).

    !Префаб выступает в роли чертежа, по которому создаются экземпляры объектов на сцене.

    Как работает связь Префаб-Экземпляр

    Когда вы перетаскиваете объект из окна иерархии (Hierarchy) в окно проекта (Project), он становится префабом (иконка куба становится синей). Теперь вы можете:

    * Перетаскивать этот префаб обратно на сцену сколько угодно раз, создавая Экземпляры (Instances). * Изменить настройки самого префаба, и эти изменения мгновенно применятся ко всем его экземплярам на всех сценах.

    Спавн префабов через код

    В статье про GameObject мы уже касались метода Instantiate. Теперь рассмотрим его в контексте игровой логики. Допустим, мы хотим создать генератор врагов.

    Главная сила префабов в том, что скрипту EnemySpawner абсолютно всё равно, как выглядит враг. Это может быть зомби, танк или космический корабль. Главное, что у нас есть ссылка на шаблон.

    Корутины: Управление временем

    До сих пор весь наш код выполнялся мгновенно. Метод Update срабатывает каждый кадр. Но что, если мы хотим сделать паузу? Например: «Подождать 2 секунды перед возрождением игрока» или «Плавно менять цвет в течение 5 секунд».

    Если вы попытаетесь использовать стандартные методы C# вроде Thread.Sleep, вы заморозите всю игру. Картинка застынет, потому что Unity выполняет всё в одном потоке.

    Для решения этой задачи Unity использует Корутины (Coroutines).

    Что такое Корутина?

    Корутина — это специальный метод, выполнение которого можно приостановить (поставить на паузу) и продолжить с того же места в следующем кадре или через заданное время.

    !Сравнение выполнения обычного кода и корутины с ожиданием.

    Синтаксис Корутины

    Для создания корутины нужно соблюдать три правила:

  • Тип возвращаемого значения должен быть IEnumerator (требует подключения using System.Collections;).
  • В теле метода должна быть хотя бы одна инструкция yield return.
  • Запускается корутина методом StartCoroutine.
  • Рассмотрим пример спавнера, который создает врагов каждые 3 секунды.

    Ключевые команды yield

    * yield return null; — подождать до следующего кадра. * yield return new WaitForSeconds(float t); — подождать секунд (зависит от игрового времени, пауза в игре остановит таймер). * yield return new WaitForSecondsRealtime(float t); — подождать секунд реального времени (игнорирует паузу).

    Математически время задержки можно описать так:

    где — время продолжения выполнения, — время вызова yield, а — указанная задержка. Unity автоматически проверяет каждый кадр, наступило ли время .

    Пользовательский интерфейс (UI)

    Игра без интерфейса слепа. Игроку нужно знать здоровье, количество патронов и счет. В Unity за отрисовку интерфейса отвечает система UI Canvas.

    Canvas (Холст)

    Все элементы интерфейса (кнопки, текст, картинки) должны находиться внутри объекта Canvas. Если вы создадите кнопку (Create -> UI -> Button), Unity автоматически создаст Canvas, если его еще нет.

    Canvas — это прозрачная пленка, натянутая поверх камеры. В отличие от игровых объектов, которые живут в метрах, элементы UI живут в пикселях экрана (или относительных единицах).

    RectTransform

    Вместо обычного Transform, элементы UI используют RectTransform. Это более сложный компонент, который позволяет настраивать: * Anchors (Якоря): К какой части экрана «приклеен» элемент (например, всегда в правом верхнем углу, независимо от разрешения экрана). * Pivot (Опорная точка): Точка, вокруг которой происходит вращение и масштабирование элемента.

    Взаимодействие кода и UI

    Чтобы управлять текстом или кнопками из скрипта, нам нужно подключить пространство имен UnityEngine.UI.

    > Примечание: В современных версиях Unity часто используется пакет TextMeshPro (TMP) вместо стандартного Text, так как он обеспечивает четкость шрифтов при любом масштабе. Однако логика работы через код у них очень похожа. В примерах ниже мы будем использовать стандартный UI для простоты.

    Давайте создадим скрипт, который считает клики по кнопке и выводит их на экран.

  • Создайте в Unity Canvas.
  • Внутри Canvas создайте Text (назовите его ScoreText) и Button.
  • Создайте скрипт ClickerGame и повесьте его на любой объект (обычно создают пустой объект GameManager).
  • Подключение кнопки (События)

    Написание метода AddScore недостаточно. Кнопка на экране не знает, что она должна вызывать этот метод.

    Чтобы связать их:

  • Выберите объект Button в иерархии.
  • Найдите компонент Button в инспекторе.
  • Внизу есть список On Click (). Нажмите +.
  • В поле объекта перетащите тот объект, на котором висит ваш скрипт ClickerGame.
  • В выпадающем меню выберите ClickerGame -> AddScore.
  • Теперь при нажатии на кнопку Unity вызовет ваш метод.

    Практический пример: Таймер обратного отсчета

    Давайте объединим корутины и UI, чтобы создать таймер, который отсчитывает время до конца уровня.

    В этом примере мы видим, как линейная логика корутины (while -> wait -> action) позволяет легко читать код, который выполняется во времени. Если бы мы делали это в Update, нам пришлось бы создавать дополнительные переменные для хранения текущего времени и постоянно вычитать из них Time.deltaTime.

    Формула для расчета оставшегося времени в Update выглядела бы так:

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

    Заключение

    Сегодня мы сделали огромный шаг от простых скриптов к полноценной архитектуре игры.

    * Префабы позволяют нам наполнять мир контентом, не дублируя работу. * Корутины дают контроль над временем и последовательностью событий. * UI создает мост между кодом игры и игроком.

    Теперь у вас есть все инструменты для создания простого игрового прототипа: вы можете создать персонажа, заставить его собирать предметы (префабы), вести счет (UI) и ограничивать игру по времени (корутины).

    В следующей статье мы углубимся в более сложные аспекты программирования: работу с данными, сохранение прогресса и переходы между сценами.

    5. Взаимодействие между скриптами, события и основы оптимизации

    Взаимодействие между скриптами, события и основы оптимизации

    Мы прошли долгий путь: от создания первого скрипта до управления физикой и интерфейсом. В предыдущих статьях наши объекты существовали немного обособленно или связывались друг с другом напрямую через публичные переменные. Например, чтобы враг нанес урон игроку, мы могли бы написать что-то вроде player.TakeDamage().

    На этапе прототипа это работает отлично. Но по мере роста проекта прямые связи превращают код в «спагетти». Если вы удалите скрипт игрока, сломаются скрипты врагов, интерфейса, звука и камеры.

    В этой статье мы научимся писать чистый, модульный код, используя События (Events) и паттерн Синглтон (Singleton). Также мы затронем важную тему Оптимизации, чтобы ваша игра работала плавно даже на слабых устройствах.

    Проблема сильной связности (Tight Coupling)

    Представьте, что у вас есть скрипт PlayerHealth. Когда здоровье падает до нуля, должно произойти следующее:

  • Проиграться анимация смерти.
  • Обновиться UI (показать экран «Game Over»).
  • Остановиться музыка.
  • Сохраниться статистика.
  • При «лобовом» подходе ваш скрипт здоровья будет выглядеть так:

    Это сильная связность. Скрипт здоровья знает слишком много о вещах, которые его не касаются. Чтобы добавить новую реакцию на смерть (например, создать эффект взрыва), вам придется лезть в код PlayerHealth и добавлять туда новую переменную.

    !Сравнение сильной связности (прямые ссылки) и слабой связности (события).

    Решение: События (Events) и Делегаты

    Лучший способ развязать этот узел — использовать Паттерн Наблюдатель (Observer Pattern). Представьте радиостанцию. Станция просто вещает сигнал. Ей всё равно, кто её слушает: один человек, миллион или никто.

    В C# для этого используются Action из пространства имен System.

    Создание события

    Перепишем наш пример. Теперь PlayerHealth просто кричит: «Я умер!», а остальные сами решают, что с этим делать.

    Подписка на событие

    Теперь пойдем в скрипт UIManager и научим его слушать это радио.

    Важнейшее правило: Всегда отписывайтесь от событий в OnDisable или OnDestroy. Если вы уничтожите объект UIManager, но не отпишете его от события, PlayerHealth попытается вызвать метод у несуществующего объекта, что приведет к ошибкам или утечкам памяти.

    Теперь, если вы захотите добавить звук смерти, вы просто создадите скрипт SoundManager и подпишетесь на то же событие PlayerHealth.OnPlayerDied. Сам скрипт игрока менять не придется.

    Паттерн Синглтон (Singleton)

    Иногда нам нужен объект, который существует в единственном экземпляре и доступен из любой точки кода. Обычно это GameManager, AudioManager или InventorySystem.

    Синглтон — это класс, который позволяет обращаться к себе через статическую переменную Instance.

    Теперь из любого скрипта (например, из монетки) вы можете написать:

    Вам не нужно искать этот объект через Find или перетаскивать ссылки в инспекторе.

    Основы оптимизации

    Unity делает много работы за вас, но она не может исправить плохую архитектуру. Оптимизация — это искусство делать так, чтобы игра тратила меньше ресурсов процессора (CPU) и оперативной памяти (RAM).

    1. Кэширование компонентов

    Мы уже упоминали это, но повторим. Метод GetComponent требует ресурсов. Метод Find требует огромных ресурсов.

    Плохо (каждый кадр ищем компонент):

    Хорошо (нашли один раз и запомнили):

    Если представить это математически, то стоимость операции поиска в цикле можно описать формулой:

    где — общая нагрузка на процессор за секунду, — количество кадров (FPS), а — стоимость одной операции поиска компонента. При 60 FPS вы заставляете компьютер выполнять тяжелую работу 60 раз в секунду без причины.

    2. Сборщик мусора (Garbage Collection) и строки

    C# — язык с автоматическим управлением памятью. Когда вы создаете переменную, память выделяется. Когда переменная больше не нужна, специальная программа (Garbage Collector или GC) очищает память. Работа GC вызывает микро-зависания (фризы).

    Самый частый источник мусора — создание строк в Update.

    Как исправить: Обновляйте текст только тогда, когда значение действительно изменилось (используя События!), а не каждый кадр.

    3. Object Pooling (Пул объектов)

    Представьте пулемет. Создавать (Instantiate) и уничтожать (Destroy) пулю 10 раз в секунду — очень дорого для процессора. Это вызывает фрагментацию памяти и нагружает GC.

    Вместо этого используется Пул объектов:

  • В начале игры создаем 50 пуль и выключаем их (SetActive(false)).
  • При выстреле находим выключенную пулю, ставим её в дуло и включаем (SetActive(true)).
  • Когда пуля попадает в стену, мы не уничтожаем её, а просто выключаем, возвращая в «бассейн».
  • !Принцип работы Object Pooling: повторное использование вместо создания и удаления.

    Порядок выполнения скриптов (Script Execution Order)

    Иногда возникают ошибки, когда ScriptA в методе Start пытается обратиться к ScriptB, но ScriptB еще не успел инициализироваться.

    По умолчанию Unity не гарантирует, чей Start выполнится раньше.

    Чтобы избежать этого:

  • Используйте Awake для внутренней инициализации (настройки своих переменных).
  • Используйте Start для внешней инициализации (обращения к другим скриптам).
  • В крайнем случае, можно настроить порядок принудительно в Project Settings -> Script Execution Order.
  • Заключение

    Сегодня мы перешли от написания кода к архитектуре.

    * События помогают сделать систему гибкой: объекты общаются, не зная друг о друге. * Синглтоны дают удобный доступ к глобальным менеджерам. * Оптимизация (кэширование, пул объектов) гарантирует, что ваша игра будет работать плавно.

    Эти знания отличают новичка от профессионала. В следующем модуле мы займемся сохранением данных, чтобы прогресс игрока не терялся после выхода из игры.