Генерация изометрической карты из Шума Перлина

Tulenber 15 May, 2020 ⸱ Intermediate ⸱ 10 min ⸱ 2019.3.13f1 ⸱

Продолжаем цикл статей, рассказывающих о процедурной генерации в Unity.

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

Предыдущая статья базировалась на серии видео от Sebastian Lague, который применил данный алгоритм для генерации трёхмерных поверхностей, но это не единственный доступный вариант визуализации. Очень популярными среди создателей игр являются такие виды отображения пространства как сайд-скролл, топ-даун и изометрический, которые с развитием трёхмерной графики не утратили свою актуальность и позволяют придать проекту уникальный стиль и атмосферу. В свою очередь, Unity постоянно работает над расширением доступных для девелоперов инструментов и работа с тайловыми картами является одним из таких относительно новых инструментов, который облегчает создание перечисленных выше видов представления пространства.

Тайловые карты

В 2017 году Unity добавила в движок возможность работы с 2d тайлами, что определённо упростило создание топ-даун и сайд-скроллеров. Годом позже также добавили шестиугольные тайлы и изометрические карты, применение которых вызывает большой интерес, ведь шестиугольники давно является основой для серии Civilization, а изометрический вид является базовым для большинства представителей жанра RPG =) Так что попробуем сделать первый шаг в сторону своей ролевой игры и перенести сгенерированную с помощью Шума Перлина карту в изометрическое пространство.

Цель

Отобразить сгенерированную Шумом Перлина карту высот при помощи изометрической тайловой карты Unity.

Подготовка

В качестве основы для реализации послужит генератор Шума Перлина из предыдущей статьи.

Для отображения поверхностей использовался ассет с тайлами от Devil’s Work.shop, из которых были выбраны девять подходящих под требования уровней высот.

Настройка работы с изометрическими картами довольно специфический процесс, который очень хорошо описала Alice Hinton-Jones в своей статье Isometric 2D Environments with Tilemap, так что не имеет смысла её переписывать.

Перечислим шаги, которые были проделаны для настройки данного проекта:

  1. Выставить тип сортировки прозрачности Edit > Project Settings > Graphics > Camera Settings > Transparency Sort Mode - Custom Axis
  2. Настроить оси сортировки прозрачности Edit > Project Settings > Graphics > Camera Settings > Transparency Sort Axis - X=0 Y=1 Z=-0.289 - Z зависит от размеров тайла и алгоритм её вычисления можно прочитать в статье Alice. Возьмите размер вашего объекта Grid по шкале Y, умножьте на 0.5 и вычтите 0.01
  3. Импортировать ассеты тайлов как текстуры
  4. Выставить им Pixels Per Unit в 863 - реальный размер тайла в текстуре
  5. Выставить Pivot в 0.4921, 0.7452 - по центру верхней грани тайла, как описывается в статье от Alice
  6. Разрешить Чтение/запись текстуры в тестовых целях
  7. Создать объект Isometric Z as Y tilemap
  8. Выставить в объекте Grid Y Cell size в 0.577 - зависит от размеров тайла, правила вычисления в статье Alice. Для определения корректного значения шкалы Y возьмите высоту основания(или верхней грани) ваших тайлов и разделите их на ширину
  9. Создать изометрическую палитру тайлов с Y Cell Size в компоненте Grid также равным 0.577
  10. Изменить настройку батчинга в компоненте Tilemap Renderer > Mode на Individual. Эта настройка позволит обойти баг расчёта перекрытия тайлов друг с другом, в случае, если текстуры тайлов находятся в разных атласах. Для использования опции Chunk необходимо, чтобы текстуры тайлов были запечены в один атлас, что можно проделать на стадии подготовки продакшн билда.

Шум Перлина

Генератор шума из прошлой статьи не требует каких-либо доработок и полностью перекочевал из прошлой статьи

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
using UnityEngine;

