Продолжаем
цикл статей, рассказывающих о процедурной генерации в Unity.
В прошлый раз мы применили Шум Перлина в качестве основы для генерации поверхности. Этот подход позволяет довольно легко и быстро получить приближённую к реальности карту высот, а добавление цветов в зависимости от высоты делает её похожей на реальную карту. Следующим шагом является использование карты высот для задания собственно самих высот и построение при их помощи объёмного пространства.
Предыдущая статья базировалась на серии видео от Sebastian Lague, который применил данный алгоритм для генерации трёхмерных поверхностей, но это не единственный доступный вариант визуализации. Очень популярными среди создателей игр являются такие виды отображения пространства как сайд-скролл, топ-даун и изометрический, которые с развитием трёхмерной графики не утратили свою актуальность и позволяют придать проекту уникальный стиль и атмосферу. В свою очередь, Unity постоянно работает над расширением доступных для девелоперов инструментов и работа с тайловыми картами является одним из таких относительно новых инструментов, который облегчает создание перечисленных выше видов представления пространства.
Тайловые карты
В 2017 году Unity добавила в движок возможность работы с 2d тайлами, что определённо упростило создание топ-даун и сайд-скроллеров. Годом позже также добавили шестиугольные тайлы и изометрические карты, применение которых вызывает большой интерес, ведь шестиугольники давно является основой для серии Civilization, а изометрический вид является базовым для большинства представителей жанра RPG =) Так что попробуем сделать первый шаг в сторону своей ролевой игры и перенести сгенерированную с помощью Шума Перлина карту в изометрическое пространство.
Цель
Отобразить сгенерированную Шумом Перлина карту высот при помощи изометрической тайловой карты Unity.
Подготовка
В качестве основы для реализации послужит генератор Шума Перлина из предыдущей статьи.
Для отображения поверхностей использовался ассет с тайлами от Devil’s Work.shop, из которых были выбраны девять подходящих под требования уровней высот.
Настройка работы с изометрическими картами довольно специфический процесс, который очень хорошо описала Alice Hinton-Jones в своей статье Isometric 2D Environments with Tilemap, так что не имеет смысла её переписывать.
Перечислим шаги, которые были проделаны для настройки данного проекта:
Выставить тип сортировки прозрачности Edit > Project Settings > Graphics > Camera Settings > Transparency Sort Mode - Custom Axis
Настроить оси сортировки прозрачности Edit > Project Settings > Graphics > Camera Settings > Transparency Sort Axis - X=0 Y=1 Z=-0.289 - Z зависит от размеров тайла и алгоритм её вычисления можно прочитать в статье Alice. Возьмите размер вашего объекта Grid по шкале Y, умножьте на 0.5 и вычтите 0.01
Импортировать ассеты тайлов как текстуры
Выставить им Pixels Per Unit в 863 - реальный размер тайла в текстуре
Выставить Pivot в 0.4921, 0.7452 - по центру верхней грани тайла, как описывается в статье от Alice
Разрешить Чтение/запись текстуры в тестовых целях
Создать объект Isometric Z as Y tilemap
Выставить в объекте Grid Y Cell size в 0.577 - зависит от размеров тайла, правила вычисления в статье Alice. Для определения корректного значения шкалы Y возьмите высоту основания(или верхней грани) ваших тайлов и разделите их на ширину
Создать изометрическую палитру тайлов с Y Cell Size в компоненте Grid также равным 0.577
Изменить настройку батчинга в компоненте Tilemap Renderer > Mode на Individual. Эта настройка позволит обойти баг расчёта перекрытия тайлов друг с другом, в случае, если текстуры тайлов находятся в разных атласах. Для использования опции Chunk необходимо, чтобы текстуры тайлов были запечены в один атлас, что можно проделать на стадии подготовки продакшн билда.
Шум Перлина
Генератор шума из прошлой статьи не требует каких-либо доработок и полностью перекочевал из прошлой статьи
usingUnityEngine;publicstaticclassNoiseMapGenerator{publicstaticfloat[]GenerateNoiseMap(intwidth,intheight,intseed,floatscale,intoctaves,floatpersistence,floatlacunarity,Vector2offset){// Массив данных о вершинах, одномерный вид поможет избавиться от лишних циклов впоследствии
float[]noiseMap=newfloat[width*height];// Порождающий элемент
System.Randomrand=newSystem.Random(seed);// Сдвиг октав, чтобы при наложении друг на друга получить более интересную картинку
Vector2[]octavesOffset=newVector2[octaves];for(inti=0;i<octaves;i++){// Учитываем внешний сдвиг положения
floatxOffset=rand.Next(-100000,100000)+offset.x;floatyOffset=rand.Next(-100000,100000)+offset.y;octavesOffset[i]=newVector2(xOffset/width,yOffset/height);}if(scale<0){scale=0.0001f;}// Учитываем половину ширины и высоты, для более визуально приятного изменения масштаба
floathalfWidth=width/2f;floathalfHeight=height/2f;// Генерируем точки на карте высот
for(inty=0;y<height;y++){for(intx=0;x<width;x++){// Задаём значения для первой октавы
floatamplitude=1;floatfrequency=1;floatnoiseHeight=0;floatsuperpositionCompensation=0;// Обработка наложения октав
for(inti=0;i<octaves;i++){// Рассчитываем координаты для получения значения из Шума Перлина
floatxResult=(x-halfWidth)/scale*frequency+octavesOffset[i].x*frequency;floatyResult=(y-halfHeight)/scale*frequency+octavesOffset[i].y*frequency;// Получение высоты из ГСПЧ
floatgeneratedValue=Mathf.PerlinNoise(xResult,yResult);// Наложение октав
noiseHeight+=generatedValue*amplitude;// Компенсируем наложение октав, чтобы остаться в границах диапазона [0,1]
noiseHeight-=superpositionCompensation;// Расчёт амплитуды, частоты и компенсации для следующей октавы
amplitude*=persistence;frequency*=lacunarity;superpositionCompensation=amplitude/2;}// Сохраняем точку для карты высот
// Из-за наложения октав есть вероятность выхода за границы диапазона [0,1]
noiseMap[y*width+x]=Mathf.Clamp01(noiseHeight);}}returnnoiseMap;}}
Предпросмотр
В тестовых целях из предыдущей статьи также была перенесена часть рендеринга шума в цветную текстуру. Во избежание заполнения шкалы высот вручную добавлено её автоматическое заполнение из массива тайлов.
NoiseMapEditorGenerate - добавление кнопки генерации тестовой текстуры в редактор объекта отвечающего за создание тайловой карты(подробнее о расширении редактора Unity можно почитать в нашей статье):
usingUnityEditor;usingUnityEngine;[CustomEditor(typeof(TileMapHandler))]publicclassNoiseMapEditorGenerate:Editor{publicoverridevoidOnInspectorGUI(){// Получение компонента редактора
TileMapHandlernoiseMap=(TileMapHandler)target;// Отрисовка стандартного редактора с перегенерацией карты в случае изменения параметров
if(DrawDefaultInspector()){noiseMap.GenerateMap();}// Кнопка перегенерации карты
if(GUILayout.Button("Generate")){noiseMap.GenerateMap();}}}
NoiseMapRenderer - класс отвечающий за создание цветной текстуры из Шума Перлина, который позволяет легко настроить параметры и протестировать результаты работы генератора без перехода в Play Mode:
usingSystem;usingSystem.Collections.Generic;usingUnityEngine;publicclassNoiseMapRenderer:MonoBehaviour{ [SerializeField]publicSpriteRendererspriteRenderer=null;// Структура с зависимостью цвета пикселя от высоты карты
[Serializable]publicstructTerrainLevel{publicfloatheight;publicColorcolor;} [SerializeField]publicList<TerrainLevel>terrainLevel=newList<TerrainLevel>();// Создаём и отрисовываем текстуру на основе шума
publicvoidRenderMap(intwidth,intheight,float[]noiseMap){Texture2Dtexture=newTexture2D(width,height);texture.wrapMode=TextureWrapMode.Clamp;texture.filterMode=FilterMode.Point;texture.SetPixels(GenerateColorMap(noiseMap));texture.Apply();spriteRenderer.sprite=Sprite.Create(texture,newRect(0.0f,0.0f,texture.width,texture.height),newVector2(0.5f,0.5f),100.0f);}// Конвертируем уровень шума в цвет для отрисовки текстуры
privateColor[]GenerateColorMap(float[]noiseMap){Color[]colorMap=newColor[noiseMap.Length];for(inti=0;i<noiseMap.Length;i++){// Базовым является цвет с наибольшим уровнем
colorMap[i]=terrainLevel[terrainLevel.Count-1].color;foreach(varlevelinterrainLevel){// Выбираем цвет в зависимости от уровня шума
if(noiseMap[i]<level.height){colorMap[i]=level.color;break;}}}returncolorMap;}}
Генерация изометрии
Для изометрической карты был выбран способ выставления высоты через Z компоненту, что позволило избежать создания дополнительных объектов тайловых карт, с целью дифференциации высоты. По сравнению с предыдущей статьёй шкала высот была равномерно распределена, что также дало неплохой, с визуальной точки зрения, результат и сократило время на заполнение шкалы уровней поверхности.
usingSystem.Collections.Generic;usingUnityEngine;usingUnityEngine.Tilemaps;publicclassTileMapHandler:MonoBehaviour{// Объект карты
publicTilemaptilemap=null;// Список с тайлами
publicList<Tile>tileList=newList<Tile>();// Входные данные для генератора шума
[SerializeField]publicintwidth; [SerializeField]publicintheight; [SerializeField]publicfloatscale; [SerializeField]publicintoctaves; [SerializeField]publicfloatpersistence; [SerializeField]publicfloatlacunarity; [SerializeField]publicintseed; [SerializeField]publicVector2offset;voidStart(){// Скрываем объект с тестовой текстурой
NoiseMapRenderermapRenderer=FindObjectOfType<NoiseMapRenderer>();mapRenderer.gameObject.SetActive(false);// Генерируем карту высот
float[]noiseMap=NoiseMapGenerator.GenerateNoiseMap(width,height,seed,scale,octaves,persistence,lacunarity,offset);// Создаём тайлы
for(inty=0;y<width;y++){for(intx=0;x<height;x++){// Уровень шума для текущего тайла
floatnoiseHeight=noiseMap[width*y+x];// Уровни генерируемой поверхности равномерно распределены по шкале шума
// "Растягиваем" шкалу шума до размеров массива тайлов
floatcolorHeight=noiseHeight*tileList.Count;// Выбираем тайл ниже получившегося значения
intcolorIndex=Mathf.FloorToInt(colorHeight);// Учитываем адресацию в массивах для максимальных значений шума
if(colorIndex==tileList.Count){colorIndex=tileList.Count-1;}// Ассеты тайлов позволяют использовать высоту в 2z
// Поэтом "растягиваем" шкалу шума больше чем с цветом в 2 раза
floattileHeight=noiseHeight*tileList.Count*2;inttileHeightIndex=Mathf.FloorToInt(tileHeight);// Сдвигаем полученную высоту, чтобы выровнять тайлы с водой и первым уровнем песка
tileHeightIndex-=4;if(tileHeightIndex<0){tileHeightIndex=0;}// Берём ассет тайла в зависимости от преобразованного уровня шума
Tiletile=tileList[colorIndex];// Устанавливаем высоту тайла в зависимости от преобразованного уровня шума
Vector3Intp=newVector3Int(x-width/2,y-height/2,tileHeightIndex);tilemap.SetTile(p,tile);}}}// Функция для генерации тестовой текстуры с параметрами, заданными для генератора шума
// Используется из расширения редактора NoiseMapEditorGenerate
publicvoidGenerateMap(){// Генерируем карту высот
float[]noiseMap=NoiseMapGenerator.GenerateNoiseMap(width,height,seed,scale,octaves,persistence,lacunarity,offset);// В зависимости от заполнения массива с ассетами тайлов генерируем равномерно распределённую зависимость цвета от высоты шума
List<NoiseMapRenderer.TerrainLevel>tl=newList<NoiseMapRenderer.TerrainLevel>();// Цвет определяется по верхней границе диапазона, поэтому делим шкалу на равные отрезки и сдвигаем вверх
floatheightOffset=1.0f/tileList.Count;for(inti=0;i<tileList.Count;i++){// Цвет берём из текстуры ассета тайла
Colorcolor=tileList[i].sprite.texture.GetPixel(tileList[i].sprite.texture.width/2,tileList[i].sprite.texture.height/2);// Создаём новый уровень цвет-шум
NoiseMapRenderer.TerrainLevellev=newNoiseMapRenderer.TerrainLevel();lev.color=color;// Преобразуем индекс в положение на шкале с диапазоном [0,1] и сдвигаем вверх
lev.height=Mathf.InverseLerp(0,tileList.Count,i)+heightOffset;// Сохраняем новый уровень цвет-шум
tl.Add(lev);}// Применяем новую шкалу цвет-шум и генерируем на её основе текстуру из заданных параметров
NoiseMapRenderermapRenderer=FindObjectOfType<NoiseMapRenderer>();mapRenderer.terrainLevel=tl;mapRenderer.RenderMap(width,height,noiseMap);}}
Результат
Тестовая текстура сгенерированной карты выглядит так:
Сгенерированная из этих же координат изометрическая карта:
Для придания большей правдоподобности, все уровни воды и первый уровень песка выровнены между собой.
Заключение
Базовое использование тайловых карт не представляет из себя большой проблемы. Но не стоит забывать, что для приведения карты к визуально приемлемому уровню потребуется очень много работы с ассетами. Да и работа с самими картами в нашем примере далека от завершения. Необходимо добавить возможность увеличения высотности карты, что потребует заполнения свободного пространства при перепадах высот больше 2z. Для свободного перемещения необходимо добавить систему блочной загрузки карты. Добавление динамических объектов потребует создания системы перемещения с учётом перепадов высот, возможно, с использованием стандартных коллайдеров. А для визуального улучшения также необходимо проработать механизм стыка тайлов с разными типами поверхностей. Всеми этими вопросами мы займёмся в следующих статьях
цикла о процедурной генерации. Пока! =)