285 lines
7.5 KiB
C#
285 lines
7.5 KiB
C#
using System;
|
|
using System.Diagnostics;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Godot;
|
|
|
|
public partial class Tile : Node2D
|
|
{
|
|
private Vector2I _size;
|
|
private Vector2I _chunkId;
|
|
private World _world;
|
|
public CancellationToken CancellationToken { get; set; }
|
|
|
|
public Tile(Vector2I size, Vector2I chunkId, World world)
|
|
{
|
|
_size = size;
|
|
_chunkId = chunkId;
|
|
_world = world;
|
|
}
|
|
|
|
public override void _Ready()
|
|
{
|
|
loadTile();
|
|
}
|
|
|
|
void loadTile()
|
|
{
|
|
|
|
Image image = Image.CreateEmpty(_size.X, _size.Y, false, Image.Format.Rgba8);
|
|
|
|
Vector2I heightMapResolution = new Vector2I(
|
|
_world.HeightMapImage.GetWidth(),
|
|
_world.HeightMapImage.GetHeight()
|
|
);
|
|
|
|
Vector2I mapResolution = _world.ChunksPerAxis * _world.TileSize;
|
|
Vector2I tileOffset = new Vector2I(
|
|
_chunkId.X * _world.TileSize,
|
|
_chunkId.Y * _world.TileSize
|
|
);
|
|
|
|
// Store parameters locally to avoid changes during generation
|
|
float rockThreshold = _world.RockThreshold;
|
|
float snowHeightThreshold = _world.SnowHeightThreshold;
|
|
float waterThreshold = _world.WaterThreshold;
|
|
float desertMoistureThreshold = _world.DesertMoistureThreshold;
|
|
float desertTemperatureThreshold = _world.DesertTemperatureThreshold;
|
|
|
|
DebugType debugType = _world.DebugType;
|
|
|
|
// Run in thread
|
|
Task.Run(() =>
|
|
{
|
|
try
|
|
{
|
|
for (int x = 0; x < _size.X; x++)
|
|
{
|
|
if (CancellationToken.IsCancellationRequested) return;
|
|
|
|
for (int y = 0; y < _size.Y; y++)
|
|
{
|
|
if (CancellationToken.IsCancellationRequested) return;
|
|
|
|
Vector2I pixelOffset = new Vector2I(
|
|
x + tileOffset.X,
|
|
y + tileOffset.Y
|
|
);
|
|
|
|
float heightValue = GetValueAtMapPosition(pixelOffset);
|
|
float heatValue = GetHeatAtMapPosition(pixelOffset);
|
|
float moistureValue = GetMoistureAtMapPosition(pixelOffset);
|
|
|
|
// Get the max difference between the coord and its neighbors
|
|
float maxDiff = 0.0f;
|
|
float centerHeight = heightValue;
|
|
bool bordersSea = false;
|
|
for (int offsetX = -1; offsetX <= 1; offsetX++)
|
|
{
|
|
for (int offsetY = -1; offsetY <= 1; offsetY++)
|
|
{
|
|
if (offsetX == 0 && offsetY == 0)
|
|
continue;
|
|
|
|
Vector2I neighborCoords = new Vector2I(
|
|
pixelOffset.X + offsetX,
|
|
pixelOffset.Y + offsetY
|
|
);
|
|
|
|
float neighborHeight = GetValueAtMapPosition(neighborCoords);
|
|
|
|
if (IsSeaAtMapPosition(neighborCoords, waterThreshold))
|
|
{
|
|
bordersSea = true;
|
|
}
|
|
|
|
float diff = Mathf.Abs(centerHeight - neighborHeight);
|
|
if (diff > maxDiff)
|
|
{
|
|
maxDiff = diff;
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
|
|
if (debugType == DebugType.None)
|
|
{
|
|
|
|
// Grass (default)
|
|
image.SetPixel(x, y, Color.FromString("#bed58a", Colors.Purple));
|
|
|
|
if (IsSeaAtMapPosition(pixelOffset, waterThreshold))
|
|
{
|
|
// Water
|
|
image.SetPixel(x, y, Color.FromString("#4380b0", Colors.Purple));
|
|
continue;
|
|
}
|
|
|
|
if (moistureValue < desertMoistureThreshold && heatValue > desertTemperatureThreshold)
|
|
{
|
|
// Desert
|
|
image.SetPixel(x, y, Color.FromString("#edc9af", Colors.Purple));
|
|
}
|
|
|
|
if (bordersSea)
|
|
{
|
|
// Beach
|
|
image.SetPixel(x, y, Color.FromString("#808080", Colors.Purple));
|
|
continue;
|
|
}
|
|
|
|
if (heightValue > snowHeightThreshold)
|
|
{
|
|
// Snow
|
|
image.SetPixel(x, y, Color.FromString("#f4f4f4", Colors.Purple));
|
|
}
|
|
|
|
// We want steep areas to be grey for rock.
|
|
if (maxDiff > rockThreshold)
|
|
{
|
|
// Steep area
|
|
image.SetPixel(x, y, Color.FromString("#e4d3a6", Colors.Purple));
|
|
continue;
|
|
}
|
|
|
|
}
|
|
else if (debugType == DebugType.Height)
|
|
{
|
|
float grayValue = heightValue;
|
|
image.SetPixel(x, y, new Color(grayValue, grayValue, grayValue));
|
|
}
|
|
else if (debugType == DebugType.Slope)
|
|
{
|
|
float grayValue = maxDiff * 5.0f;
|
|
image.SetPixel(x, y, new Color(grayValue, grayValue, grayValue));
|
|
} else if (debugType == DebugType.Temperature)
|
|
{
|
|
image.SetPixel(x, y, new Color(heatValue, heatValue, heatValue));
|
|
} else if (debugType == DebugType.Moisture)
|
|
{
|
|
image.SetPixel(x, y, new Color(moistureValue, moistureValue, moistureValue));
|
|
}
|
|
|
|
|
|
|
|
|
|
}
|
|
}
|
|
|
|
if (!CancellationToken.IsCancellationRequested)
|
|
{
|
|
// Create a texture from the image and assign it to a Sprite2D
|
|
Texture2D texture = ImageTexture.CreateFromImage(image);
|
|
Sprite2D sprite = new Sprite2D();
|
|
sprite.Texture = texture;
|
|
sprite.TextureFilter = TextureFilterEnum.Nearest;
|
|
CallDeferred("add_child", sprite);
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Generation was cancelled, do nothing
|
|
}
|
|
}, CancellationToken);
|
|
|
|
}
|
|
|
|
// Terrain Query Methods
|
|
private float GetValueAtMapPosition(Vector2I position)
|
|
{
|
|
|
|
// Clamp position to map bounds
|
|
position.X = Mathf.Clamp(position.X, 0, _world.ChunksPerAxis.X * _world.TileSize - 1);
|
|
position.Y = Mathf.Clamp(position.Y, 0, _world.ChunksPerAxis.Y * _world.TileSize - 1);
|
|
|
|
Vector2I heightMapResolution = new Vector2I(
|
|
_world.HeightMapImage.GetWidth(),
|
|
_world.HeightMapImage.GetHeight()
|
|
);
|
|
|
|
Vector2I mapResolution = _world.ChunksPerAxis * _world.TileSize;
|
|
|
|
Vector2 mapDelta = (Vector2) position / (Vector2) mapResolution;
|
|
|
|
Vector2I heightMapCoords = new Vector2I(
|
|
(int)(mapDelta.X * heightMapResolution.X),
|
|
(int)(mapDelta.Y * heightMapResolution.Y)
|
|
);
|
|
|
|
Color hmColor = _world.HeightMapImage.GetPixel(heightMapCoords.X, heightMapCoords.Y);
|
|
|
|
return hmColor.R;
|
|
}
|
|
|
|
private bool isSeaAtMapPosition(Vector2I position)
|
|
{
|
|
float heightValue = GetValueAtMapPosition(position);
|
|
return heightValue <= _world.WaterThreshold;
|
|
}
|
|
|
|
private bool IsSeaAtMapPosition(Vector2I position, float waterThreshold)
|
|
{
|
|
float heightValue = GetValueAtMapPosition(position);
|
|
return heightValue <= waterThreshold;
|
|
}
|
|
|
|
private FastNoiseLite _heatNoise = null;
|
|
private float GetHeatAtMapPosition(Vector2I position)
|
|
{
|
|
|
|
if (_heatNoise == null)
|
|
{
|
|
_heatNoise = new FastNoiseLite();
|
|
_heatNoise.SetNoiseType(FastNoiseLite.NoiseTypeEnum.Perlin);
|
|
_heatNoise.SetFrequency(1/_world.TemperatureNoiseFrequency);
|
|
_heatNoise.SetSeed(1738);
|
|
}
|
|
|
|
float heat = _heatNoise.GetNoise2D(position.X, position.Y);
|
|
heat *= 0.2f;
|
|
|
|
// Fall off towards the poles. Use sine function for a globe effect.
|
|
float mapResolutionY = _world.ChunksPerAxis.Y * _world.TileSize;
|
|
float latitudeFactor = Mathf.Sin(Mathf.Pi * position.Y / mapResolutionY);
|
|
heat = latitudeFactor + heat;
|
|
heat = Mathf.Clamp(
|
|
heat,
|
|
0.0f,
|
|
1.0f
|
|
);
|
|
|
|
return heat;
|
|
}
|
|
|
|
private FastNoiseLite _moistureNoise = null;
|
|
private float GetMoistureAtMapPosition(Vector2I position)
|
|
{
|
|
|
|
if (_moistureNoise == null)
|
|
{
|
|
_moistureNoise = new FastNoiseLite();
|
|
_moistureNoise.SetNoiseType(FastNoiseLite.NoiseTypeEnum.Perlin);
|
|
_moistureNoise.SetFrequency(1/_world.MoistureNoiseFrequency);
|
|
_moistureNoise.SetSeed(1337);
|
|
}
|
|
|
|
float moisture = _moistureNoise.GetNoise2D(position.X, position.Y);
|
|
moisture *= 0.2f;
|
|
|
|
// Fall off towards the poles. Use sine function for a globe effect.
|
|
float mapResolutionY = _world.ChunksPerAxis.Y * _world.TileSize;
|
|
float latitudeFactor = Mathf.Sin(Mathf.Pi * position.Y / mapResolutionY);
|
|
moisture = latitudeFactor + moisture;
|
|
moisture = Mathf.Clamp(
|
|
moisture,
|
|
0.0f,
|
|
1.0f
|
|
);
|
|
|
|
|
|
return moisture;
|
|
}
|
|
|
|
}
|