public static class NoiseMapGenerator
{
    public static float[] GenerateNoiseMap(int width, int height, int seed, float scale, int octaves, float persistence, float lacunarity, Vector2 offset)
    {
        // Массив данных о вершинах, одномерный вид поможет избавиться от лишних циклов впоследствии
        float[] noiseMap = new float[width*height];

        // Порождающий элемент
        System.Random rand = new System.Random(seed);

        // Сдвиг октав, чтобы при наложении друг на друга получить более интересную картинку
        Vector2[] octavesOffset = new Vector2[octaves];
        for (int i = 0; i < octaves; i++)
        {
            // Учитываем внешний сдвиг положения
            float xOffset = rand.Next(-100000, 100000) + offset.x;
            float yOffset = rand.Next(-100000, 100000) + offset.y;
            octavesOffset[i] = new Vector2(xOffset / width, yOffset / height);
        }

        if (scale < 0)
        {
            scale = 0.0001f;
        }

        // Учитываем половину ширины и высоты, для более визуально приятного изменения масштаба
        float halfWidth = width / 2f;
        float halfHeight = height / 2f;

        // Генерируем точки на карте высот
        for (int y = 0; y < height; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Задаём значения для первой октавы
                float amplitude = 1;
                float frequency = 1;
                float noiseHeight = 0;
                float superpositionCompensation = 0;

                // Обработка наложения октав
                for (int i = 0; i < octaves; i++)
                {
                    // Рассчитываем координаты для получения значения из Шума Перлина
                    float xResult = (x - halfWidth) / scale * frequency + octavesOffset[i].x * frequency;
                    float yResult = (y - halfHeight) / scale * frequency + octavesOffset[i].y * frequency;

                    // Получение высоты из ГСПЧ
                    float generatedValue = 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);
            }
        }

        return noiseMap;
    }
}

Предпросмотр

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

NoiseMapEditorGenerate - добавление кнопки генерации тестовой текстуры в редактор объекта отвечающего за создание тайловой карты(подробнее о расширении редактора Unity можно почитать в нашей статье):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
using UnityEditor;
using UnityEngine;

[CustomEditor(typeof(TileMapHandler))]
public class NoiseMapEditorGenerate : Editor
{
    public override void OnInspectorGUI()
    {
        // Получение компонента редактора
        TileMapHandler noiseMap = (TileMapHandler)target;

        // Отрисовка стандартного редактора с перегенерацией карты в случае изменения параметров
        if (DrawDefaultInspector())
        {
            noiseMap.GenerateMap();
        }

        // Кнопка перегенерации карты
        if (GUILayout.Button("Generate"))
        {
            noiseMap.GenerateMap();
        }
    }
}

NoiseMapRenderer - класс отвечающий за создание цветной текстуры из Шума Перлина, который позволяет легко настроить параметры и протестировать результаты работы генератора без перехода в Play Mode:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.Collections.Generic;
using UnityEngine;

public class NoiseMapRenderer : MonoBehaviour
{
    [SerializeField] public SpriteRenderer spriteRenderer = null;

    // Структура с зависимостью цвета пикселя от высоты карты
    [Serializable]
    public struct TerrainLevel
    {
        public float height;
        public Color color;
    }
    [SerializeField] public List<TerrainLevel> terrainLevel = new List<TerrainLevel>();

    // Создаём и отрисовываем текстуру на основе шума
    public void RenderMap(int width, int height, float[] noiseMap)
    {
        Texture2D texture = new Texture2D(width, height);
        texture.wrapMode = TextureWrapMode.Clamp;
        texture.filterMode = FilterMode.Point;
        texture.SetPixels(GenerateColorMap(noiseMap));
        texture.Apply();

        spriteRenderer.sprite = Sprite.Create(texture, new Rect(0.0f, 0.0f, texture.width, texture.height), new Vector2(0.5f, 0.5f), 100.0f);
    }

    // Конвертируем уровень шума в цвет для отрисовки текстуры
    private Color[] GenerateColorMap(float[] noiseMap)
    {
        Color[] colorMap = new Color[noiseMap.Length];
        for (int i = 0; i < noiseMap.Length; i++)
        {
            // Базовым является цвет с наибольшим уровнем
            colorMap[i] = terrainLevel[terrainLevel.Count-1].color;
            foreach (var level in terrainLevel)
            {
                // Выбираем цвет в зависимости от уровня шума
                if (noiseMap[i] < level.height)
                {
                    colorMap[i] = level.color;
                    break;
                }
            }
        }

        return colorMap;
    }
}

