Это вторая статья из цикла о процедурно генерируемых с помощью Unity и C# картах мира. Цикл будет состоять из четырех статей.
Содержание
Часть 1:
Введение
Генерирование шума
Начало работы
Генерирование карты высот
Часть 2 (эта статья):
Свертывание карты на одной оси
Свертывание карты на обеих осях
Поиск соседних элементов
Битовые маски
Заливка
Часть 3:
Генерирование тепловой карты
Генерирование карты влажности
Генерирование рек
Часть 4:
Генерирование биомов
Генерирование сферических карт
Свертывание карты по одной оси
(От переводчика: я не уверен, что wrapping в контексте математической терминологии переводится как «свертывание». Если кто-то знает более подходящий термин, напишите, исправлю.)
В первой части цикла мы настроили небольшой фреймворк, который поможет нам создавать карты. Карту высот, которую мы создали ранее, невозможно склеить встык.
Так произошло, потому что мы создали двухмерные данные шума, которые не могут обеспечить нам необходимое. Для бесшовного свертывания нашего мира мы добавим новое измерение в наш генератор шума.
С помощью трехмерного шума мы можем создать данные в круговом расположении, при этом конечные двухмерные данные можно будет свернуть по одной оси. Созданные данные похожи на цилиндр в трехмерном пространстве.
Представьте, что мы разрезали этот цилиндр и расстелили на плоскости. Именно этим мы и займемся. Края, по которым мы разрезали цилиндр, при склейке не будут иметь видимого шва.
Чтобы сделать это, необходимо изменить функцию GetData в классе Generator.
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 = 0, x2 = 1;
float y1 = 0, y2 = 1;
float dx = x2 - x1;
float dy = y2 - y1;
//Сэмплируем шум с небольшими интервалами
float s = x / (float)Width;
float t = y / (float)Height;
// Вычисляем трехмерные координаты
float nx = x1 + Mathf.Cos (s * 2 * Mathf.PI) * dx / (2 * Mathf.PI);
float ny = x1 + Mathf.Sin (s * 2 * Mathf.PI) * dx / (2 * Mathf.PI);
float nz = t;
float heightValue = (float)HeightMap.Get (nx, ny, nz);
// отслеживаем максимальные и минимальные найденные значения
if (heightValue > mapData.Max)
mapData.Max = heightValue;
if (heightValue < mapData.Min)
mapData.Min = heightValue;
mapData.Data [x, y] = heightValue;
}
}
}
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 = 0, x2 = 2;
float y1 = 0, y2 = 2;
float dx = x2 - x1;
float dy = y2 - y1;
// Сэмплируем шум с небольшими интервалами
float s = x / (float)Width;
float t = y / (float)Height;
// Вычисляем четырехмерные координаты
float nx = x1 + Mathf.Cos (s*2*Mathf.PI) * dx/(2*Mathf.PI);
float ny = y1 + Mathf.Cos (t*2*Mathf.PI) * dy/(2*Mathf.PI);
float nz = x1 + Mathf.Sin (s*2*Mathf.PI) * dx/(2*Mathf.PI);
float nw = y1 + Mathf.Sin (t*2*Mathf.PI) * dy/(2*Mathf.PI);
float heightValue = (float)HeightMap.Get (nx, ny, nz, nw);
// отслеживаем максимальные и минимальные найденные значения
if (heightValue > mapData.Max) mapData.Max = heightValue;
if (heightValue < mapData.Min) mapData.Min = heightValue;
mapData.Data[x,y] = heightValue;
}
}
}
public Tile Left;
public Tile Right;
public Tile Top;
public Tile Bottom;
private Tile GetTop(Tile t)
{
return Tiles [t.X, MathHelper.Mod (t.Y - 1, Height)];
}
private Tile GetBottom(Tile t)
{
return Tiles [t.X, MathHelper.Mod (t.Y + 1, Height)];
}
private Tile GetLeft(Tile t)
{
return Tiles [MathHelper.Mod(t.X - 1, Width), t.Y];
}
private Tile GetRight(Tile t)
{
return Tiles [MathHelper.Mod (t.X + 1, Width), t.Y];
}
private void UpdateNeighbors()
{
for (var x = 0; x < Width; x++)
{
for (var y = 0; y < Height; y++)
{
Tile t = Tiles[x,y];
t.Top = GetTop(t);
t.Bottom = GetBottom (t);
t.Left = GetLeft (t);
t.Right = GetRight (t);
}
}
}
public void UpdateBitmask()
{
int count = 0;
if (Top.HeightType == HeightType)
count += 1;
if (Right.HeightType == HeightType)
count += 2;
if (Bottom.HeightType == HeightType)
count += 4;
if (Left.HeightType == HeightType)
count += 8;
Bitmask = count;
}
private void UpdateBitmasks()
{
for (var x = 0; x < Width; x++) {
for (var y = 0; y < Height; y++) {
Tiles [x, y].UpdateBitmask ();
}
}
}
//затемняем цвет граничного тайла
if (tiles[x,y].Bitmask != 15)
pixels[x + y * width] = Color.Lerp(pixels[x + y * width], Color.black, 0.4f);
using UnityEngine;
using System.Collections.Generic;
public enum TileGroupType
{
Water,
Land
}
public class TileGroup {
public TileGroupType Type;
public List<Tile> Tiles;
public TileGroup()
{
Tiles = new List<Tile> ();
}
}
public bool Collidable;
public bool FloodFilled;
List<TileGroup> Waters = new List<TileGroup> ();
List<TileGroup> Lands = new List<TileGroup> ();
private void FloodFill()
{
// Используем стек вместо рекурсии
Stack<Tile> stack = new Stack<Tile>();
for (int x = 0; x < Width; x++) {
for (int y = 0; y < Height; y++) {
Tile t = Tiles[x,y];
//Тайл уже залит, пропускаем его
if (t.FloodFilled) continue;
// Суша
if (t.Collidable)
{
TileGroup group = new TileGroup();
group.Type = TileGroupType.Land;
stack.Push(t);
while(stack.Count > 0) {
FloodFill(stack.Pop(), ref group, ref stack);
}
if (group.Tiles.Count > 0)
Lands.Add (group);
}
// Вода
else {
TileGroup group = new TileGroup();
group.Type = TileGroupType.Water;
stack.Push(t);
while(stack.Count > 0) {
FloodFill(stack.Pop(), ref group, ref stack);
}
if (group.Tiles.Count > 0)
Waters.Add (group);
}
}
}
}
private void FloodFill(Tile tile, ref TileGroup tiles, ref Stack<Tile> stack)
{
// Валидация
if (tile.FloodFilled)
return;
if (tiles.Type == TileGroupType.Land && !tile.Collidable)
return;
if (tiles.Type == TileGroupType.Water && tile.Collidable)
return;
// Добавление в TileGroup
tiles.Tiles.Add (tile);
tile.FloodFilled = true;
// заливка соседей
Tile t = GetTop (tile);
if (!t.FloodFilled && tile.Collidable == t.Collidable)
stack.Push (t);
t = GetBottom (tile);
if (!t.FloodFilled && tile.Collidable == t.Collidable)
stack.Push (t);
t = GetLeft (tile);
if (!t.FloodFilled && tile.Collidable == t.Collidable)
stack.Push (t);
t = GetRight (tile);
if (!t.FloodFilled && tile.Collidable == t.Collidable)
stack.Push (t);
}
К сожалению, не доступен сервер mySQL