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

Tulenber 22 May, 2020 ⸱ Intermediate ⸱ 15 min ⸱ 2019.3.14f1 ⸱

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

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

Цель

Сгенерировать при помощи Шума Перлина данные для сектора и объединить несколько секторов в общую карту перекрывающую камеру.

Изометрическая проекция

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

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

Самым простым средством представления пространства будет его отображение на плоскости, для примера координаты тайлов в двухмерном пространстве:
2D grid

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

Сектор

Сектор в нашем случае представляет из себя группу тайлов, с одинаковой длиной и шириной (высоту, как мы договорились ранее опускаем). Сразу отмечу, что сектора ведут себя в пространстве так же, как и отдельные тайлы, так что можно абстрагироваться от того, с какими объектами мы в данный момент имеем дело. В основном мы будем оперировать именно секторами.

Сектор в двухмерном пространстве:
2D sector

Сектор в изометрическом пространстве:
Isometric sector

Отображение изометрического пространства на экране

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

Наиболее эффективное размещение секторов выглядит таким образом:
Isometric screen

Количество столбцов можно вычислить по формуле int sectorCountX = Mathf.CeilToInt(cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1; увеличение на единицу закрывает крайние значения.
Соответственно, формула для количества строк int sectorCountY = 2 * (Mathf.CeilToInt(cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1); где увеличение на единицу также закрывает крайние значения, а удвоенный размер берётся из-за учёта сдвига строк относительно друг друга лишь на половину высоты тайла.

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

Обход пространства

Вишенкой на торте будет алгоритм обхода этого “эффективного” размещения секторов.

Для смещения по строкам используется последовательное увеличение координат X и Y. Для смещения по столбцам, в соответствии с изометрической координатной сеткой, необходимо увеличивать X на единицу, а Y уменьшать.
Isometric screen fill

Реализация

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

  • Расчёт смещения для генерации Шума Перлина
  • Расчёт количества секторов, необходимых для заполнения экрана
  • Расчёт положения секторов в пространстве
  • Обход секторов для заполнения экрана
Рефакторинг генератора шума

Настройки генератора шума были вынесены в отдельную структуру, что упростит их передачу между различными объектами.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
using System;
using UnityEngine;

[Serializable]
public struct GeneratorSettings
{
    public float scale;
    
    public int octaves;
    public float persistence;
    public float lacunarity;

    public int seed;
    public Vector2 offset;
}

Соответственно в генератор шума был добавлен интерфейс для работы с настройками из такой структуры.

 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
using UnityEngine;

public static class NoiseMapGenerator
{
    public static float[] GenerateNoiseMap(GeneratorSettings settings, Vector2 offset)
    {
        // Учёт сдвига из настроек генератора
        Vector2 generationOffset = offset + settings.offset;
        // Размеры генерируемой зоны по умолчанию равны размеру сектора
        return GenerateNoiseMap(TileMapSector.SectorSizeInTiles, TileMapSector.SectorSizeInTiles, settings.seed, settings.scale, settings.octaves, settings.persistence, settings.lacunarity, generationOffset);
    }
    
    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;
    }
}

Рефакторинг тестового отображения сгенерированного пространства

Тестовый рендеринг отделён от основного класса генерирующего изометрические карты и фактически повторяет алгоритм генерации изометрических структур, но для двухмерной текстуры.

  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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
using System;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

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

    // Функция для генерации тестовой текстуры с параметрами, заданными для генератора шума
    // Используется из расширения редактора
    public void GenerateMap()
    {
        // Перевод настроек тайлов в структуру зависимости цвета пикселя от высоты
        TileMapHandler handler = FindObjectOfType<TileMapHandler>();
        List<Tile> tileList = handler.tileList;

        terrainLevel = new List<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);
            TerrainLevel lev = new TerrainLevel();
            lev.color = color;
            lev.height = Mathf.InverseLerp(0, tileList.Count, i) + heightOffset;
            terrainLevel.Add(lev);
        }

        // Рендеринг тестового отображения пространства для нашей камеры
        Vector2Int sectorSize = handler.GetScreenSizeInSectors();
        RenderMap(sectorSize.x, sectorSize.y, handler.generatorSettings);
    }

    // Создаём и отрисовываем текстуру на основе шума
    public void RenderMap(int sectorLength, int sectorWidth, GeneratorSettings settings)
    {
        // Удаление ранее сгенерированных текстур для режима редактора
        for (int i = transform.childCount; i > 0; --i)
            DestroyImmediate(transform.GetChild(0).gameObject);

        // Исходные параметры для обхода секторов
        int startX = -sectorLength;
        int startY = 0;
        int currentSectorX = startX;
        int currentSectorY = startY;
        bool increaseX = true;

        // Для оптимизации использования цикла при помощи остатка от деления сдвигаем индекс на единицу
        int lastSectorIndex = sectorLength * sectorWidth + 1;
        for (int sectorIndex = 1; sectorIndex < lastSectorIndex; sectorIndex++)
        {
            // Генерируем текущий сектор
            GenerateSector(new Vector2Int(currentSectorX, currentSectorY), settings);

            // Сдвигаемся на следующий столбец
            currentSectorX++;
            currentSectorY--;

            // Переход на следующую строку
            if (sectorIndex % sectorLength == 0)
            {
                // Переход к следующей строке с чередованием увеличения X и Y
                if (increaseX)
                {
                    currentSectorX = ++startX;
                    currentSectorY = startY;
                    increaseX = false;
                }
                else
                {
                    currentSectorX = startX;
                    currentSectorY = ++startY;
                    increaseX = true;
                }
            }
        }
    }

    private void GenerateSector(Vector2Int sectorCoordinates, GeneratorSettings settings)
    {
        // Расчёт сдвига сектора для генератора шума
        int sectorSize = TileMapSector.SectorSizeInTiles;
        float sectorOffsetX = sectorSize * sectorCoordinates.x * sectorSize / settings.scale;
        float sectorOffsetY = sectorSize * sectorCoordinates.y * sectorSize / settings.scale;
        Vector2 sectorOffset = new Vector2(sectorOffsetX, sectorOffsetY);
        // Генерация шума
        float[] noiseMap = NoiseMapGenerator.GenerateNoiseMap(settings, sectorOffset);

        // Генерация и расположение текстуры сектора
        GameObject sectorSprite = new GameObject();

        SpriteRenderer spriteRenderer = sectorSprite.AddComponent<SpriteRenderer>();
        Texture2D texture = new Texture2D(sectorSize, sectorSize)
        {
            wrapMode = TextureWrapMode.Clamp, 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);
        
        sectorSprite.transform.SetParent(transform, false);
        sectorSprite.name = "" + sectorCoordinates.x + "_" + sectorCoordinates.y;
        float positionOffsetMultiplier = sectorSize * transform.localScale.x / spriteRenderer.sprite.pixelsPerUnit;
        sectorSprite.transform.Translate(new Vector3(sectorCoordinates.x * positionOffsetMultiplier, sectorCoordinates.y * positionOffsetMultiplier), Space.Self);
    }

    // Конвертируем уровень шума в цвет для отрисовки текстуры
    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;
    }
}

