Модели, сделанные в блендере, о которых я рассказывал в предыдущих частях, готовы и могут быть загружены в игровой движок. В этой части пойдёт речь о том, как собрать и заставить двигаться дорогу в Unity 3d.
Введение
Прошлая часть повествовала о создании сегмента дороги и запекании теней в его текстуры. Кроме самого дорожного полотна с тремя полосами для движения, сегмент дороги включает в себя обочину дороги, дома, стоящие вдоль дороги, и деревья растущие рядом. В реальном мире деревья, дома и прочие объекты при свете солнца отбрасывали бы тени на дорогу и друг на друга, хорошо было бы такое поведение отобразить и в игровом мире. Для чего к цветовой информации в текстуры моделей сегмента и была добавлена информация об уровне освещённости того или иного участка модели.
Имея готовые сегменты дороги можно попробовать загрузить их в Unity 3D и посмотреть, как игра будет выглядеть в действительности, ведь пользователи будут видеть именно ту картинку, которую выведет игровой движок, а не сгенерированную в трёхмерном редакторе.
Unity 3D
Я предполагаю, что читатель уже знаком с основами игрового движка Unity 3D и умеет писать программы на языке C#, ибо изучение с нуля как языка, так и движка выходит за рамки данной статьи.
Сохраняем модели сегментов дороги в файлы формата «.fbx», которые понимает Unity. Как это сделать читайте в первой части. Загрузить модель в движок довольно просто, достаточно перетащить её из проводника на вкладку «Project» редактора Unity. Вкладка «Project» в Unity является чем-то проводника в Windows.
После того, как будут добавлены «.fbx» файлы в движок, во вкладке «Project» появятся все содержащиеся в них модели, в нашем случае - сегменты дороги, теперь с ними можно работать внутри игрового движка.
Для дальнейшей работы нам понадобится сцена. В Unity вся основная работа с графической частью происходит на сценах, на них располагаются модели, источники света, графические эффекты и много чего другого, что мы привыкли видеть в играх. Простая игра может состоять всего из одной сцены, а может иметь несколько, например на одной сцене будет находиться меню игры, а на другой - уровень с игроком и окружающим его миром игры. На текущий момент нам достаточно одной сцены, на которой мы соберём нашу дорогу и посмотрим, как она выглядит в действии.
Когда создаётся проект в Unity, внутри него уже будет сцена, созданная по умолчанию вместе с проектом. На сцене находятся два объекта: источник направленного освещения и камера, через которую игрок будет видеть уровень, и разворачивающееся на нём действо. Взаимодействовать с объектами сцены можно на вкладке «Scene».
Перетаскиваем сегменты дороги из вкладки «Project» на вкладку «Scene» и располагаем их друг за другом, используя блок настроек «Transform», который находится на вкладке «Inspector». В блоке «Transform» есть настройки «Position», вводя значения в поля рядом с буквами «Х», «Y» и «Z» можно менять положение объекта в пространстве по осям координат.
Выстроив все сегменты в ряд и получив прямой отрезок дороги переходим к настройке камеры. Выделяем камеру и перемещаем её в то место, откуда игрок будет видеть дорогу. Когда объект выделяется на сцене, у него появляются три стрелки красного, синего и зелёного цветов, соответствующие осям системы координат. Если потянуть за одну из стрелок, то объект станет перемещаться в пространстве вдоль той оси координат, которой соответствует стрелка. В центре, из которого выходят стрелки, есть три плоскости, в каждой из этих плоскостей лежат две из трёх стрелок, если потянуть за одну из этих плоскостей, то объект будет перемещаться не вдоль одной из осей, а в плоскости, ограниченной двумя осями координат.
Когда выделена камера, на вкладке «Inspector» будут отображаться её параметры, в том числе и компонент «Transform», с помощью которого также можно изменить положение камеры.
Но основные настройки, которые представляют на вкладке «Inspector» для нас интерес это - «Field of View» и «Focal Length». По факту, это одна настройка. Параметры «Field of View» и «Focal Length» связаны между собой, если изменить один из них, то изменится и второй, что и неудивительно, ведь от фокусного расстояния объектива зависит угол зрения камеры, чем меньше фокусное расстояние, тем шире угол зрения.
Подбираем угол зрения на свой вкус, настройки, которые использовал я, показаны на рисунке. Проконтролировать изображение, выводимое камерой, можно либо в небольшом окошке «Main Camera» в правом нижнем углу вкладки «Scene», оно появится, как только будет выделена камера, либо на вкладке «Game». Вкладка «Game» показывает то, что будет видеть пользователь, когда запустит игру. Если какая-то из вкладок отсутствует, то её всегда можно открыть воспользовавшись меню «Window», вкладки «Scene», «Game» и «Inspector» находятся в пункте «General».
Допустим мы добились того, что изображение, выдаваемое камерой, нас устраивает, но есть одна проблема - оно не двигается, если только мы не рассчитываем, на то, что игрок станет медитировать, созерцая неподвижное изображение на экране, нам надо заставить его двигаться, желательно навстречу камере.
Вверху в центре главного окна Unity есть три кнопки: запуск игры, пауза и покадровое исполнение. Но даже если запустить игру, двигаться всё равно ничего не будет. Для того, чтобы привести дорогу в движение нам понадобится программный код.
C#
Поведение объектов в игровом движке Unity контролируется особыми компонентами, они носят название скрипты (scripts). Скрипты прикрепляются к тем объектам, поведением которых они будут управлять. Скрипт представляет из себя файл с кодом, написанным на языке программирования C#.
На вкладке «Project» внутри папки «Assets» создаём папку «Scripts», которая будет хранить все файлы с кодом игры. Нажимаем на кнопку с изображением плюса, и в открывшемся меню в группе пунктов «Create» выбираем пункт «С# Script». В папке «Assets» будет создан файл с расширением «.cs», остаётся только ввести его имя и нажать «Enter».
Первый файл, который мы создадим, будет называться «RoadController», его код должен будет двигать дорогу на нас, создавая эффект движения по дороге вперёд. Код внутри него приведён ниже:
using UnityEngine;
public class RoadController : MonoBehaviour
{
void FixedUpdate()
{
transform.Translate(Vector3.back * 1.5f * Time.fixedDeltaTime);
}
}
Код простой, как и стоящая перед ним задача. Все классы, управляющие объектами на сцене, наследуют от базового класса «MonoBehaviour», который предоставляет доступ к событиям жизненного цикла движка. Сейчас я не стану подробно останавливаться на всех его событиях, в текущем случае интерес для нас представляет только одно из них - «FixedUpdate».
Когда мы открываем только что созданный файл скрипта, в нём всегда присутствуют два метода: «Start» и «Update». Метод «Start» запускается только один раз в самом начале, когда надо инициализировать переменные и прочие объекты. Для перемещения дороги он не подходит, поэтому его можно смело удалить.
Метод «Update» запускается один раз за кадр, он больше подходит для стоящей перед нами задачи, но у него есть один недостаток. Сам кадр в игре не имеет фиксированной длительности, он может длиться разное количество времени. Его длительность зависит как от возможностей компьютера, на котором работает игра, так и от количества задач и расчётов, выполняемых в самой игре. Из-за чего количество времени, прошедшее между двумя вызовами метода «Update», не будет постоянным. Если расположить управляющий движением дороги код внутри метода «Update», то её сегменты могут двигаться неравномерно, что, спустя некоторое время, приведёт к появлению зазоров между ними, пример на рисунке ниже:
Решить эту проблему способен метод «FixedUpdate». В отличие от обычного «Update», «FixedUpdate» выполняется с фиксированной частотой, то есть время между вызовами одинаковое.
Доступ к данным положения на сцене и размера объекта осуществляется через экземпляр класса «Transform», каждый объект на сцене имеет его, мы уже встречались с ним во вкладке инспектора. В классе «Transform» есть метод «Translate», который передвигает объект на определённое расстояние в указанном направлении. В качестве аргумента он принимает вектор, указывающий направление и дистанцию перемещения.
Рассмотрим подробнее произведение, которое передаётся в «Translate». Векторы в трёхмерном пространстве Unity определяется структурой «Vector3». Vector3.back - это краткая запись вектора Vector3(0,0,-1), направленного по оси Z в отрицательную сторону, а длина его равна единице.
Значение «1.5f» имеет тип «float», мною взято произвольно, меняя его можно ускорять или замедлять движение дороги.
Третий множитель «Time.deltaTime» - это интервал времени в секундах между предыдущим и текущим кадрами, он имеет тип float. Игра будет запускаться на разных компьютерах, производительность которых может очень сильно различаться, а следовательно, различаться будет и время выполнения операций внутри игры. Множитель «Time.deltaTime» нужен для того, чтобы скорость движения всегда была одинаковой, вне зависимости от частоты кадров, которую выдаёт игра на конкретном ПК.
Готовый скрипт нужно поместить на тот объект, которым скрипт будет управлять, в нашем случае это сегмент дороги. Для чего достаточно перетащить скрипт на саму модель на сцене, или на вкладку «Inspector», когда на сцене выделена та модель, на которую помещается скрипт.
Проверяем во вкладке инспектора, что напротив имени нашего скрипта стоит галочка, если её нет, то работать скрипт не будет. Запускаем игру и, если всё было сделано правильно, то видим движение дороги.
Заключение
Модель дороги готова, она загружена в игровой движок и даже начинает движение при запуске игры, но спустя определённое время дорога заканчивается, а игрок остаётся висеть в пустом пространстве, где ничего не происходит, так быть не должно. В следующей части я расскажу о том, как генерировать новые сегменты, чтобы дорога не заканчивалась.
Игру, о которой идёт речь, можно скачать тут, так же удобно воспользоваться приложением для загрузки и установки игр с itch.io, которое скачивается здесь.