Как сделать уровни в юнити
Реализация и оптимизация генератора уровней в Unity
В мае этого года мы обсуждали алгоритм, который используем для генерации внешнего мира в игре Fireside. Сегодня мы возвращаемся к этой теме. В прошлый раз нам удалось сгенерировать набор точек на текстуре с помощью фильтрации шума Перлина. Хотя это решение удобно, оно имеет две важные проблемы:
Интеграция генерации карт в движок Unity
Мы будем писать Scriptable Objects движка Unity для создания модульного окружения в целях генерации карт. Таким образом, мы дадим гейм-дизайнерам свободу настройки входных данных алгоритма без необходимости работы с кодом. Если вы ещё не слышали о ScriptableObjects, то рекомендую для начала изучить документацию Unity.
Сначала нам потребуется набор различных контейнеров данных. Наш конвейер довольно сложен, и если поместить все необходимые параметры в один объект, он окажется слишком объёмным. Поэтому мы будем использовать по одному пакету данных на каждый уровень алгоритма.
Итак, карта составляется из одного или нескольких сегментов (slice), состоящих из одного или нескольких фрагментов (chunk), созданных из одной или нескольких текстур. Примечание: в большинстве алгоритмов этап сегментов пропускается, но я включил этот этап для дизайна конкретной игры и генерации путей; о причинах я расскажу в этой статье. Можно без проблем игнорировать сегменты и всё равно реализовать описанное здесь решение. При помощи очень удобного ExtendedScriptableObjectDrawer Тома Кэйла мы можем расширить настройки для простоты редактирования.
Здесь вы видите, какой тип данных и на каком уровне мы упаковываем. По сути, каждая генерируемая нами текстура будет распределять один ассет карты. Поэтому чтобы получить разнообразное распределение ассетов, нам нужно наложить друг на друга множество текстур. Разбиение карты на фрагменты и сегменты позволяет нам изменять генерируемые ассеты в соответствии с расстоянием от точки начала координат.
Каждый уровень данных имеет связанный с ним класс C#, использующий паттерн «фабрика», который мы применяем для выполнения логики каждого этапа. Если бы мы хотели только распределять ассеты, то этапы генерации были бы очень простыми. Однако нам также нужно создать пути, по которым будет двигаться игрок. Это немного усложняет архитектуру, потому что после генерации точек нам нужно соединить фрагменты и сегменты.
Генератор карт
Генератор сегментов
Генератор фрагментов
Если не учитывать пока генерацию путей, то единственная логика, которая нам сейчас нужна — это преобразование сгенерированных на текстуре точек в мировое пространство. Это реализуется благодаря использованию параметра масштабирования из параметров карты, что обеспечивает нам удобный контроль над плотностью размещения ассетов.
scale = 25
scale = 50
Поскольку мы сохранили кажду точку в мировом пространстве со связанным с ней префабом, для расположения ассетов достаточно просто вызвать Instantiate для префаба, ссылка на который указана в соответствующем слое параметров фрагмента. Единственное, что нужно учитывать — наш алгоритм не гарантирует, что ассеты не наложатся друг на друга. Пока мы применим такое решение: дадим каждому префабу коллайдер и будем уничтожать все ассеты, с которыми пересекаемся при создании экземпляра префаба. Как сказано в нашем предыдущем девлоге, нужно вызвать Physics2D.SyncTransforms() и yield return new WaitForFixedUpdate(), чтобы проверки коллизий работали правильно.
Вот и всё! Нам удалось преобразовать наш эксперимент на Processing в работающую систему на движке Unity! Но, увы…
Ускоряем работу
Мы улучшили наш алгоритм, распараллелив его. Так как мы генерируем набор независимых друг от друга текстур (но зависящих от лежащего в их основе шума Перлина), то можно распараллелить генерацию текстур в каждом фрагменте и даже распараллелить генерацию фрагментов.
В официальной документации C# написано, что async / await являются базовой функциональностью C#. Хотя я хорошо знаком с другими возможностями. перечисленными на этом сайте, до начала проекта я не использовал ни async, ни Tasks. Основная причина заключается в том, что в Unity есть похожая функциональность. И это… (барабанная дробь) корутины. На самом деле, в руководствах по программированию на C# в качестве примера используется стандартный способ применения корутин (выполнение запроса к серверу). Это объясняет, почему я (и многие другие Unity-разработчики, которых я знаю) пока не использовал пока асинхронное программирование на C#. Однако это очень полезная возможность и мы используем её, чтобы распараллелить генерацию карт.
Вот краткое введение в асинхронное программирование. Как и в случае с корутинами, при реализации асинхронного метода нам нужно возвращать особый тип (Task). Кроме того, нужно пометить метод ключевым словом async. Затем можно использовать ключевое слово await таким же образом, каким бы мы использовали оператор yield в корутине.
Однако существует также очень удобный метод Task.WhenAll, который создаёт Task, блокирующий исполнение, пока не будет завершён набор задач. Это позволяет нам реализовать следующее:
В отличие от корутин, эти задачи выполняются параллельно и не тратят время выполнения в основном потоке. Теперь мы просто можем использовать такой подход при генерации как фрагментов, так и текстур. Это значительно увеличивает производительность: с примерно 10 секунд на сегмент до 3 на сегмент.
При этом мы получаем алгоритм, способный генерировать достаточно сложные и обширные карты примерно за 10 секунд (3 сегмента). Возможны дальнейшие оптимизации, а производительностью можно управлять с помощью размера используемых текстур.
Игра-головоломка Neo Angle. Работа с уровнями в Unity
Всем доброго времени суток! Я бы хотел вам рассказать историю своей новой игры-головоломки Neo Angle, а также поделиться опытом импортирования, хранения и генерации уровней в Unity.
Начну с краткой предыстории 5-летней давности, когда я решил заняться геймдевом, впервые познакомившись с языком программирования action script 3.0 (для разработки flash-игр) в университете.
Закончив семестр, я решил, что смог бы осилить собственную игру. Так, посидев какое-то время за блокнотом и карадашом, пришла идея головоломки с треугольниками, где игроку нужно путем переворачивания треугольного камня заполнить заданную форму и закончить уровень на финишной клетке.
Таким образом, затратив неделю на разработку в одиночку, была выпущена моя первая flash-игра SeaQuest.
И понесло меня разрабатывать дальше. Выпустив еще несколько портальных флэшек, я вернулся к идее с треугольниками, что привело к новой части, где геймплей был полностью переработан, а именно добавлены дополнительные интерактивные объекты: кнопки с препятствиями, телепорты и вращатели. Цель уровня также изменилась, теперь нужно было собрать все жемчужины и прийти к финишу.
Результат под названием Stone Quest
На этом линейка логических игр завершилась и была успешно мной позабыта.
Далее последовал обещанный обвал флэш индустрии, который перекинул меня в unity разработку для мобильных. И вот, в декабре 2016 года, совершенно случайно мне в голову вернулась мысль о треугольно-ориентированных головоломках. Особенно подтолкнуло к разработке то, что геймплей отлично подходит под тачскрин. Было решено использовать механику предыдущей игры, но с другой стилизацией.
Вот такая предыстория, которая привела к активной полуторамесячной разработке. Большая часть работы, как ни странно, ушла на геймдизайн. Из предыдущей версии почти все уровни мне показались недостаточно интересными, хотя возможности геймплея имеют гораздо больший потенциал. В следствие чего разработка началась с редактора уровней, который я написал за вечер на старом добром флэше, с возможностью тестирования написанных уровней и их экспорта/импорта в xml.
Результат работы в редакторе продемонстрирован ниже:
Прежде чем перейти к работе с Unity, мне необходимо было убедиться в том, что я смогу предоставить достаточное количество уровней для мобильной игры, которая требует разнообразный контент. Поэтому первые полторы недели я даже не открывал Unity, а работал в своем редакторе, параллельно занимаясь графикой. К слову, был выбран набирающий популярность ретро-стиль synthwave 80-ых. Он достаточно прост в исполнении, являясь при этом очень привлекательным.
Таким образом, создав допустимый набор уровней, я приступил к их портированию в Unity, продолжив тратить часть времени на доработку и создание новых.
В связи с этим, возникли следующие вопросы: каким образом импортировать, хранить и генерировать уровни в Unity из xml файла?
Найденных вариантов решения было три:
1. При старте игры вычитывать xml файл и каждый раз динамически создавать уровни на runtime.
2. В edit mode сгенерировать уровни и создать на каждый по сцене.
3. В edit mode сгенерировать уровни и создать для каждого префаб.
Очевидно, что первый вариант не самый лучший, т.к. все уровни заранее подготовлены и нету смысла их каждый раз заново создавать. Второй вариант больше подходит для более сложных и масштабных проектов, где каждый уровень имеет свою логику, интерфейс, структуру и т.п. В моем случае пришлось бы дублировать интерфейсы и ключевые объекты в каждую сцену с уровнем и при каждом изменении ui нужно было бы обновлять все сцены. Остается последний третий вариант, который как раз очень удачно подошел. О его реализации я и продолжу повествование.
Для работы с объектами в edit mode добавим кастомную панель в редакторе Unity. Для этого создаем класс наследуемый от EditorWindow в папке Assets/Editor и добавляем туда следующий метод:
Далее, в методе OnGUI можно начинать добавлять кнопки и поля для нашего окна.
Больше информации по компонентам можно найти в официальной документации. А я продолжу описание моего Level Loader’a, продемонстрировав принцип работы ниже:
Сперва, мы вычитываем уровни из xml файла, путем нажатия на Read Levels, после этого создается выпадающий список с возможностью выбора уровня и появляются дополнительные контролы, которые позволяют сгенерировать уровень на сцене, создать префаб выбранного уровня, пересоздать префабы всех уровней, показать/скрыть номера полей (для тестирования отрисовки и отслеживания ошибок).
Для создания объектов на сцене в edit mode используется стандартная функция Instantiate, а для удаления DestroyImmediate.
Какое-то время я создавал префабы уровней вручную, перетягивая их со сцены в папку Resources. Однако, это быстро мне надоело и я полез в интернет за информацией о том, как создавать префабы в edit mode программными средствами. Ниже конструкция, позволяющая это сделать:
Далее, в режиме игры уровни добавляются на сцену следующим образом:
Так, этапы добавления уровней получились следующие:
Спасибо за внимание! Поиграть в игру можно в Google Play по запросу Neo Angle.
Опыт разработки первой игры на Unity, часть 3
Ошибка планирования
Возникла внезапная проблема: пусть во время битвы герои и получают опыт, повышают уровень — но этот прогресс должен сохраниться только при успешном завершении уровня. А смена уровня у меня идет следующим пунктом плана работ!
Так что в этой части будет смена уровня вместо прокачки героев.
Взаимоисключающие цели
Вот какая штука. По моей задумке хочу сделать следующее:
1) Сделать выбор следующей битвы так же, как в AFK Arena сделан мистический лабиринт.
Краткое описание
Игрок может выбрать только ближайшую к себе точку. Например, он выбрал клетку с Мистиком. Тогда следующим шагом он может выбрать либо правую точку (карету), либо центральную. Крайний левый флаг будет для него недоступен
2) Сделать выбор следующей битвы максимально простым.
Загвоздка в том, что вариант с лабиринтом сложен для восприятия — а я хочу, чтобы моя игра была максимально однокнопочной.
Плюсы же лабиринта очевидны — игрок сам решает, что ему будет комфортнее на следующем шаге, у игрока есть выбор, что делать дальше. Плюс игрок видит точки интереса на всей карте и может планировать свой маршрут, исходя из этого.
Грамотно озвученная проблема — часть решения
Хмм, в лабиринте игрок принимает решение… Делает выбор… Планирует маршрут… Стоило только озвучить проблему — тут же пришло в голову решение. Сочетает и простоту восприятия, и предоставляет игроку выбор, игрок ощущает, что он принимает решение. Встречайте!
Тут игрок сражается с противником, после чего выбирает, что ему делать дальше: пойти в следующую битву или пойти в… Эээ, пока не анонсированную, но уже придуманную сущность. О ней будет одна из следующих статей 😉
Объекты или UI?
Выбор следующей битвы представляю в виде условных островков, соединённых линиями. И вот эти островки могут выглядеть по разному: например, с замком на нем. И вот мне почему-то кажется, что GameObject больше подходит для такого..
С другой стороны готовые кнопки уже имеют важную для дальнейшего реализацию — всякие красивости для нажатого состояния, отпускаемого, ну и так далее. Итак, выбираю! Следующие уровни будут отображаться в виде UI кнопок — как раз потому что они уже имеют нужную мне визуализацию нажатий.
Завершение биты — переход на экран выбора — начало битвы
Битва и экран карты— две отдельные сцены.
Используется только одна сцена — но при «переходе» активируются / деактивируются соответствующие объекты.
Пока что выбрал второй вариант. С ним игра банально переходит между этими состояниями быстрее. Минус пока один — игра вроде как кушает больше ресурсов. Сделаю — проверю.
Переделка архитектуры объектов на сцене
Суть вот в чем. Будет примерно следующее
При «переходе» на экран карты отключаются все объекты, относящиеся к битве, и включаются объекты, относящиеся к карте. При выборе уровня все происходит ровно наоборот.
Но сейчас у меня при старте на сцене куча объектов безо всякого порядка — в основном из-за того, как сделан спавн героев игрока и противника
Решаю просто: перед вызовом спавна героев создаю два дополнительных объекта — для героев игрока и героев противника соответственно
После этого при спавне героев достаточно указать, что соответствующий _goParent… их родитель. Это так мило — теперь у них есть родитель.
Главное, что мне это дало — как только spawner отключается, пропадают все герои и панель с их скиллом. И появляются при его включении.
На данный момент этого достаточно, но чуть позже в рамках этого этапа нужно сделать так, чтобы при повторной активации все вновь появлялись на своих первоначальных местах — ну и чтобы сбрасывались их параметры.
А дальше что?
Ах да — забыл. Теперь архитектура объектов на сцене выглядит вот так
Дальше нужно добавить саму структуру кнопок / битв. Располагаться они будут как на одной из картинок выше.
Хммм, а как это сделать кодом?
Конечно же, первым делом полез в интернет! Наверняка уже кто-то такое делал) Только ничего подобного не нашел. Хорошо, придется думать.
У меня есть повторяющийся паттерн поведения: 1 — 2 — 1 — 2 и так далее. Значит, мне нужно сделать так, чтобы на экране появлялась комбинация «1 — 2». Хотя стоп — а вдруг я захочу сделать 1 — 2 — 2 — 2 — 1 или что-то такое.
Между делом наткнулся на новую для себя в юнити штуку — Grid. Судя по всему, там как раз можно задать любой шаблон — в том числе такой, какой мне нужен. Но как-то не удалось заставить его работать.
В итоге придумал какую-то абсолютно хитровыдуманную конструкцию. Что-то мне подсказывает, что я перемудрил
Ну и что я натворил
Ура! Чем нравится это решение: вместо второй «i» могу подставить 1 или 2. Например, мне нужна последовательность 1 — 1 — 2 — 1 — 2 — 2 — 2.
Тогда я делаю что-то типа такого:
Вызов ее через for, но вместо _maxLvl будет 1 («SpawnMap(i, 1);»).
Вызов ее через for, но вместо _maxLvl будет 2 («SpawnMap(i, i);»).
Вызов ее через for, но вместо _maxLvl будет 2 («SpawnMap(i, 2);»).
И да, в первом случае for не нужен, но это для меня. Иначе могу запутаться.
Разделение битв и особой сущности
По задумке с одной стороны будут битвы, с другой — будет находиться эта самая сущность. Но тут вообще просто — создаю функцию SetBtnType(int type)
И добавляю ее вызов в SpawnMap() куда-нибудь в конец
Конечно, и в самом спавнере еще нужно добавить массив с кнопками
Перед тем, как сделать переключением «состояния» рабочим, нужно сделать еще одну вещь.
Сброс параметров всего, что отключается
Помните DEBUG_SWITCH, которая просто отключает / включает объекты? Забудем о ней! Теперь на каждом объекте есть скрипт, который не просто отключает объекты, а еще делает что-нибудь с их параметрами, если это нужно.
И эта свитч вызывает нужную функцию из скрипта каждого такого. По сути, у моих объектов появился контроллер, отслеживающий их состояние. Круто же!
Какие типовые сущности у меня есть? Герои, кнопки их скиллов и кнопки карты.
Сходу наткнулся на интересное архитектурное решение. По какой-то причине контроль ползунков на кнопке использовании скила у меня находится… В скрипте Characteristics. Это гениально, не иначе. Значит, переношу всю эту систему туда, где ей самое место — в скрипт кнопки. А Characteristics будет туда только передавать актуальные данные.
Заодно исправил незамеченный баг с постоянным ускорением наполнения маны. И добавил шкалы здоровья с маной над всеми героями — ничем не отличается от аналогичных шкал на кнопках.
Вот такая штука теперь у меня переключает «активные сцены»
Честно — понятия не имею, как ее сократить. Первый for проходится по всем героям игрока, следующий — по героям противника. Если попытаться сразу обновить цель для героя в первом цикле, то герои игрока не найдут противника, увы.
А всякие WakeUp вообще простейшие
В итоге получилось вот так
При респавне герои начинают с начальными характеристиками.
Следующая задача — поменять сущность имеющихся спавнеров. Теперь их задачей будет что-то типа инициализации объектов: создание, выдача параметров, но не появления на экране. За появление отныне отвечает SwitchActiveState, в котором у меня и происходит переключение. Это ответственная задача, но я верю в него.
Но это еще не все!
Игра умеет менять состояние «битва / карта», теперь нужно сделать так, чтобы кнопки на карте запускали битву — причем разную в зависимости от кнопки!
Для этого вернусь в скрипт, спавнящий на карте кнопки боев и добавлю что-то типа такого
Гляньте, как классно все получается! Номер уровня увеличивается только у кнопок по центру и крайних левых
Подготовка завершена! Теперь, наконец, можно запускать битву, выбрав нужную кнопку на карте.
В бой!
В скрипте смены состояния делаю небольшие перестановки, вынеся кучу текста по разным функциям
И происходит магия!
Тут можно увидеть, что включаются разные уровни в зависимости от нажатой кнопки. Еще можно заметить, что правые кнопки ничего не включают — это нормально пока что. Там будет кое-что другое.
В дальнейшем уровни будет отличаться — противниками и сложностью. Но сделать это смогу только когда разберусь с серверной частью.
Результат
Игра научилась запускать нужную битву и выходить обратно на экран карты. Считаю, это успех! Остался только маленький штрих
Уф, теперь можно заняться прокачкой героев. Вернусь, когда с ней закончу!
Процедурная генерация лабиринтов в Unity
Примечание: этот туториал написан для Unity 2017.1.0 и предназначен для опытных пользователей. Подразумевается, что вы уже хорошо знакомы с программирование игр в Unity.
Вы, как Unity-разработчик, наверно, имеете достаточный опыт в создании уровней вручную. Но хотели ли вы когда-нибудь генерировать уровни на лету? Процедурная генерация мешей для полов и стен, в отличие от простого расположения заранее созданных моделей, обеспечивает гораздо большую гибкость и реиграбельность игры.
В этом туториале вы научитесь следующему:
Приступаем к работе
В большинстве алгоритмов (таких, например, как этот и этот) создаются «идеальные» плотные лабиринты, то есть такие, у которых есть только один верный путь и нет петель. Они похожи на лабиринты, публикуемые в газетных разделах «Головоломки».
Однако в большинство игр приятнее играть, когда лабиринты неидеальны и в них есть петли. Они должны быть обширными и состоящими их открытых пространств, а не из узких извилистых коридоров. Это особенно справедливо для жанра rogue-like, в котором процедурные уровни являются не столько «лабиринтами», а скорее подземельями.
В этом туториале мы реализуем один из простейших алгоритмов лабиринтов, описанный здесь. Я выбрал его для того, чтобы реализовать лабиринты в игре с минимальным количеством усилий. Такой простой подход хорошо работает в классических играх, перечисленных по ссылке, поэтому мы можем использовать его для создания лабиринтов в игре под названием Speedy Treasure Thief.
В этой игре каждый уровень — это новый лабиринт, в котором спрятан сундук с сокровищем. Однако у вас не так много времени на его поиски и побег до того, как вернутся стражники! На каждом уровне есть ограничение по времени и вы можете играть, пока вас не поймают. Набранные очки зависят от количества украденных вами сокровищ.
Для начала создадим в Unity новый пустой проект.
Скачайте заготовку проекта, распакуйте её и импортируйте в новый проект **proc-mazes-starter.unitypackage**. В заготовке проекта есть следующее содержимое:
Задаём архитектуру кода
Начнём с добавления в сцену пустого проекта. Выберите GameObject ▸ Create Empty, назовите его Controller и поместите в (X:0, Y:0, Z:0). Этот объект будет просто точкой присоединения скриптов, управляющих игрой.
В папке Scripts проекта создайте скрипт C# с названием GameController, а затем создайте ещё один скрипт и назовите его MazeConstructor. Первый скрипт будет управлять игрой в целом, а второй — заниматься генерированием лабиринта.
Замените все строки в GameController следующим кодом:
Вкратце расскажу, что мы только что создали:
Теперь замените всё в MazeConstructor на следующий код:
Вот, что здесь происходит:
Для отображения данных лабиринта и проверки того, как он выглядит, добавим в MazeConstructor следующий метод:
Рассмотрим каждый из откомментированных разделов:
Неплохое начало! Однако код пока не генерирует сам лабиринт. В следующем разделе я расскажу, как решить эту задачу.
Генерирование данных лабиринта
Заметьте, что в MazeConstructor.GenerateNewMaze() пока пусто; это заготовка, которую мы заполним позже. В конце метода Start() скрипта GameController добавьте следующую строку. Она будет вызывать этот метод-заготовку:
«Волшебные» числа 13 и 15 — это параметры метода, определяющие размеры лабиринта. Хотя мы пока их не используем, эти параметры размера задают количество строк и столбцов сетки.
На этом этапе мы можем приступить к генерированию данных для лабиринта. Создайте новый скрипт MazeDataGenerator; этот класс инкапсулирует логику генерирования данных, и будет использоваться в MazeConstructor. Откройте новый скрипт и замените всё на следующий код:
Заметьте, что этот класс не наследует от MonoBehaviour. Он не будет использоваться непосредственно как компонент, а только внутри MazeConstructor, поэтому не обязан обладать функционалом MonoBehaviour.
Здесь добавлено предупреждение о том, что лучше использовать для размеров нечётные числа, потому что сгенерированный лабиринт будет окружён стенами.
Запустите игру, чтобы увидеть пустые данные лабиринта, но уже с правильными размерами:
Отлично! Всё готово к сохранению и отображению данных лабиринта! Настало время реализовать внутри FromDimensions() алгоритм генерации лабиринта.
Описанный выше алгоритм обходит каждую вторую ячейку в сетке (то есть не каждую ячейку!) располагая стену и выбирая соседнее пространство для блокировки. Программируемый здесь алгоритм будет слегка отличаться от него, он также решает, нужно ли пропускать пространство, что может приводить к возникновению в лабиринте открытых пространств. Поскольку алгоритм не должен хранить много информации или знать много об остальной части лабиринта, например, о точках ветвления, по которым нужно пройти, то код становится очень простым.
Как вы видите, код получает границы 2D-массива, а затем обходит его:
Перезапустите игру, чтобы увидеть, что данные лабиринта каждый раз новые. Отлично!
Следующая серьёзная задача — генерация 3D-меша из 2D-данных лабиринта.
Генерирование меша лабиринта
Теперь после генерирования всех данных лабиринта мы можем на основании этих данных построить меш.
Создайте ещё один новый скрипт MazeMeshGenerator. Так же, как MazeDataGenerator инкапсулировал логику генерирования лабиринта, MazeMeshGenerator будет содержать логику генерирования меша и использоваться MazeConstructor для выполнения этого этапа генерирования лабиринта.
Точнее, он позже будет содержать логику генерирования меша. Сначала мы просто создадим для демонстрации текстурированный четырёхугольник, а потом изменим этот код для генерирования всего лабиринта. Чтобы сделать это, нам нужно внести небольшие изменения в редактор Unity, и уже потом углубляться в код.
Для начала нам нужно привязать материалы, которые будут применяться к сгенерированному мешу.
Выберите в окне Project папку Graphics, затем выберите в окне Hierarchy Controller, чтобы отобразить в Inspector его компонент Maze Constructor.
Перетащите материалы из папки Graphics в слоты материалов Maze Constructor. Используйте floor-mat для Material 1 и wall-mat для Material 2, а start и treasure перетащите в соответствующие слоты.
Так как мы уже работаем в Inspector, добавим также тэг Generated: нажмите на меню Tag в верхней части Inspector и выберите Add Tag. При генерировании мешей мы будем назначать им этот тэг, чтобы находить их.
Внеся все необходимые изменения в редакторе Unity, откройте новый скрипт и замените всё на этот код:
Основная часть интересного кода находится внутри FromData() ; это метод, который MazeConstructor вызывает для генерирования меша. В данный момент этот код просто создаёт единственный четырёхугольник для демонстрации своей работы. Вскоре мы расширим его до целого уровня.
На этой иллюстрации показано, из чего создан четырёхугольник:
Код длинный, но довольно сильно повторяющийся с небольшими вариациями:
Сначала добавим частное поле для хранения генератора меша.
Создадим его экземляр в Awake(), сохранив генератор меша в новом поле добавлением следующей строки в верхней части метода Awake():
Далее добавим метод DisplayMaze():
Наконец, для вызова DisplayMaze() добавим в конец GenerateNewMaze() следующую строку:
Мы написали класс MazeMeshGenerator и создали его экземпляр в MazeConstructor, поэтому нажмите Play:
Мы построили затекстурированный четырёхугольник полностью с помощью кода! Это интересное и важное начало, поэтому прервитесь, чтобы проанализировать свою работу на этом этапе и понять, как работает код.
Ого, какой длинный кусок кода! Но здесь снова повторяется почти одно и то же, только меняются некоторые числа. В частности, код генерирования четырёхугольника перемещён в отдельный метод AddQuad() для его повторного вызова для пола, потолка и стен каждой ячейки сетки.
Поздравляю, на этом мы закончили с генерированием лабиринта и с основной частью программирования, необходимого для Speedy Treasure Thief! В следующем разделе мы рассмотрим оставшуюся часть игры.
Завершаем игру
Нам нужно внести в код и другие дополнения и изменения, но сначала давайте используем то, что находилось в заготовке проекта. Как я упоминал во введении, в заготовке проекта находятся два скрипта, сцена с игроком и UI, а также вся графика для игры с лабиринтом. Скрипт FpsMovement — это просто односкриптовая версия контроллера персонажа из моей книги, а TriggerEventRouter — это вспомогательный код, удобный работы с триггерами игры.
В сцене уже настроен игрок, в том числе у него есть компонент FpsMovement и
к камере присоединён направленный источник света. Кроме того, в окне Lighting Settings отключены скайбокс и окружающее освещение. Наконец, в сцене есть холст UI с метками для очков и времени.
И это всё, что есть в заготовке проекта. Теперь мы напишем оставшийся код для игры.
Начнём с MazeConstructor. Для начала добавим следующие свойства для хранения размеров и координат:
Теперь нужно добавить новые методы. Первый — это DisposeOldMaze() ; как понятно из названия, он удаляет существующий лабиринт. Код находит все объекты с тэгом Generated и уничтожает их.
Аналогично, FindGoalPosition() по сути делает то же самое, только начинает с максимальных значений и выполняет обратный отсчёт. Добавим и этот метод тоже.
PlaceStartTrigger() и PlaceGoalTrigger() размещают объекты в сцене в позициях начала и цели. Их коллайдер является триггером, применяется соответствующий материал, а затем добавляется TriggerEventRouter (из заготовки проекта). Этот компонент получает функцию обработки события, которая вызывается, когда что-то входит в объём триггера. Добавим и эти два метода.
Наконец, заменим весь метод GenerateNewMaze() следующим кодом:
Переписанный GenerateNewMaze() вызывает новые методы, которые мы только что добавили, для таких операций, как удаление старого меша и расположение триггеров.
Мы уже многое добавили в MazeConstructor, отличная работа! К счастью, с этим классом мы закончили. Остался ещё один фрагмент кода.
Теперь добавим новый код в GameController. Заменим всё содержимое файла следующим:
Отличная работа! Процедурная генерация лабиринтов может быть сложной задачей, но в результате мы получаем захватывающий и динамичный игровой процесс.
Куда двигаться дальше?
Если вы повторяли за мной, то уже создали готовую игру. Если хотите, можете скачать отсюда готовый проект Unity.
Случайная генерация предметов и врагов на карте может оказаться очень интересным занятием!
- Как сделать скрутку в распределительной коробке
- Как узнать что на даче отключили электричество