Сектор

Отвечает за расположение тайлов на карте.

 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
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public static class TileMapSector
{
    public static int SectorSizeInTiles = 10;

    public static void GenerateSector(float[] noiseMap, List<Tile> tileList, Tilemap tileMap)
    {
        // Размеры сектора
        int length = SectorSizeInTiles;
        int width = SectorSizeInTiles;

        // Очистка предыдущих значений
        tileMap.ClearAllTiles();

        // Обход всех тайлов сектора
        for (int y = 0; y < length; y++)
        {
            for (int x = 0; x < width; x++)
            {
                // Уровень шума для текущего тайла
                float noiseHeight = noiseMap[length*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 - length / 2, y - width / 2, tileHeightIndex);
                tileMap.SetTile(p, tile);
            }
        }
    }
}

Расчёт смещения для генерации Шума Перлина

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

 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
// Сдвиг октав для получения более интересной картинки при наложении их друг на друга
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);
}

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

// Генерация точки на карте высот
for (int y = 0; y < height; y++)
{
    for (int x = 0; x < width; x++)
    {
        // Обработка наложения октав
        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;
        }
    }
}

Соответственно для правильного расчёта смещения секторов применяется такая формула:

1
2
3
int sectorSize = TileMapSector.SectorSizeInTiles;
float sectorOffsetX = sectorSize * sector.x * sectorSize / generatorSettings.scale;
float sectorOffsetY = sectorSize * sector.y * sectorSize / generatorSettings.scale;

Расчёт количества секторов, необходимых для заполнения экрана

Как уже говорилось выше:

1
2
int sectorCountX = Mathf.CeilToInt(cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1;
int sectorCountY = 2 * (Mathf.CeilToInt(cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1);

Расчёт положения секторов в пространстве

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

1
2
3
4
5
var tileSize = grid.cellSize;
float positionOffsetMultiplierX = TileMapSector.SectorSizeInTiles * tileSize.x * 0.5f;
float positionOffsetMultiplierY = TileMapSector.SectorSizeInTiles * tileSize.y * 0.5f;
float positionX = currentSectorX * positionOffsetMultiplierX - currentSectorY * positionOffsetMultiplierX;
float positionY = currentSectorX * positionOffsetMultiplierY + currentSectorY * positionOffsetMultiplierY;

Код класса генерации карты полностью
  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
104
105
106
107
108
109
110
111
112
113
114
115
116
117
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;

public class TileMapHandler : MonoBehaviour
{
    // Префаб тайловой карты
    public GameObject tileMapPrefab = null;
    // Сетка для отображения сгенерированных тайловых карт
    public Grid grid = null;
    // Список тайлов
    public List<Tile> tileList = new List<Tile>();

    // Настройки генератора
    [SerializeField] public GeneratorSettings generatorSettings;

    // Список сгенерированных карт
    private readonly List<Transform> _tileMaps = new List<Transform>();

    // Start is called before the first frame update
    void Start()
    {
        // Скрытие тестовой текстуры
        NoiseMapRenderer mapRenderer = FindObjectOfType<NoiseMapRenderer>();
        mapRenderer.gameObject.SetActive(false);

        // Размер экрана в секторах
        Vector2Int screenSizeInSectors = GetScreenSizeInSectors();

        // Параметры для позиционирования секторов в пространстве
        var tileSize = grid.cellSize;
        float positionOffsetMultiplierX = TileMapSector.SectorSizeInTiles * tileSize.x * 0.5f;
        float positionOffsetMultiplierY = TileMapSector.SectorSizeInTiles * tileSize.y * 0.5f;

        // Параметры для обхода тайлов по столбцам-строкам
        int startX = -screenSizeInSectors.x;
        int startY = 0;
        int currentSectorX = startX;
        int currentSectorY = startY;
        bool increaseX = true;

        // Для оптимизации использования цикла при помощи остатка от деления сдвигаем индекс на единицу
        int lastSectorIndex = screenSizeInSectors.x * screenSizeInSectors.y + 1;
        for (int currentSectorIndex = 1; currentSectorIndex < lastSectorIndex; currentSectorIndex++)
        {
            // Объект сектора
            Transform sector = GetTileMapSector(new Vector2Int(currentSectorX, currentSectorY));
            sector.SetParent(grid.transform);

            // Рассчитываем положение сектора в пространстве
            float positionX = currentSectorX * positionOffsetMultiplierX - currentSectorY * positionOffsetMultiplierX;
            float positionY = currentSectorX * positionOffsetMultiplierY + currentSectorY * positionOffsetMultiplierY;
            sector.Translate(new Vector3(positionX, positionY, 0), Space.Self);
            sector.name = "" + currentSectorX + "," + currentSectorY;

            // Сохраняем сектор
            _tileMaps.Add(sector);

            // Переход на следующий столбец
            currentSectorX++;
            currentSectorY--;

            // Переход на следующую строку
            if (currentSectorIndex % screenSizeInSectors.x == 0)
            {
                if (increaseX)
                {
                    currentSectorX = ++startX;
                    currentSectorY = startY;
                    increaseX = false;
                }
                else
                {
                    currentSectorX = startX;
                    currentSectorY = ++startY;
                    increaseX = true;
                }
            }
        }
    }

    // Размер экрана в секторах
    public Vector2Int GetScreenSizeInSectors()
    {
        // Высота камеры
        float cameraHeight = Camera.main.orthographicSize * 2;
        // Ширина камеры равна высота камеры * соотношение сторон
        float screenAspect = Camera.main.aspect;
        float cameraWidth = cameraHeight * screenAspect;

        // Расчёт количества тайлов для заполнения экрана
        var cellSize = grid.cellSize;
        int sectorCountX = Mathf.CeilToInt(cameraWidth / (TileMapSector.SectorSizeInTiles * cellSize.x)) + 1;
        int sectorCountY = 2 * (Mathf.CeilToInt(cameraHeight / (TileMapSector.SectorSizeInTiles * cellSize.y)) + 1);
        
        return new Vector2Int(sectorCountX, sectorCountY);
    }

    // Генерация сектора
    private Transform GetTileMapSector(Vector2Int sector)
    {
        // Объект тайловой карты
        GameObject sectorGameObject = Instantiate(tileMapPrefab);

        // Расчёт смещения сектора для генератора шума
        int sectorSize = TileMapSector.SectorSizeInTiles;
        float sectorOffsetX = sectorSize * sector.x * sectorSize / generatorSettings.scale;
        float sectorOffsetY = sectorSize * sector.y * sectorSize / generatorSettings.scale;
        Vector2 sectorOffset = new Vector2(sectorOffsetX, sectorOffsetY);

        Tilemap sectorTileMap = sectorGameObject.GetComponent<Tilemap>();
        // Генерация сектора тайловой карты
        TileMapSector.GenerateSector(NoiseMapGenerator.GenerateNoiseMap(generatorSettings, sectorOffset), tileList, sectorTileMap);
        
        return sectorGameObject.transform;
    }
}

Результат

Тестовая текстура:
Test texture

Результат генерации изометрической карты, поделённой на сектора:
Result

Заключение

Это небольшой шаг для нашего примера процедурной генерации, но в нём решены такие ключевые задачи, как позиционирование области генерации Шума Перлина, базовые механизмы работы с изометрической системой координат и разделение пространства на сектора. Эти три задачи являются подготовкой к динамической загрузке секторов при перемещении и фактически созданию бесконечного пространства, но этим мы займёмся в следующей статье нашего цикла . Пока! =)



Privacy policyCookie policyTerms of service
Tulenber 2020