Генерация изометрии

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

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class TileMapHandler : MonoBehaviour
{
    // Объект карты
    public Tilemap tilemap = null;
    // Список с тайлами
    public List<Tile> tileList = new List<Tile>();
    
    // Входные данные для генератора шума
    [SerializeField] public int width;
    [SerializeField] public int height;
    [SerializeField] public float scale;

    [SerializeField] public int octaves;
    [SerializeField] public float persistence;
    [SerializeField] public float lacunarity;

    [SerializeField] public int seed;
    [SerializeField] public Vector2 offset;

    void Start()
    {
        // Скрываем объект с тестовой текстурой
        NoiseMapRenderer mapRenderer = FindObjectOfType<NoiseMapRenderer>();
        mapRenderer.gameObject.SetActive(false);

        // Генерируем карту высот
        float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(width, height, seed, scale, octaves, persistence, lacunarity, offset);

        // Создаём тайлы
        for (int y = 0; y < width; y++)
        {
            for (int x = 0; x < height; x++)
            {
                // Уровень шума для текущего тайла
                float noiseHeight = noiseMap[width*y + x];

                // Уровни генерируемой поверхности равномерно распределены по шкале шума
                // "Растягиваем" шкалу шума до размеров массива тайлов
                float colorHeight = noiseHeight * tileList.Count;
                // Выбираем тайл ниже получившегося значения
                int colorIndex = Mathf.FloorToInt(colorHeight);
                // Учитываем адресацию в массивах для максимальных значений шума
                if (colorIndex == tileList.Count)
                {
                    colorIndex = tileList.Count-1;
                }

                // Ассеты тайлов позволяют использовать высоту в 2z
                // Поэтом "растягиваем" шкалу шума больше чем с цветом в 2 раза
                float tileHeight = noiseHeight * tileList.Count * 2;
                int tileHeightIndex = Mathf.FloorToInt(tileHeight);
                
                // Сдвигаем полученную высоту, чтобы выровнять тайлы с водой и первым уровнем песка
                tileHeightIndex -= 4;
                if (tileHeightIndex < 0)
                {
                    tileHeightIndex = 0;
                }

                // Берём ассет тайла в зависимости от преобразованного уровня шума
                Tile tile = tileList[colorIndex];

                // Устанавливаем высоту тайла в зависимости от преобразованного уровня шума
                Vector3Int p = new Vector3Int(x - width / 2, y - height / 2, tileHeightIndex);
                tilemap.SetTile(p, tile);
            }
        }
    }

    // Функция для генерации тестовой текстуры с параметрами, заданными для генератора шума
    // Используется из расширения редактора NoiseMapEditorGenerate
    public void GenerateMap()
    {
        // Генерируем карту высот
        float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(width, height, seed, scale, octaves, persistence, lacunarity, offset);

        // В зависимости от заполнения массива с ассетами тайлов генерируем равномерно распределённую зависимость цвета от высоты шума
        List<NoiseMapRenderer.TerrainLevel> tl = new List<NoiseMapRenderer.TerrainLevel>();
        // Цвет определяется по верхней границе диапазона, поэтому делим шкалу на равные отрезки и сдвигаем вверх
        float heightOffset = 1.0f / tileList.Count;
        for (int i = 0; i < tileList.Count; i++)
        {
            // Цвет берём из текстуры ассета тайла
            Color color = tileList[i].sprite.texture.GetPixel(tileList[i].sprite.texture.width / 2, tileList[i].sprite.texture.height / 2);
            // Создаём новый уровень цвет-шум
            NoiseMapRenderer.TerrainLevel lev = new NoiseMapRenderer.TerrainLevel();
            lev.color = color;
            // Преобразуем индекс в положение на шкале с диапазоном [0,1] и сдвигаем вверх
            lev.height = Mathf.InverseLerp(0, tileList.Count, i) + heightOffset;
            // Сохраняем новый уровень цвет-шум
            tl.Add(lev);
        }

        // Применяем новую шкалу цвет-шум и генерируем на её основе текстуру из заданных параметров
        NoiseMapRenderer mapRenderer = FindObjectOfType<NoiseMapRenderer>();
        mapRenderer.terrainLevel = tl;
        mapRenderer.RenderMap(width, height, noiseMap);
    }
}

Результат

Тестовая текстура сгенерированной карты выглядит так:
Open Test Runner

Сгенерированная из этих же координат изометрическая карта:
Open Test Runner

Open Test Runner

Open Test Runner

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

Заключение

Базовое использование тайловых карт не представляет из себя большой проблемы. Но не стоит забывать, что для приведения карты к визуально приемлемому уровню потребуется очень много работы с ассетами. Да и работа с самими картами в нашем примере далека от завершения. Необходимо добавить возможность увеличения высотности карты, что потребует заполнения свободного пространства при перепадах высот больше 2z. Для свободного перемещения необходимо добавить систему блочной загрузки карты. Добавление динамических объектов потребует создания системы перемещения с учётом перепадов высот, возможно, с использованием стандартных коллайдеров. А для визуального улучшения также необходимо проработать механизм стыка тайлов с разными типами поверхностей. Всеми этими вопросами мы займёмся в следующих статьях цикла о процедурной генерации. Пока! =)



Privacy policyCookie policyTerms of service
Tulenber 2020