В этом цикле статей мы научимся создавать процедурно генерируемые карты мира с помощью Unity и C#. Цикл будет состоять из четырех статей.
Содержание
Часть 1 (эта статья):
Введение
Генерирование шума
Начало работы
Генерирование карты высот
Часть 2:
Свертывание карты на одной оси
Свертывание карты на обеих осях
Поиск соседних элементов
Битовые маски
Заливка
Часть 3:
Генерирование тепловой карты
Генерирование карты влажности
Генерирование рек
Часть 4:
Генерирование биомов
Генерирование сферических карт
Введение
В этих обучающих статьях мы создадим процедурно генерируемые карты, похожие на такие:
Здесь представлены следующие карты:
- тепловая карта (левая верхняя)
- карта высот (правая верхняя)
- карта влажности (правая нижняя)
- карта биомов (левая нижняя)
В следующих статьях этой серии мы узнаем, как управлять данными этих карт. Также мы рассмотрим способ проецирования карт на сферические поверхности.
Генерирование шума
В Интернете есть множество различных генераторов шума, большинство из них имеют открытые исходники, поэтому здесь не нужно изобретать велосипед. Я позаимствовал портированную версию библиотеки Accidental Noise.
Портирование на C# выполнено Nikolaj Mariager.
Для правильной работы в Unity в портированную версию внесены незначительные изменения.
Вы можете использовать любой понравившийся генератор шума. Все техники, перечисленные в статье, могут применяться к другим источникам шума.
Начало работы
Сначала нам необходимо создать контейнер для хранения данных, которые мы будем генерировать.
Начнем с создания класса MapData. Переменные Min и Max нужны для отслеживания нижнего и верхнего пределов генерируемых значений.
public class MapData {
public float[,] Data;
public float Min { get; set; }
public float Max { get; set; }
public MapData(int width, int height)
{
Data = new float[width, height];
Min = float.MaxValue;
Max = float.MinValue;
}
}
Также мы создадим класс Tile, который будет позже использоваться для создания игровых объектов Unity из генерируемых данных.
public class Tile
{
public float HeightValue { get; set; }
public int X, Y;
public Tile()
{
}
}
Чтобы посмотреть, что происходит, нам необходимо графическое представление данных. Для этого мы создадим новый класс TextureGenerator.
Пока этот класс будет создавать черно-белое отображение наших данных.
using UnityEngine;
public static class TextureGenerator {
public static Texture2D GetTexture(int width, int height, Tile[,] tiles)
{
var texture = new Texture2D(width, height);
var pixels = new Color[width * height];
for (var x = 0; x < width; x++)
{
for (var y = 0; y < height; y++)
{
float value = tiles[x, y].HeightValue;
//Set color range, 0 = black, 1 = white
pixels[x + y * width] = Color.Lerp (Color.black, Color.white, value);
}
}
texture.SetPixels(pixels);
texture.wrapMode = TextureWrapMode.Clamp;
texture.Apply();
return texture;
}
}
Скоро мы расширим этот класс.
Генерирование карты высот
Я решил, что карты будут фиксированного размера, поэтому нужно указать ширину (Width) и высоту (Height) карты. Также нам понадобятся настраиваемые параметры для генератора шума.
Мы сделаем эти данные отображаемыми в Unity Inspector, чтобы настройка карт была намного проще.
Класс Generator инициализирует модуль Noise, генерирует данные карты высот, создает массив тайлов, а затем генерирует текстурное представление этих данных.
Вот код с комментариями:
using UnityEngine;
using AccidentalNoise;
public class Generator : MonoBehaviour {
// Настраиваемые переменные для Unity Inspector
[SerializeField]
int Width = 256;
[SerializeField]
int Height = 256;
[SerializeField]
int TerrainOctaves = 6;
[SerializeField]
double TerrainFrequency = 1.25;
// Модуль генератора шума
ImplicitFractal HeightMap;
// Данные карты высот
MapData HeightData;
// Конечные объекты
Tile[,] Tiles;
// Вывод нашей текстуры (компонент unity)
MeshRenderer HeightMapRenderer;
void Start()
{
// Получаем меш, в который будут рендериться выходные данные
HeightMapRenderer = transform.Find ("HeightTexture").GetComponent ();
// Инициализируем генератор
Initialize ();
// Создаем карту высот
GetData (HeightMap, ref HeightData);
// Создаем конечные объекты на основании наших данных
LoadTiles();
// Рендерим текстурное представление нашей карты
HeightMapRenderer.materials[0].mainTexture = TextureGenerator.GetTexture (Width, Height, Tiles);
}
private void Initialize()
{
// Инициализируем генератор карты высот
HeightMap = new ImplicitFractal (FractalType.MULTI,
BasisType.SIMPLEX,
InterpolationType.QUINTIC,
TerrainOctaves,
TerrainFrequency,
UnityEngine.Random.Range (0, int.MaxValue));
}
// Извлекаем данные из модуля шума
private void GetData(ImplicitModuleBase module, ref MapData mapData)
{
mapData = new MapData (Width, Height);
// циклично проходим по каждой точке x,y - получаем значение высоты
for (var x = 0; x < Width; x++)
{
for (var y = 0; y < Height; y++)
{
//Сэмплируем шум с небольшими интервалами
float x1 = x / (float)Width;
float y1 = y / (float)Height;
float value = (float)HeightMap.Get (x1, y1);
//отслеживаем максимальные и минимальные найденные значения
if (value > mapData.Max) mapData.Max = value;
if (value < mapData.Min) mapData.Min = value;
mapData.Data[x,y] = value;
}
}
}
// Создаем массив тайлов из наших данных
private void LoadTiles()
{
Tiles = new Tile[Width, Height];
for (var x = 0; x < Width; x++)
{
for (var y = 0; y < Height; y++)
{
Tile t = new Tile();
t.X = x;
t.Y = y;
float value = HeightData.Data[x, y];
//нормализуем наше значение от 0 до 1
value = (value - HeightData.Min) / (HeightData.Max - HeightData.Min);
t.HeightValue = value;
Tiles[x,y] = t;
}
}
}
}
После запуска кода мы получим следующую текстуру:
Выглядит пока не очень интересно, но начало положено. У нас есть массив данных, содержащий значения от 0 до 1, с очень любопытным рисунком.
Теперь нам нужно придать значимости нашим данным. Например, пусть все, что меньше 0,4, будет считаться водой. Мы можем изменить следующее в нашем TextureGenerator, назначив все значения ниже 0,4 синими, а выше — белыми:
if (value < 0.4f)
pixels[x + y * width] = Color.blue;
else
pixels[x + y * width] = Color.white;
После этого мы получил следующее конечное изображение:
У нас уже что-то получается. Появляются фигуры, соответствующие этому простому правилу. Давайте сделаем следующий шаг.
Добавим других настраиваемых переменных в наш класс Generator. Они будут указывать на параметры, с которыми связаны значения высот.
float DeepWater = 0.2f;
float ShallowWater = 0.4f;
float Sand = 0.5f;
float Grass = 0.7f;
float Forest = 0.8f;
float Rock = 0.9f;
float Snow = 1;
Также добавим новые цвета в генератор текстур:
private static Color DeepColor = new Color(0, 0, 0.5f, 1);
private static Color ShallowColor = new Color(25/255f, 25/255f, 150/255f, 1);
private static Color SandColor = new Color(240 / 255f, 240 / 255f, 64 / 255f, 1);
private static Color GrassColor = new Color(50 / 255f, 220 / 255f, 20 / 255f, 1);
private static Color ForestColor = new Color(16 / 255f, 160 / 255f, 0, 1);
private static Color RockColor = new Color(0.5f, 0.5f, 0.5f, 1);
private static Color SnowColor = new Color(1, 1, 1, 1);
Добавив таким образом новые правила, мы получим следующие результаты:
У нас получилась интересная карта вершин с представляющей ее текстурой.
Исходники кода первой части вы можете скачать отсюда: World Generator Part1.
Вторая часть.