» » » Создание динамически изменяемого ландшафта для RTS на Unity3D

 

Создание динамически изменяемого ландшафта для RTS на Unity3D

Автор: admin от 27-10-2015, 15:24, посмотрело: 1595

Давным-давно я имел радость играть в замечательнейшую RTS под названием «Периметр: Геометрия Воины» (ссылка на steam) от отечественного разработчика K-D Labs. Это игра о том, как огромные летающие города под названием «Фреймы» бороздят просторы «Спанджа» — цепи соединенных между собой миров. Сюжет достаточно странный и абстрактный, но гораздо более интересной и инновационной составляющей игры была одна из ее технических особенностей, а не сюжет. В отличие от большинства RTS, где сражения происходят на статической местности, в «Периметре», одной из ключевых игровых механик был терраформинг. У игрока были средства манипулировать ландшафтом с целью возведения на нем своих сооружений а также целый арсенал боевых единиц, способных этот ландшафт превратить в потрескавшийся, поплывший и изрыгающий раскаленные камни/противных насекомых ужас.

Как известно, мир RTS нынче переживает некоторый упадок. Инди-разработчики слишком заняты тем, что клепают ретро-платформеры и rouge-like игры зубодробительной сложности, и поэтому, переиграв в «Периметр» некоторое время назад я решил, что должен и сам попробовать реализовать что-то подобное — идея была интересной и с технической и с геймплейной точек зрения. Обладая некоторым практическим опытом в разработке игр (ранее я совершал попытки сделать кое-что на XNA), я подумал, что чтобы добиться хоть какого-то успеха в одиночку мне придется воспользоваться чем нибудь более высокоуровневым и простым. Выбор мой пал на Unity 3D, чья пятая версия только-только вышла из под пресса.

Вооруженный вагоном энтузиазма, вдохновением от только что пройденного «Периметра» и просмотренной серией видеотуториалов по Unity, я начал делать наброски и знакомиться с инструментарием, который мне предложил Unity Editor.

Что предлагает community


Как всегда, первый мой блин вышел комом. Без достаточного обдумывания я начал реализовывать ландшафт с помощью плоскости и кода, который должен был поднимать либо опускать вершины этой плоскости. Многие читатели, хотя бы немного знакомые с Unity могут возразить «Но ведь у Unity есть компонент Terrain, предназначенный специально для этих целей!» — и будут правы. Проблема лишь в том, что я, будучи слишком увлеченным реализацией своей идеи забыл об одной важной вещи: RTFM! Изучи я документацию и форумы чуть более тщательно, я бы не стал решать задачу таким откровенно дурацким способом, а сразу воспользовался бы готовым компонентом.

После двух дней бесполезного пота и алгоритмистики (Плоскость явно не была предназначена для использования в таких целях), я таки начал делать ландшафт с использованием Terrain. Должен сказать, что среди отдельных членов сообщества Unity, ходила идея создания динамического ландшафта для своей игры. Некоторые люди задавали на форумах вопросы и получали ответы. Им рекомендовали использовать метод SetHeights, принимающий на вход кусок нормализованного от 0f до 1f heightmap'а, который будет установлен начиная с точки (xBase; yBase) на выбранный ландшафт.

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

Сама по себе деформирующая часть была до неприличия простая.


Объект DeformationData содержал в себе координаты X и Y, к которым нужно применить деформацию, нормализованный heightmap, который аддитивно либо мультипликативно накладывался на текущий ландшафт и прочий boilerplate, необходимый для работы механизма деформаций.

Также был генератор деформаций, позволяющий, например,


И все это было основой всего Tech Demo, если так конечно можно выразиться.

Анализ результатов первой попытки


Если вы посмотрели Tech Demo, то, вероятно, сразу же заметили определенные проблемы в механизме деформаций. Если же вы не смотрели его (за что я вас не виню), то я вам расскажу что было не так. Основной проблемой была производительность. Точнее ее полное отсутствие. Когда начинались деформации, framerate падал до очень маленьких (на некоторых машинах однозначных) чисел, что было неприемлемо, потому как никакой графики в игре по сути не было. Мне удалось выяснить, что сам по себе метод SetHeights() вызывает сложнейшую череду расчетов LOD для ландшафта и по этой причине не годится для деформаций ландшафта в реальном времени. Казалось бы, мои надежды рухнули и реализация деформаций в реальном времени на Unity невозможна, но я не сдавался и выяснил очевидную, но очень важную особенность механизма пересчета LOD.

Чем меньше разрешение карты высот ландшафта, тем меньше удар по производительности при применении SetHeights().

Разрешение карты высот — это параметр, характеризующий качество отображения ландшафта. Оно является целочисленным (очевидно), отчего в сниппетах выше для обозначения координаты на карте использовались целочисленные переменные. И оно может быть больше размеров ландшафта, например, для ландшафта 256х256 можно установить разрешение карты высот равным 513, что придаст ландшафту точности и менее угловатые очертания. Почему именно 513, а не 512, я расскажу в следующей секции.

Игры с разрешением карты высот позволили мне найти более-менее оптимальные размеры для моей конфигурации, но я был сильно огорчен результатами. Для успешного применения такого ландшафта в RTS, его размеры должны быть достаточно большими, чтобы на нем можно было сосуществовать некоторое время хотя бы двум игрокам. По моим первоначальным оценкам, карта размером 2х2км (или 2048х2048 Unity Units) должна была быть в самый раз. Для того, чтобы не замечать влияния на framerate деформаций в реальном времени, размер ландшафта должен был быть не более 512х512 единиц. Более того, одинарная точность карты высот давала не самые впечатляющие результаты, когда дело касалось визуального качества. Ландшафт был местами угловат и кривоват, что требовало удвоения точности карт высот.

В общем, дела обстояли не очень хорошо.

Super Terrain — концепт и теория


Маленькая заметка: в этой секции рассматривается теоретическая и концептуальная части Super Terrain. Код реализации рассматривается в деталях в следующей секции.

Примерно тогда меня начала посещать следующая мысль: «Раз мы не можем сделать один большой ландшафт и при этом иметь достаточную производительность при деформациях, то почему бы не сделать кучу маленьких и не разместить их бок о бок? — Как chunk'и в Minecraft?» Идея была неплохой, но была сопряжена с некоторыми проблемами:


  • Как выбрать размер для «chunk'а»

  • Как сделать так, чтобы на стыках «chunk'ов» не было заметных швов

  • Как применять деформации, происходящие на стыках соседних chunk'ов


Первая проблема

Первая проблема была достаточно пустяковой: Я просто выбрал размер для chunk'а 256х256 с двойной точностью (heightmap resolution = 513). Такой setup не вызывал у проблем с производительностью на моей машине. Возможно в будущем пришлось бы пересмотреть размеры chunk'ов, но на текущем этапе, такое решение меня устраивало.

Вторая проблема

Что касается второй проблемы, то у нее было две составляющих. Первая, очевидно, состояла в том, чтобы выравнять высоты соседних «пикселей» карт высот соседних chunk'ов. Именно во время решения этой проблемы я и понял, почему разрешение карты высот является степенью двойки + 1. Продемонстрирую на иллюстрации:

Создание динамически изменяемого ландшафта для RTS на Unity3D

Очевидно, что для того, чтобы сохранялось равенство высот в соседних ландшафтах, «последний» пиксель карты высот первого ландшафта должен быть по высоте равен «первому» пикселю следующего:

Создание динамически изменяемого ландшафта для RTS на Unity3D

Очевидно, «Super Terrain» — это и есть матрица Unity Terrain'ов, объединенных механизмом наложения heightmap'ов и деформаций.

После того, как реализация кода для объединения ландшафтов была завершена (применение локальных деформаций малого размера было оставлено на потом — сейчас требовалось разработать механизм создания матрицы Terrain'ов и начальная инициализация карты высот), открылась еще одна составляющая второй проблемы (отсутствие швов на стыке ландшафтов), которая к счастью была разрешена достаточно просто. Проблема состояла в том, что для такого «совмещения» ландшафтов необходимо объяснить Unity, что они используются совместно и являются соседними. Для этого разработчиками был предусмотрен метод SetHeighbors. Мне до сих пор не совсем понятно, как он работает, но без него на стыках ландшафтов появляются артефакты с тенями и темные полосы.

Третья проблема

Самая интересная и сложная из трех, эта проблема не давала мне покоя не меньше недели. Я отбросил четыре различных реализации пока не пришел к финальной, о которой я и расскажу. Сразу сделаю небольшое замечание об одном важном ограничении моей реализации — она предполагает, что локальная деформация не может быть размером больше, чем один chunk. Деформация по прежнему может находиться на их стыке, однако сторона матрицы деформации не должна превышать разрешения карты высот chunk'а и все они должны быть квадратными (карты высот, сами chunk'и и деформации). В целом, это не является большим ограничением, так как любая деформация больших размеров может быть получена путем применения нескольких малых по очереди. Что касается «квадратности», то это ограничение карты высот. То есть обязана быть квадратной только ее карта высот, на которой могут быть «нулевые» участки при аддитивном либо «единичные» при мультипликативном применениях.

