Найти в Дзене
ZDG

Ни бум-бум в Unity: Объекты против кода

Оглавление

Как я уже писал, Unity очень напоминает Flash. Оба предоставляют два способа работы с проектом.

1. Манипуляция в редакторе

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

Такой подход предполагает максимум наглядности. Движок берёт на себя основную часть работы. Так что им могут пользоваться даже непрограммисты.

В то же время никакой относительно сложный проект не может обойтись без дополнительного кода. Эта проблема решается навешиванием скриптов на объекты.

То есть мы выбираем объект, добавляем к нему скрипт и в этом скрипте описываем какие-то действия для объекта.

В силу архитектуры движка такие скрипты довольно ограничены в поведении.

В Flash у скрипта объекта есть метод onEnterFrame(), а в Unity метод Update(). Они вызываются автоматически в каждом кадре. То есть если приложение работает с частотой 60 кадров в секунду, то 60 раз в секунду у каждого активного объекта вызывается метод Update(), где можно обновлять состояние объекта – например, двигать его по экрану.

2. Чистый код

Здесь мы просто пишем приложение с нуля, как и любую программу. Что это даёт:

  • можно создавать архитектуру и логику любой сложности, не будучи завязанными на скрипты, прикреплённые к объектам
  • код получается более солидный, в отличие от мозаичной помойки, разбросанной по разным объектам

Связь между кодом и объектами

Если мы пишем чистый код, то как нам связать его с теми объектами, которые созданы в редакторе?

В редакторе Юнити есть понятие "Game Object", которому программно соответствует класс GameObject. Это любой объект, который вы добавляете на сцену. Сам по себе он пустой контейнер. Чтобы придать ему функциональность, его можно наполнять различными компонентами, такими как 3D-полигоны, физические модели или скрипты.

В Юнити приложение нельзя написать с нуля. Нужно обязательно прикрепить скрипт к уже существующему объекту типа GameObject.

Поэтому, чтобы следовать концепции чистого кода в Unity, нужно в редакторе создать единственный пустой объект и прикрепить к нему единственный скрипт. Больше ничего в редакторе вы можете не делать, и развернуть всё приложение целиком из этого скрипта. Хотя в принципе это необязательно, так как можно заранее создать какие-то объекты сцены – например, построить целый игровой уровень – и они будут уже доступны этому скрипту.

Компоненты GameObject

Как было сказано выше, GameObject это просто пустой контейнер. Давайте проследим, как из пустого контейнера сделать 3D-куб в редакторе, а затем то же самое в чистом коде.

Сначала в иерархии создаём пустой объект через "Create Empty":

И получаем в иерархии новый объект, который по умолчанию называется GameObject:

-2

Но это не класс, а просто название данного объекта. Его можно переименовать в Cube:

-3

Одновременно этот объект появляется на сцене, а в инспекторе появляются его свойства. Пока что это всего лишь трёхмерные координаты и трансформации (поворот, масштаб).

-4

Хотя этот объект находится на сцене, он невидим. У него нет ничего, что могло бы быть видимым. Нужно добавить ему какие-то компоненты.

Нажмём на кнопку "Add Component" и увидим, что добавить можно целую кучу разного:

-5

Зайдём в подменю Mesh:

-6

И добавим Mesh Filter. Это компонент, который хранит 3D-полигоны для отображения.

В инспекторе у нашего пустого объекта появился новый компонент:

-7

Однако он всё ещё невидим. Для компонента Mesh Filter нужно задать свойство Mesh, то есть собственно данные полигонов. Нажмём справа от Mesh на диск с точкой (непонятно, что он символизирует), и появится меню с заранее заданными формами объектов. Выберем там куб:

-8

Теперь у нас объект с добавленным компонентом Mesh Filter, с добавленной в него геометрией куба:

-9

Но на сцене мы его всё ещё не видим. Почему?

Mesh Filter это только геометрия. Чтобы объект начал отображаться на сцене, нужен ещё один компонент: Mesh Renderer. Добавим и его:

-10

Компонент Mesh Renderer читает данные геометрии из компонента Mesh Filter и отображает их на сцене:

-11

Наш куб теперь виден, но почему-то имеет ярко-розовый цвет. Это потому, что не задан материал для отображения.

В компоненте Mesh Renderer, в разделе Materials, нажмём на диск с точкой и выберем один из готовых материалов:

-12

И теперь наш куб имеет серый цвет.

Мы завершили создание Game Object в редакторе. Обратите внимание: всё, что мы сделали, можно было сделать в один шаг, добавив готовый примитив "Куб". До этого мы добавляли примитив "Сфера", и если открыть её инспектор, то мы увидим там те же самые компоненты Mesh Filter и Mesh Renderer, выбранную геометрию и выбранный материал:

-13

Как это повторить программно?

Для начала нужно прикрепить к какому-нибудь объекту скрипт. Создадим ещё один пустой объект и назовём его Script:

-14

Теперь в инспекторе нажмём Add Component и добавим новый скрипт:

-15

Я назову его Cube:

-16

Компонент скрипта появился в инспекторе, а также в разделе Assets нашего проекта:

-17

По факту, у нас в проекте есть папка Assets, а в ней есть файл Cube.cs, который можно открывать и редактировать как угодно. Отсюда и начинается чистое программирование.

Файл сейчас содержит такой шаблонный текст:

-18

Очевидно, это язык Си-шарп, он же До-диез, он же C#. (Ого, с точкой Дзен не делает хэштег!)

Я не могу в этой теме углубляться в описание языка, поэтому вы должны быть знакомы с синтаксисом, а также с концепциями ООП, такими как классы, наследование, интерфейсы, статические методы и т.п.

Итак, все скрипты, которые прикрепляются к объектам, являются классами. То есть это не просто какие-то строчки кода, а именно изолированные классы. Все они наследуются от встроенного в Unity класса MonoBehaviour. Это единственное ограничение.

Далее, у них есть предопределённые методы Start() и Update(). Про Update() мы уже знаем, что он вызывается в каждом кадре. А Start() вызывается один раз при инициализации объекта.

Таким образом, в методе Start() мы напишем код для инициализации куба, а в методе Update() пока ничего писать не будем.

Чтобы сделать куб программно, надо пройти те же шаги, что и при создании его руками. Сначала создадим пустой GameObject:

GameObject gameObject = new GameObject();

Также можно вместо создания нового объекта GameObject получить ссылку на уже существующий, к которому прикреплён скрипт. Это свойство gameObject класса MonoBehaviour:

GameObject gameObject = this.gameObject;

В общем, неважно как, но у нас теперь есть объект gameObject.

Следующий шаг – добавить в него компоненты MeshFilter и MeshRenderer. Это делается с помощью метода AddComponent с дженериками:

gameObject.AddComponent<MeshFilter>();
gameObject.AddComponent<MeshRenderer>();

И почти всё готово. Как видим, названия классов совпадают с названиями компонентов. Но для MeshFilter требуется задать свойство mesh. А для MeshRenderer – материал.

Чтобы получить доступ к добавленным компонентам, можно писать так:

gameObject.GetComponent<MeshFilter>();

Но так как это слишком неэффективно, то прямо во время добавления сохраним ссылки на компоненты в локальных переменных meshFilter и meshRenderer:

MeshFilter meshFilter = gameObject.AddComponent<MeshFilter>();
MeshRenderer meshRenderer = gameObject.AddComponent<MeshRenderer>();

Теперь настроим мeshFilter, задав ему свойство mesh. Но сначала нужно сделать объект класса Mesh:

Mesh mesh = new Mesh();

Дальнейшее может показаться сложным, но это всего лишь стандартная часть работы с 3D. Mesh состоит из следующих частей:

  1. набор вершин
  2. набор треугольников
  3. набор нормалей к вершинам

В коде это будут свойства Mesh.vertices, Mesh.triangles и Mesh.normals:

-19

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

Заполнив таким образом свойства объекта mesh, мы присваиваем его свойству MeshFilter.mesh:

meshFilter.mesh = mesh;

Теперь зададим материал для рендерера:

meshRenderer.material = new Material(Shader.Find("Standard"));

Здесь всё проще. Создаётся новый объект класса Material и присваивается свойству MeshRenderer.material. Этот материал уже существует в проекте и у него название "Standard" (мы выбирали такой же в редакторе). Выбор материала по имени делается через Shader.Find(), но на этом пока не будем задерживаться.

Теперь посмотрим на полный код:

-20

И на то, что получается при нажатии кнопки Play:

-21

По центру – сфера, которая была добавлена как готовый примитив. Слева – куб, который был добавлен как пустой объект и затем в него были добавлены компоненты в редакторе. Справа – наш программно созданный объект. Это просто треугольник. Чтобы сделать из него куб, нужно добавить ещё вершин и треугольников в meshRenderer.mesh, но я сэкономил на объёме кода.

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

-22

То есть Юнити здесь оказывается очень удобным. Он показывает нам программно созданные свойства объектов так, как будто мы их сделали в редакторе. И даже позволяет их редактировать в реальном времени!

Но как только мы остановим проигрывание, они исчезнут.

Продолжим программно генерировать меш в следующем выпуске: