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; // Use limited parallelism instead of unlimited Task.Run var parallelOptions = new ParallelOptions { MaxDegreeOfParallelism = Math.Max(1, (int)(System.Environment.ProcessorCount * 0.5)), CancellationToken = CancellationToken }; Task.Run(() => { try { Parallel.For(0, _size.X, parallelOptions, 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)) { 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)) { // Water image.SetPixel(x, y, Color.FromString("#4380b0", Colors.Purple)); continue; } if (isDesertAtMapPosition(pixelOffset)) { // 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 && !isDesertAtMapPosition(pixelOffset)) { // 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 Vector2I ToHeightMapCoordinates(Vector2I position) { 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) ); // Clamp to valid range heightMapCoords.X = Mathf.Clamp(heightMapCoords.X, 0, heightMapResolution.X - 1); heightMapCoords.Y = Mathf.Clamp(heightMapCoords.Y, 0, heightMapResolution.Y - 1); return heightMapCoords; } private float GetValueAtMapPosition(Vector2I position) { Vector2I heightMapCoords = ToHeightMapCoordinates(position); 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 isDesertAtMapPosition(Vector2I position) { float heatValue = GetHeatAtMapPosition(position); float moistureValue = GetMoistureAtMapPosition(position); return moistureValue < _world.DesertMoistureThreshold && heatValue > _world.DesertTemperatureThreshold; } private bool IsSeaAtMapPosition(Vector2I position) { float heightValue = GetValueAtMapPosition(position); return heightValue <= _world.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.SetFractalType(FastNoiseLite.FractalTypeEnum.Fbm); _heatNoise.SetFractalOctaves(12); _heatNoise.SetSeed(1738); } Vector2I heightMapCoords = ToHeightMapCoordinates(position); float heat = _heatNoise.GetNoise2D(heightMapCoords.X, heightMapCoords.Y); heat *= _world.TemperatureNoiseAmplitude; // Fall off towards the poles. Use sine function for a globe effect. float mapResolutionY = _world.HeightMapImage.GetHeight(); float latitudeFactor = Mathf.Sin(Mathf.Pi * heightMapCoords.Y / mapResolutionY); heat = latitudeFactor + heat; 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.SetFractalType(FastNoiseLite.FractalTypeEnum.Fbm); _moistureNoise.SetFractalOctaves(12); _moistureNoise.SetSeed(1337); } Vector2I heightMapCoords = ToHeightMapCoordinates(position); float moisture = _moistureNoise.GetNoise2D(heightMapCoords.X, heightMapCoords.Y); moisture *= _world.MoistureNoiseAmplitude; // Fall off towards the poles. Use sine function for a globe effect. float mapResolutionY = _world.HeightMapImage.GetHeight(); float latitudeFactor = Mathf.Sin(Mathf.Pi * heightMapCoords.Y / mapResolutionY); moisture = latitudeFactor + moisture; return moisture; } }