Сама по себе идея алгоритма для универсального применения деформации состояла в следующем:


  1. Разделить heightmap деформации на девять частей по одной на каждый из chunk'ов, на который потенциально может повлиять деформация. Так центральная часть будет отвечать за деформацию chunk'а, на который непосредственно попала деформация, стороны отвечают за chunk'и, находящиеся слева/справа или сверху/снизу и т.д. В случае, если деформация не изменяет chunk, то ее составляющая будет равна null.

  2. Применить частичные heightmap'ы к соответствующим chunk'ам, либо проигнорировать изменения, если частичный heightmap равен null.


Такой подход позволяет универсально применять и деформации находящиеся как в самом центре chunk'а и не затрагивающие других chunk'ов, так и на границах с ними либо даже в углах. Делить именно на девять частей (а не на четыре) нужно по причине того, что в случае, если деформация начинается или заканчивается на границе chunk'а, граничные пиксели того, который находится по соседству с ним тоже должны быть изменены. (Для того, чтобы не было видимым швов — см. решение проблемы номер 2).

Super Terrain — практика


Создание SuperTerrain

В рамках второй проблемы был разработан мезанизм, позволяющий объединить несколько Terrain'ов в один и применять «глобальные» карты высот, ложащиеся на весь ландшафт целиком.

Скажу сразу, что понадобилась эта возможность глобального применения карты высот потому как для создание ландшафта было процедурным, а для его использования использовался Square-Diamond алгоритм, выходом которого и была большая матрица float'ов — наша большая карта высот.

В целом, создание SuperTerrain — достаточно простой и интуитивно понятный процесс, описанный


Собственно создание ландшафтов происходит в методе InitializeTerrainArray(), который заполняет массив Terrain'ов новыми экземплярами и перемещает их на нужное место в игровом мире. Метод BuildNewTerrain() создает очередной экземпляр и инициализирует его нужными параметрами а также помещает внутрь GameObject'a «parent» (предполагается, что на сцене будет предварительно создан игровой объект, вмещающий в себя chunk'и SuperTerrain'а, чтобы не загрязнять инспектор лишними игровыми объектами и упростить cleanup, если он понадобится.)

Здесь же применяется лечение одной из проблем с черными полосами на границах ландшафта — метод SetNeighbours(), который итерируется по созданным ландшафтам и проставляет им соседей. Важное замечание: метод TerrainData.SetNeighbors() должен применяться для всех ландшафтов в группе. То есть, если вы указали, что ландшафт А является соседом сверху ландшафта В, то вам также нужно указать что ландшафт В является соседом снизу для ландшафта А. Эта избыточность не совсем понятна, однако она значительно упрощает итеративное применение метода, как в нашем случае.

В коде выше есть несколько интересных моментов, например — использование divisor'а при создании очередного ландшафта. Если честно, я и сам не понимаю, почему это нужно — просто создание ландшафта обычным способом (без divisor'а) создает ландшафт неправильного размера (что может быть багом, а может я просто плохо читал документацию). Данная поправка была получена эмпирически и до сих пор не подводила, поэтому я решил оставить ее как есть.

Вы также могли заметить, что внизу листинга имеются два подозрительных метода-хэлпера. На самом деле, это просто результат рефакторинга (так как я показываю листинги более-менее стабильной версии, которая прошла несколько рефакторингов, но все еще не идеальна). Эти методы используются и дальше, при применении локальных и глобальных деформаций. Из их названия несложно догадаться, что они делают.

Применение глобальной карты высот

Теперь, после того как ландшафт создан, настало самое время научить его применять «глобальную карту высот». Для этого в SuperTerrain предусмотрена


