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; } }