Базовые концепции Unity для программистов
Привет, Хабр! При проработке темы Unity мы нашли интересный блог, возможно, заслуживающий вашего более пристального внимания. Предлагаем вам перевод статьи о базовых концепциях Unity, также опубликованный на портале Medium
Если вы обладаете опытом программирования и пытаетесь вкатиться в разработку игр, порой бывает непросто найти такие учебные материалы, в которых в достаточной степени объясняется необходимый контекст. Вероятно, придется выбирать между материалами, в одних из которых описана парадигма ООП, в других — язык C# и концепции Unity, либо сразу начинать с продвинутых руководств; в последнем случае придется самостоятельно дедуктивно выводить базовые концепции.
Поэтому, чтобы отчасти заполнить этот пробел, я решил написать серию статей Unity for Software Engineers. Это первая из них. Статья рассчитана на читателей, имеющих представление о программировании и программной архитектуре, в особенности на тех, кому близок тот же подход к обучению, что и мне: начинать с основ и постепенно идти вверх.
Я начал путь в программировании около 17 лет назад, открыв для себя Game Maker. Многие часы потратил на самостоятельное программирование маленьких игр и инструментов, в процессе всерьез увлекшись программированием.
Однако с тех пор ландшафт игровой разработки серьезно изменился. Когда я попробовал Unity после долгой паузы с игровой разработкой, мне больше всего хотелось понять базовые концепции: из каких кирпичиков строится игра в Unity? Что нужно знать о принципах представления этих кирпичиков в памяти или на диске? Как организован идиоматический код? Какие паттерны предпочтительны?
Сцена
Сцена – это самый крупный блок, описывающий организацию объектов в памяти. В сценах содержатся объекты, из которых состоит ваша игра.
В базовом случае сцена представляет отдельно взятый уровень вашей игры, где в любой конкретный момент времени загружена одна сцена. В более продвинутых сценариях у вас одновременно могут быть активны две или более сцен. В таком случае сцены можно дополнительно загружать в память или выгружать. Особенно удобно загружать во время геймплея несколько сцен, когда строишь крупномасштабный мир; когда держишь отдаленные области игрового мира на диске, а не в памяти, проще выдерживать стоящие перед вами требования по производительности.
Редактор сцен Unity, в котором загружена задаваемая по умолчанию пустая сцена в режиме 3D. В пустых сценах Unity3D по умолчанию содержатся объекты Main Camera (Главная Камера) и Directional light (Направленный свет).
Пример сцены в редакторе Unity; здесь выделено несколько объектов. Такое представление сцены можно использовать для редактирования уровней в игре.
Каждый игровой объект в Unity должен находиться в сцене.
Игровые объекты
Игровой Объект (в коде GameObject ) – один из базовых кирпичиков, из которых строится игра.
В виде игровых объектов можно представлять как физические сущности, наблюдаемые в игре (напр., персонаж, грунт, дерево, ландшафт, свет, оружие, пуля, взрыв) так и метафизические (напр., менеджер снаряжения, контроллер мультиплеерного режима, т.д.).
У каждого игрового объекта есть значения положения и поворота. Для метафизических объектов они не имеют значения.
Допускается вложение игровых объектов друг в друга. Положение и поворот каждого объекта отсчитывается относительно его родительского объекта. Объект, расположенный непосредственно в сцене, позиционируется относительно «мировых координат».
Группа объектов, совместно вложенных в сцене и объединенных в пустом объекте “Interior_Props”, сделано в целях структурирования
Есть много причин, по которым вам может понадобиться вложение объектов. Например, вы можете решить, что со структурной точки зрения будет целесообразно поместить всю вашу «окружающую среду» (например, отдельные элементы, из которых состоит город или деревня) в пустой родительский объект. Таким образом, окружающую среду можно «компактифицировать» и вместе со всем представлением данной сцены перенести куда требуется при разработке игры.
Группа объектов, вложенных в объект «игрок». Здесь мы видим оружие игрока, его аватарку и различные элементы пользовательского интерфейса, отображаемые вокруг игрока
Вложение объектов может быть значимым и с функциональной точки зрения. Например, в объекте «Car» может содержаться код, управляющий как скоростью, так и поворотами машины в целом. Но у него могут быть отдельные дочерние объекты, представляющие четыре колеса (причем, все колеса будут крутиться независимо), корпус машины, окна, т.д. При перемещении родительского объекта «Car» будут двигаться и все его дочерние объекты, сохраняющие ориентацию относительно родительского объекта и относительно друг друга. Например, мы можем запланировать, что персонаж открывает дверцу, и это действие касается именно дверцы, а не всей машины.
Компоненты (и моноповедения)
Объект «Warrior» с предыдущего скриншота показан над окном «Инспектор» в интерфейсе Unity. Каждый из проиллюстрированных разделов (напр., Animator, Rigidbody, Collider) – это компоненты, слагающие этот объект
Каждый игровой объект состоит из компонентов.
Компонент реализует четко определенный набор поведений, необходимых, чтобы мог выполниться GameObject . Все, благодаря чему объект получается таким, каков он есть — это вклад компонентов, из которых он состоит:
- У единственного «видимого» элемента машины будет компонент Renderer, который отрисовывает машину и, вероятно, компонент Collider, задающий для нее границы столкновений.
- Если машина представляет персонажа, то у самого объекта car может быть Player Input Controller (Контроллер ввода от персонажа), принимающий все события, связанные с нажатиями клавиш, и транслирующий их в код, отвечающий за движение машины.
В коде есть MonoBehavior , вездесущий родительский класс для представления компонентов. Большинство невстроенных компонентов будут наследовать от MonoBehavior , который, в свою очередь, наследует от Behavior и Component , соответственно.
- Все объекты, обладающие здоровьем, будь то Player (Игрок) или Enemy (Враг) могут иметь компонент LivingObject , задающий исходное значение здоровья, принимающий урон и приводящий в исполнение смерть, когда объект умирает.
- Кроме того, у игрока может быть компонент ввода, контролирующий сообщаемые ему движения, а у врага может быть аналогичный компонент, реализованный при помощи искусственного интеллекта.
Как вы догадываетесь, компоненты также могут предоставлять и публичные методы. Другие компоненты могут принимать ссылку на данный и вызывать эти публичные методы.
Ресурсы
Ресурсы – это расположенные на диске сущности, из которых состоит игровой проект. К ним относятся сети (модели), текстуры, спрайты, звуки и другие ресурсы.
Когда ваши сцены сериализуются на диск, система представляет их в виде ресурсов, состоящих из игровых объектов внутри них. В следующем разделе мы также рассмотрим, как превратить часто переиспользуемые игровые объекты в ресурс под названием «шаблонные экземпляры» (prefab).
Также ресурсы могут представлять менее «осязаемые» объекты, например, карты контроля ввода, графические настройки, строковые базы данных для интернационализации и многое другое. Также можно создавать собственные типы ресурсов при помощи ScriptableObjects. Вот статья о том, как сохранять такие вещи.
Для проекта, находящегося в разработке, ресурсы – это ключевая информационная составляющая базы кода, наряду с кодом как таковым.
В готовом пакете с игрой будет содержаться большинство ваших ресурсов. Они будут сохранены на диске на том устройстве, где установлена игра.
Шаблонные экземпляры
Игровые объекты, их компоненты и параметры ввода существуют в сцене как отдельные экземпляры. Но что, если объекты определенного класса то и дело повторяются? Такие объекты можно оформить в виде шаблонов, каждый из которых – фактически, объект в виде ресурса.
Шаблоны экземпляров в сцене поддаются локальным модификациям, позволяющим отличать их друг от друга (например, если объект дерево выполнен в виде шаблона, то можно сделать экземпляры деревьев разной высоты). Все экземпляры, выполненные по шаблону, наследуют от него и переопределяют данные шаблона.
Вложенные шаблоны
Начиная с Unity 2018.3, поддерживается вложение шаблонов, чего и следовало ожидать:
- Родительский объект с дочерними объектами, представленными в виде шаблонов, сам может быть представлен в виде шаблона. Внутри родительского шаблона дочерний шаблон допускает собственные модификации. В сцене инстанцируется сразу вся иерархия шаблонов, а поверх нее также могут надстраиваться модификации, специфичные для конкретной сцены.
- Шаблонный экземпляр, находящийся в сцене и снабженный собственными локальными модификациями, может быть сохранен как самостоятельный ресурс «Prefab Variant». Этот вариант представляет собой шаблонный ресурс, наследующий от другого шаблона, поверх которого применены дополнительные модификации.
Сериализация и десериализация
Все ресурсы, сцены и объекты вашего проекта долговременно сохраняются на диске. При редактировании игры эти объекты загружаются в память, а затем сохраняются обратно на диск с помощью системы сериализации, действующей в Unity. При тестовых прогонах игры объекты и сцены, находящиеся в памяти, загружаются при помощи одной и той же системы сериализации. Эта система также соотносит ресурсы, находящиеся в скомпилированном пакете, с загруженными/выгруженными объектами сцены в памяти.
Поток сериализации/десериализации, действующий в движке Unity, загружает в память ресурсы, расположенные на диске (в вашем проекте: для редактирования или тестового прогона игры, либо в самой игре, при загрузке сцены) и отвечает за сохранение состояния отредактированных вами объектов и компонентов обратно в соответствующие сцены и шаблонные экземпляры.
Следовательно, система сериализации также является ключевым элементом работы с редактором Unity. Чтобы MonoBehavior мог принять ввод при конструировании сцены в ходе ее инициализации, эти поля должны быть сериализованы.
Большинство базовых типов Unity, в частности, GameObject , MonoBehavior и ресурсы поддаются сериализации и могут получать исходные значения при создании прямо из редактора Unity. Публичные поля в вашем MonoBehavior сериализуются по умолчанию (если относятся к сериализуемому типу), а приватные поля для этого сначала нужно пометить атрибутом Unity [SerializeField] , и тогда они тоже могут быть сериализованы.
Скриншот игры Chaos Reborn производства Snapshot Games, 2015 год. BY-CC-SA 3.0
Использование компонентов
Компоненты ( Components ) определяют поведение объектов в игре. Они — функциональная часть каждого игрового объекта ( GameObject ). Если вы ещё не поняли взаимосвязи компонентов и игровых объектов, прочитайте страницу GameObjects, прежде чем двигаться дальше.
Игровой объект является контейнером для различных компонентов. По умолчанию у всех игровых объектов есть компонент Transform . Потому что Transform диктует, где расположен игровой объект, и как он поворачивается и масштабируется. Без компонента Transform, игровой объект не будет иметь место в мире. Попробуйте создать пустой игровой объект в качестве примера. Выберите в меню GameObject->Create Empty . Выберите новый игровой объект, и посмотрите в инспектор ( Inspector ).
Даже пустые игровые объекты имеют компонент Transform
Помните, что вы всегда можете использовать инспектор, чтобы увидеть, какие компоненты добавлены к выбранному игровому объекту. Компоненты добавляются и удаляются, а инспектор всегда покажет вам, какие из них находятся на объекте в настоящее время. Вы будете использовать инспектор, чтобы изменить все свойства любого компонента (включая скрипты)
Добавление компонентов
Вы можете добавить компоненты к выбранному игровому объекту через меню Components. Сейчас мы попробуем это, добавив Rigidbody на пустой игровой объект, который только что создали. Выделите его и выберите в меню Component->Physics->Rigidbody . После этого, вы увидите, что в инспекторе отобразился компонент Rigidbody и его свойства. Если нажать Play в то время как пустой игровой объект все еще выбран, вы можете получить небольшой сюрприз. Попробуйте, и обратите внимание, как Rigidbody добавил функциональность пустому игровому объекту (Y-компонент игрового объекта начинает уменьшаться, потому что физический движок в Unity заставляет игровой объект падать под действием силы тяжести).
Пустой игровой объект с компонентом Rigidbody
Другой вариант заключается в использовании браузера компонентов ( Component Browser ), который может быть активирована с помощью кнопки Add Component в инспекторе объекта.
Браузер компонентов
Браузер обеспечивает удобную навигацию по компонентам с помощью категорий, а также имеет окно поиска, которое можно использовать, чтобы найти компоненты по имени.
Вы можете прикрепить любое количество или комбинацию компонентов к одному игровому объекту. Некоторые компоненты работают лучше в сочетании с другими. Например, Rigidbody работает с любым коллайдером. Rigidbody контролирует Transform через физический движок NVIDIA PhysX ,а коллайдер позволяет Rigidbody сталкиваться и взаимодействовать с другими коллайдерами.
Если вы хотите узнать больше об использовании каждого компонента, вы можете прочитать о любом из них на соответствующей странице справочника компонентов. Вы также можете получить доступ к указанной странице для компонента в Unity, нажав на маленький ? на заголовке компонента в инспекторе.
Редактирование компонентов
Одной из замечательных особенностей компонентов является гибкость. При подключении компонента к игровому объекту, существуют различные значения или свойства ( Properties ) в компоненте, которые могут изменяться в редакторе при создании игры или через скрипты в запущенной игре. Есть два основных типа свойств: значения ( Values ) и ссылки ( References ).
Взгляните на изображение ниже. Это пустой Игровой Объект с компонентом Audio Source . Все параметры компонента Audio Source в Инспекторе выставлены по умолчанию.
Компонент содержит одно свойство-ссылку и семь свойств-значений. Audio Clip — это свойство-ссылка. Когда этот аудио источник начинает играть, он будет пытаться проиграть файл, на который ссылается свойство Audio Clip . Если такой ссылки не окажется, то возникнет ошибка, так как никакое аудио не будет проиграно. Вы должны назначить файл в Инспекторе. Это просто: перетащите файл из Project View на свойство-ссылку или с помощью выбора объекта (Object Selector).
Теперь файл со звуковым эффектом назначен свойству Audio Clip
Компоненты могут включать ссылки на любые другие типы компонентов, игровых объектов или ассетов. Вы можете узнать больше о назначении ссылок на странице о редактировании свойств-ссылок.
Все остальные свойства после Audio Clip это свойства-значения. Они могут быть отрегулированы прямо в Инспекторе. Свойства значения после Audio Clip — это все переключатели, числовые значения и выпадающие меню, но свойства-значения могут также быть текстовыми строками, цветами, кривыми и другими типами. Вы можете узнать больше об этом и о редактировании свойств-значении на странице о редактировании свойств-значений.
Команды контекстного меню компонента
Контекстное меню для компонента имеет ряд полезных команд.
Контекстное меню компонента
Те же команды также доступны через иконку-шестеренку в крайнем верхнем правом углу панели компонента в инспекторе.
Сбросить
Эта команда восстанавливает значения свойств компонента, которые были до самой последней сессии редактирования.
Удалить
Команда Remove Component доступна в случаях, когда вы более не нуждаетесь в связи компонента с игровым объектом. Обратите внимание, что некоторые комбинации компонентов, которые зависят друг от друга, работают только когда Rigidbody также прикреплено; вы увидите предупреждающее сообщение, если вы попробуете удалить компоненты, которые зависят от каких-либо других компонентов.
Переместить Вверх/Вниз
Каждый компонент Image Effects применяет определенный визуальный эффект к итоговому изображению, но порядок, в котором эффекты применяются, важен. Контекстное меню имеет Move Up (поднять вверх) и Move Down (опустить вниз).
Скопировать/Вставить
Команда Copy Component сохраняет тип и текущие настройки свойств компонента. Они могут быть вставлены в другой компонент того же типа с помощью команды Paste Component Values . Вы также можете создать компонент в объекте со скопированными свойствами, используя команду Paste Component As New .
Проверка свойств
Пока ваша игра находится в Режиме Проигрывания , вы вольны изменять свойства любых Игровых Объектов в инспекторе. Например, вы можете захотеть поэкспериментировать с разными высотами прыжка. Если вы создадите свойство Jump Height в скрипте, вы сможете войти в Режим Проигрывания, изменить значение, и нажать кнопку прыжка, чтобы посмотреть, что произойдет. Затем, не выходя из Режима Проигрывания, вы можете изменить его снова и увидеть результат через секунду. Когда вы выйдете из Режима Проигрывания, ваши свойства вернутся к их изначальным “предыгровым” значениям, так что вы не потеряете свои труды. Такой рабочий процесс дает вам невероятную мощь для экспериментов, регулировки и совершенствования геймплея без лишних затрат времени в повторяющихся циклах. Попробуйте это с любыми свойствами в Режиме Проигрывания. Мы думаем, вы будете впечатлены.
Компоненты и моноповедения в Unity
Каждый игровой объект в Unity состоит из компонентов. Любой компонент четко реализует конкретный набор поведений, необходимых для того, чтобы выполнялся GameObject. Почему объект получается таким, каков он есть? Именно благодаря вкладу, который вносят компоненты, из которых этот объект состоит.
На картинках ниже — объект «Warrior». Он показан над окном «Инспектор» в интерфейсе игрового движка Unity. Обратите внимание на проиллюстрированные разделы (Animator, Rigidbody, Collider) – они, как раз таки, и являются компонентами, слагающими этот объект.
Представим, к примеру, что речь идет про автомобиль в игре:
— у единственного «видимого» элемента авто будет компонент Renderer, отрисовывающий машину и, скорее всего, компонент Collider, который задает для авто границы столкновений; — если авто представляет персонажа, то у самого объекта Car возможно наличие Player Input Controller (контроллера ввода от персонажа). Этот контроллер принимает все события, связанные с нажатиями клавиш, а также транслирует их в код, который, в свою очередь, отвечает за движение автомобиля в игре.
Нельзя не сказать, что есть возможность писать большие и сложные компоненты, в которых компонент один в один равен кодируемому объекту (к примеру, компонент player содержит код, который полностью описывает персонажа, а компонент enemy, в свою очередь, полностью кодирует противника). Как правило, принято извлекать логику и дробить ее на небольшие «обтекаемые» кусочки, которые будут соответствовать конкретным признакам. К примеру: • все объекты, которые обладают здоровьем, будь то Player или Enemy, могут иметь компонент LivingObject, который задает исходное значение здоровья, принимает урон и приводит в исполнение смерть, если объект умирает; • у игрока может быть компонент ввода, который контролирует сообщаемые ему движения, а у врага — аналогичный компонент, реализованный посредством искусственного интеллекта.
На протяжении своего жизненного цикла компоненты получают разные обратные вызовы, которые в среде Unity называются Сообщениями. В частности, к Сообщениям относятся: — OnEnable/OnDisable, — Start, — OnDestroy, — Update и прочие.
Если объект реализует метод Update() , то данный метод будет вызываться Unity в каждом кадре игрового цикла, и происходить это будет до тех пор, пока объект является активным, а заданный компонент действует. Эти методы можно пометить private — в таком случае игровой движок Unity все равно будет вызывать их.
Наверное, вы уже догадались, что компоненты могут предоставлять и публичные методы. В таком случае другие компоненты могут принимать ссылку на данный компонент и вызывать эти публичные методы.
Остается добавить, что в коде есть MonoBehavior — вездесущий родительский класс, предназначенный для представления компонентов. Большая часть невстроенных компонентов будут наследовать от MonoBehavior, а он уже, в свою очередь, будет наследовать от Behavior и Component, соответственно.