Согласен, выглядит эта пара методов не очень красиво, но я постараюсь все объяснить. Итак, название метода SetGlobalHeightmap говорит само за себя. Все, что он делает — итерируется по всем chunk'ам (которые здесь названы subterrain'ами) и применяет к ним именно тот кусочек карты высот, который соответствует его координатам. Здесь и используется злополучный SetHeights, производительность которого и заставляет нас идти на все эти извращения. Как видно из кода, константа SuperTerrainHeightmapResolution не учитывает отличие на 1 разрешения карты высот от степени двойки (чье существование обосновывается в прошлой секции). И пусть вас не смутит ее название — эта константа хранит разрешение карты высот для chunk'а, а не для всего SuperTerrain. Поскольку код SuperTerrain активно использует различные константы, я сразу покажу вам класс GameplayConstants. Возможно, так будет понятнее, что же все таки происходит. Я убрал из данного класса все не относящееся к SuperTerrain.


Что касается метода GetSubHeightMap, то это просто очередной хэлпер, копирующий часть часть переданной матрицы в матрицу-минор. Это нужно потому, что SetHeights не может применить часть матрицы. Это ограничение вызывает целую кучу лишних выделений памяти, но с ним ничего нельзя поделать. К сожалению, разработчики Unity не предусмотрели сценарий изменения ландшафта в реальном времени.

Метод GetSubHeightMap используется и дальше при применении локальных деформаций, но об этом позже.

Применение локальных деформаций

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


Несложно догадаться, что наследники этого класса реализуют абстрактный метод Generate(), где и описывают логику по созданию соответствующего heightmap'а для деформации. Также TerrainDeformation содержит информацию о том, как именно она должна применяться к текущему ландшафту — это определяет виртуальный метод ApplyToPoint. По-умолчанию он определяет деформацию как аддитивную, но перегрузив метод можно добиться более сложных методов комбинирования двух высот. Что касается разделения матрицы деформации на суб-матрицы и применение их к соответствующим chunk'ам, то этот код находится в классе SuperTerrain и выделен в


Как вы уже наверное догадались, единственный public метод, который есть в листинге — и есть самый главный. Метод ApplyDeformation() позволяет применить указанную деформацию к ландшафту в заданных координатах. Первым делом при его вызове происходит конвертация координат на ландшафте в координаты на карте высот (помните? Если размеры ландшафта отличаются от разрешения карты высот, то это нужно учесть). Вся работа по применению деформации происходит внутри девяти вызовов ApplyPartialHeightmap, которые применяют куски карты высот от деформации к соответствующим им chunk'ам. Как я уже говорил ранее, нам нужно именно девять частей, а не четыре чтобы учесть все возможные граничные и угловые случаи:

Создание динамически изменяемого ландшафта для RTS на Unity3D

Именно этим делением и занимаются методы GetXXXSubmap() — получением необходимых миноров деформации на основании данных о положении деформации и границах различных chunk'ов. Каждый из методов возвращает null, в случае если деформация не имеет влияния на соответствующий chunk и метод по применению этих самых миноров (ApplyPartialHeightmap()) ничего не делает, если на вход ему приходит null.

Результаты и выводы


Получившийся механизм конечно далек от идеала, но он уже функционален и позволяет регулировать важные параметры ландшафта, чтобы добиться кое-какой гибкости в плане настроек производительности. Среди основных потенциальных улучшений можно отметить следующие:


  • Тяжелую работу выполнять в отдельном процессе, чтобы снизить влияние на framerate в особенно интенсивных сценах.

  • Оптимизировать разбиение на миноры, избавившись от выделения памяти каждый раз, посредством, например, кеширования. Пока-что сложно представить как можно будет кэшировать что-то подобное. Для начала можно ограничиться самыми частыми кейсами — небольшая деформация прямо по середине chunk'а.

  • Добавить возможность влиять не только на геометрию ландшафта, но и на его текстуру — деформации с изменением splat-map'ов.

  • Оптимизации для применения нескольких дфеормаций за один кадр. Например, накапливать деформации для chunk'ов в каком-нибудь буфере и по окончанию обработки логики каким-либо образом их комбинировать и применять — получим один вызов SetHeights на chunk, даже если было несколько деформаций.




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

Для Windows

Для Linux



Источник: Хабрахабр

Категория: Программирование » Game Development

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

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *