using Godot; using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Godot.Collections; public class World : Spatial { public enum GenerationState { Undefined, Heightmap, TileType, Objects, Done } public GenerationState State = GenerationState.Done; // referenced scenes private PackedScene _worldChunkScene = GD.Load("res://scenes/WorldChunk.tscn"); // constants public const int ChunkSize = 16; public int Seed = 0; public HexGrid HexGrid = new HexGrid(); public Spatial Chunks; public Color DebugColor; // ui elements // scene nodes // resources // exports // [Export] public Vector2 Size = new Vector2(1, 1); // signals [Signal] delegate void OnTilesChanged(Array removedChunkIndices, Array addedChunkIndices); // delegate void OnCoordClicked(Vector2 world_pos); // other members private Vector2 _centerPlaneCoord; private int[] _centerChunkCoord = { 0, 0 }; private int[] _previousCenterChunkCoord = { 0, 0 }; private Rect2 _centerChunkRect = new Rect2(); private Random _debugColorRandom = new Random(); private Godot.Collections.Dictionary _cachedWorldChunks; private List _activeChunkIndices = new(); private List _addedChunkIndices = new(); private List _removedChunkIndices = new(); private OpenSimplexNoise noiseGenerator = new(); public World() { Debug.Assert(ChunkSize % 2 == 0); _cachedWorldChunks = new Godot.Collections.Dictionary(); } // Called when the node enters the scene tree for the first time. public override void _Ready() { Chunks = (Spatial)FindNode("Chunks"); Debug.Assert(Chunks != null); InitNoiseGenerator(); SetCenterPlaneCoord(Vector2.Zero); } public void InitNoiseGenerator() { noiseGenerator = new OpenSimplexNoise(); noiseGenerator.Seed = Seed; noiseGenerator.Octaves = 1; noiseGenerator.Period = 10; noiseGenerator.Persistence = 0.5f; noiseGenerator.Lacunarity = 2; } public WorldChunk GetOrCreateWorldChunk(int xIndex, int yIndex, Color debugColor) { if (IsTileCached(xIndex, yIndex)) { WorldChunk cachedChunk = _cachedWorldChunks[new Vector2(xIndex, yIndex)]; return cachedChunk; } return CreateWorldChunk(xIndex, yIndex, debugColor); } private bool IsTileCached(int xIndex, int yIndex) { return _cachedWorldChunks.ContainsKey(new Vector2(xIndex, yIndex)); } private WorldChunk CreateWorldChunk(int xIndex, int yIndex, Color debugColor) { WorldChunk result = (WorldChunk)_worldChunkScene.Instance(); result.SetSize(ChunkSize); Vector2 offsetCoordSouthWest = new Vector2(xIndex, yIndex) * ChunkSize; Vector2 offsetCoordNorthEast = offsetCoordSouthWest + new Vector2(1, 1) * (ChunkSize - 1); Vector2 planeCoordSouthWest = HexGrid.GetHexCenterFromOffset(offsetCoordSouthWest) + new Vector2(-HexGrid.HexSize.x, HexGrid.HexSize.y) * 0.5f; Vector2 planeCoordNorthEast = HexGrid.GetHexCenterFromOffset(offsetCoordNorthEast) + new Vector2(HexGrid.HexSize.x, -HexGrid.HexSize.y) * 0.5f; result.ChunkAddress = new Vector2(xIndex, yIndex); result.PlaneRect = new Rect2( new Vector2(planeCoordSouthWest.x, planeCoordNorthEast.y), new Vector2(planeCoordNorthEast.x - planeCoordSouthWest.x, planeCoordSouthWest.y - planeCoordNorthEast.y)); result.DebugColor = debugColor; result.DebugColor.a = 0.6f; Chunks.AddChild(result); Vector2 chunkIndex = new Vector2(xIndex, yIndex); _cachedWorldChunks.Add(chunkIndex, result); return result; } public void UpdateCenterChunkFromPlaneCoord(Vector2 planeCoord) { if (State != GenerationState.Done) { GD.PrintErr("Cannot update chunk to new planeCoord " + planeCoord + ": Chunk generation not yet finished!"); return; } // mark all chunks as retired Godot.Collections.Dictionary oldCachedChunks = new(_cachedWorldChunks); // set new center chunk var chunkIndex = GetChunkTupleFromPlaneCoord(planeCoord); WorldChunk currentChunk = GetOrCreateWorldChunk(chunkIndex.Item1, chunkIndex.Item2, new Color(GD.Randf(), GD.Randf(), GD.Randf())); _centerChunkRect = currentChunk.PlaneRect; // load or create adjacent chunks _activeChunkIndices = new List(); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1 - 1, chunkIndex.Item2 - 1)); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1, chunkIndex.Item2 - 1)); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1 + 1, chunkIndex.Item2 - 1)); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1 - 1, chunkIndex.Item2)); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1, chunkIndex.Item2)); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1 + 1, chunkIndex.Item2)); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1 - 1, chunkIndex.Item2 + 1)); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1, chunkIndex.Item2 + 1)); _activeChunkIndices.Add(new Vector2(chunkIndex.Item1 + 1, chunkIndex.Item2 + 1)); foreach(Vector2 activeChunkIndex in _activeChunkIndices) { GetOrCreateWorldChunk((int) activeChunkIndex.x, (int) activeChunkIndex.y, new Color(GD.Randf(), GD.Randf(), GD.Randf())); } // unload retired chunks _removedChunkIndices.Clear(); _addedChunkIndices.Clear(); foreach (var cachedChunkKey in oldCachedChunks.Keys) { if (!_activeChunkIndices.Contains(cachedChunkKey)) { RemoveChunk(cachedChunkKey); } } foreach (var chunkKey in _activeChunkIndices) { if (!oldCachedChunks.ContainsKey(chunkKey)) { _addedChunkIndices.Add(chunkKey); } } if (_addedChunkIndices.Count > 0) { State = GenerationState.Heightmap; } } private void RemoveChunk(Vector2 cachedChunkKey) { _cachedWorldChunks.Remove(cachedChunkKey); _removedChunkIndices.Add(cachedChunkKey); foreach (WorldChunk chunk in Chunks.GetChildren()) { if (chunk.ChunkAddress == new Vector2(cachedChunkKey.x, cachedChunkKey.y)) { chunk.QueueFree(); } } } private Tuple GetChunkTupleFromPlaneCoord(Vector2 planeCoord) { HexCell centerOffsetCoord = HexGrid.GetHexAt(planeCoord); Vector2 chunkIndexFloat = (centerOffsetCoord.OffsetCoords / (float)ChunkSize).Floor(); Tuple chunkIndex = new Tuple((int)chunkIndexFloat.x, (int)chunkIndexFloat.y); return chunkIndex; } public void SetCenterPlaneCoord(Vector2 centerPlaneCoord) { if (!_centerChunkRect.HasPoint(centerPlaneCoord)) { UpdateCenterChunkFromPlaneCoord(centerPlaneCoord); } } public override void _Process(float delta) { GenerationState oldState = State; if (State == GenerationState.Heightmap) { // generate heightmap for all new chunks foreach (Vector2 chunkIndex in _addedChunkIndices) { WorldChunk chunk = _cachedWorldChunks[chunkIndex]; Color debugChunkColor = new Color(Mathf.Abs(chunkIndex.x) / 5, Mathf.Abs(chunkIndex.y) / 5, Mathf.RoundToInt(Mathf.Abs(chunkIndex.x + chunkIndex.y)) % 2); GD.Print("Generating for offset " + chunkIndex + " chunk: " + chunk + " debugChunkColor: " + debugChunkColor); ImageTexture noiseImageTexture = new ImageTexture(); noiseImageTexture.CreateFromImage(noiseGenerator.GetImage(ChunkSize, ChunkSize, chunkIndex * ChunkSize), (uint) 0); // Debug Texture Image simpleImage = new Image(); simpleImage.Create(ChunkSize, ChunkSize, false, Image.Format.Rgb8); simpleImage.Lock(); foreach (int i in Enumerable.Range(0, ChunkSize)) { foreach (int j in Enumerable.Range(0, ChunkSize)) { if ((i + j) % 2 == 0) { simpleImage.SetPixelv(new Vector2(i, j), Colors.Aqua); } else { simpleImage.SetPixelv(new Vector2(i, j), debugChunkColor); } } } simpleImage.Unlock(); // noiseImageTexture.CreateFromImage(simpleImage, 0); chunk.SetHeightmap(noiseImageTexture); } // assign height map images State = GenerationState.TileType; } else if (State == GenerationState.TileType) { // assign tile type images State = GenerationState.Objects; } else if (State == GenerationState.Objects) { // generate objects State = GenerationState.Done; } if (oldState != GenerationState.Done && State == GenerationState.Done) { EmitSignal("OnTilesChanged", _removedChunkIndices.ToArray(), _addedChunkIndices.ToArray()); } } }