using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Godot; using Godot.Collections; public class World : Spatial { public enum GenerationState { Undefined, Heightmap, TileType, Objects, Done } // constants public const int ChunkSize = 18; public const int NumChunkRows = 3; public const int NumChunkColumns = NumChunkRows; private static readonly Color RockColor = new(0.5f, 0.5f, 0.4f); private static readonly Color GrassColor = new(0, 0.4f, 0); private static readonly Color DarkGrassColor = new(0.05882353f, 0.5411765f, 0.05882353f); private static readonly Color LightWaterColor = new(0.05882353f, 0.05882353f, 0.8627451f); private readonly List _addedChunkIndices = new(); private readonly Godot.Collections.Dictionary _cachedWorldChunks; private readonly Image _heightmapImage = new(); private readonly List _removedChunkIndices = new(); private readonly Image _tileTypeMapImage = new(); // referenced scenes private readonly PackedScene _worldChunkScene = GD.Load("res://scenes/WorldChunk.tscn"); private List _activeChunkIndices = new(); private Rect2 _centerChunkRect; // delegate void OnCoordClicked(Vector2 world_pos); // other members private Vector2 _centerPlaneCoord; private Vector2 _chunkIndexNorthEast; private Vector2 _chunkIndexSouthWest; private Array _grassAssets; private ImageTexture _heightmapTexture; private OpenSimplexNoise _noiseGenerator = new(); private Array _rockAssets; private TileInstanceManager _tileInstanceManager; private Array _treeAssets; private ImageTexture _viewTileTypeTexture; public Vector2 CenterChunkIndex = Vector2.Zero; public Spatial Chunks; public Color DebugColor; public HexGrid HexGrid = new(); public int Seed = 0; public GenerationState State = GenerationState.Done; public Vector2 WorldTextureCoordinateOffset; // ui elements // scene nodes // resources // exports // [Export] public Vector2 Size = new Vector2(1, 1); // signals [Signal] private delegate void OnTilesChanged(Array removedChunkIndices, Array addedChunkIndices); [Signal] private delegate void OnWorldViewTileTypeImageChanged(Image viewTileTypeImage); [Signal] private delegate void OnHeightmapImageChanged(Image heightmapImage); 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); _tileInstanceManager = (TileInstanceManager)FindNode("TileInstanceManager"); Debug.Assert(_tileInstanceManager != null); _tileInstanceManager.TileMultiMeshInstance.Multimesh.InstanceCount = ChunkSize * ChunkSize * NumChunkColumns * NumChunkRows; InitNoiseGenerator(); GetNode("Assets").Visible = false; _rockAssets = new Array(); foreach (Spatial asset in GetNode("Assets/Rocks").GetChildren()) _rockAssets.Add(asset); _grassAssets = new Array(); foreach (Spatial asset in GetNode("Assets/Grass").GetChildren()) _grassAssets.Add(asset); _treeAssets = new Array(); foreach (Spatial asset in GetNode("Assets/Trees").GetChildren()) _treeAssets.Add(asset); 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 (IsChunkCached(xIndex, yIndex)) { var cachedChunk = _cachedWorldChunks[new Vector2(xIndex, yIndex)]; return cachedChunk; } return CreateWorldChunk(xIndex, yIndex, debugColor); } private bool IsChunkCached(int xIndex, int yIndex) { return _cachedWorldChunks.ContainsKey(new Vector2(xIndex, yIndex)); } private WorldChunk CreateWorldChunk(int xIndex, int yIndex, Color debugColor) { var result = (WorldChunk)_worldChunkScene.Instance(); result.SetSize(ChunkSize); var offsetCoordSouthWest = new Vector2(xIndex, yIndex) * ChunkSize; var offsetCoordNorthEast = offsetCoordSouthWest + new Vector2(1, 1) * (ChunkSize - 1); var planeCoordSouthWest = HexGrid.GetHexCenterFromOffset(offsetCoordSouthWest) + new Vector2(-HexGrid.HexSize.x, HexGrid.HexSize.y) * 0.5f; var planeCoordNorthEast = HexGrid.GetHexCenterFromOffset(offsetCoordNorthEast) + new Vector2(HexGrid.HexSize.x, -HexGrid.HexSize.y) * 0.5f; result.ChunkIndex = 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); var chunkIndex = new Vector2(xIndex, yIndex); _cachedWorldChunks.Add(chunkIndex, result); return result; } private bool IsColorEqualApprox(Color colorA, Color colorB) { var colorDifference = new Vector3(colorA.r - colorB.r, colorA.g - colorB.g, colorA.b - colorB.b); return colorDifference.LengthSquared() < 0.1 * 0.1; } private Spatial SelectAsset(Vector2 offsetCoord, Array assets, Random randomGenerator, double probability) { if (randomGenerator.NextDouble() < 1.0 - probability) return null; var assetIndex = randomGenerator.Next(assets.Count); var assetInstance = (Spatial)assets[assetIndex].Duplicate(); var assetTransform = Transform.Identity; assetTransform.origin = HexGrid.GetHexCenterVec3FromOffset(offsetCoord); // TODO: assetTransform.origin.y = GetHeightAtOffset(offsetCoord); assetTransform.origin.y = 0; assetTransform.basis = assetTransform.basis.Rotated(Vector3.Up, (float)(randomGenerator.NextDouble() * Mathf.Pi * 2)); assetInstance.Transform = assetTransform; return assetInstance; } private void PopulateChunk(WorldChunk chunk) { var environmentRandom = new Random(Seed); var tileTypeImage = chunk.TileTypeOffscreenViewport.GetTexture().GetData(); tileTypeImage.Lock(); foreach (var textureCoordU in Enumerable.Range(0, chunk.Size)) foreach (var textureCoordV in Enumerable.Range(0, chunk.Size)) { var colorValue = tileTypeImage.GetPixel(textureCoordU, textureCoordV); var textureCoord = new Vector2(textureCoordU, textureCoordV); var offsetCoord = chunk.ChunkIndex * ChunkSize + textureCoord; if (IsColorEqualApprox(colorValue, RockColor)) { var rockAsset = SelectAsset(offsetCoord, _rockAssets, environmentRandom, 0.15); if (rockAsset != null) chunk.Entities.AddChild(rockAsset); // TODO: MarkCellUnwalkable(cell); } else if (IsColorEqualApprox(colorValue, GrassColor) || IsColorEqualApprox(colorValue, DarkGrassColor)) { var grassAsset = SelectAsset(offsetCoord, _grassAssets, environmentRandom, 0.15); if (grassAsset != null) chunk.Entities.AddChild(grassAsset); Tree treeAsset = SelectAsset(offsetCoord, _treeAssets, environmentRandom, 0.05) as Tree; if (treeAsset != null) { chunk.Entities.AddChild(treeAsset); treeAsset.Connect("EntityClicked", this, nameof(OnEntityClicked)); } // TODO: MarkCellUnwalkable(cell); // else if (environmentRandom.NextDouble() < 0.01) // { // var chestAsset = (Chest)_chestScene.Instance(); // var assetTransform = Transform.Identity; // assetTransform.origin = GetHexCenterFromOffset(offsetCoord); // assetTransform.origin.y = GetHeightAtOffset(offsetCoord); // chestAsset.Transform = assetTransform; // Entities.AddChild(chestAsset); // MarkCellUnwalkable(cell); // } } // else if (IsColorWater(colorValue)) // { // MarkCellUnwalkable(cell); // } } tileTypeImage.Unlock(); } public Vector2 WorldToOffsetCoords(Vector3 fromPositionWorld) { return HexGrid.GetHexAt(new Vector2(fromPositionWorld.x, fromPositionWorld.z)).OffsetCoords; } public Vector3 GetHexCenterFromOffset(Vector2 fromPositionOffset) { return HexGrid.GetHexCenterVec3FromOffset(fromPositionOffset); } 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); CenterChunkIndex = new Vector2(chunkIndex.Item1, chunkIndex.Item2); var 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)); Debug.Assert(_activeChunkIndices.Count == NumChunkRows * NumChunkColumns); foreach (var 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); var chunk = _cachedWorldChunks[chunkKey]; GenerateChunkNoiseMap(chunk); State = GenerationState.Heightmap; } } private void GenerateChunkNoiseMap(WorldChunk chunk) { var chunkIndex = chunk.ChunkIndex; var debugChunkColor = new Color(Mathf.Abs(chunkIndex.x) / 5, Mathf.Abs(chunkIndex.y) / 5, Mathf.RoundToInt(Mathf.Abs(chunkIndex.x + chunkIndex.y)) % 2); var noiseImageTexture = new ImageTexture(); noiseImageTexture.CreateFromImage(_noiseGenerator.GetImage(ChunkSize, ChunkSize, chunkIndex * ChunkSize), 0); // Debug Texture var simpleImage = new Image(); simpleImage.Create(ChunkSize, ChunkSize, false, Image.Format.Rgb8); simpleImage.Lock(); foreach (var i in Enumerable.Range(0, ChunkSize)) foreach (var 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.SetNoisemap(noiseImageTexture); } private void RemoveChunk(Vector2 cachedChunkKey) { _cachedWorldChunks.Remove(cachedChunkKey); _removedChunkIndices.Add(cachedChunkKey); foreach (WorldChunk chunk in Chunks.GetChildren()) if (chunk.ChunkIndex == new Vector2(cachedChunkKey.x, cachedChunkKey.y)) chunk.QueueFree(); } private Tuple GetChunkTupleFromPlaneCoord(Vector2 planeCoord) { var centerOffsetCoord = HexGrid.GetHexAt(planeCoord); var chunkIndexFloat = (centerOffsetCoord.OffsetCoords / ChunkSize).Floor(); var chunkIndex = new Tuple((int)chunkIndexFloat.x, (int)chunkIndexFloat.y); return chunkIndex; } public void SetCenterPlaneCoord(Vector2 centerPlaneCoord) { if (!_centerChunkRect.HasPoint(centerPlaneCoord)) { UpdateCenterChunkFromPlaneCoord(centerPlaneCoord); UpdateChunkBounds(); UpdateNavigationBounds(); } } private void UpdateWorldViewTexture() { var worldChunkSize = ChunkSize; var numWorldChunkRows = NumChunkRows; var numWorldChunkColumns = NumChunkColumns; _heightmapImage.Create(worldChunkSize * numWorldChunkColumns, worldChunkSize * numWorldChunkRows, false, Image.Format.Rgba8); _tileTypeMapImage.Create(worldChunkSize * numWorldChunkColumns, worldChunkSize * numWorldChunkRows, false, Image.Format.Rgba8); foreach (var chunkIndex in _activeChunkIndices) { var worldChunk = GetOrCreateWorldChunk((int)chunkIndex.x, (int)chunkIndex.y, Colors.White); _heightmapImage.BlendRect( worldChunk.HeightmapOffscreenViewport.GetTexture().GetData(), new Rect2(Vector2.Zero, Vector2.One * worldChunkSize), (chunkIndex - CenterChunkIndex + Vector2.One) * worldChunkSize); _tileTypeMapImage.BlendRect( worldChunk.TileTypeOffscreenViewport.GetTexture().GetData(), new Rect2(Vector2.Zero, Vector2.One * worldChunkSize), (chunkIndex - CenterChunkIndex + Vector2.One) * worldChunkSize); } _heightmapTexture = new ImageTexture(); _heightmapTexture.CreateFromImage(_heightmapImage); _viewTileTypeTexture = new ImageTexture(); _viewTileTypeTexture.CreateFromImage(_tileTypeMapImage); WorldTextureCoordinateOffset = _chunkIndexSouthWest * worldChunkSize; EmitSignal("OnWorldViewTileTypeImageChanged", _tileTypeMapImage); EmitSignal("OnHeightmapImageChanged", _heightmapImage); } private void UpdateChunkBounds() { _chunkIndexSouthWest = Vector2.Inf; _chunkIndexNorthEast = -Vector2.Inf; foreach (var chunkIndex in _activeChunkIndices) { var worldChunk = GetOrCreateWorldChunk((int)chunkIndex.x, (int)chunkIndex.y, Colors.White); if (chunkIndex.x <= _chunkIndexSouthWest.x && chunkIndex.y <= _chunkIndexSouthWest.y) _chunkIndexSouthWest = chunkIndex; else if (chunkIndex.x >= _chunkIndexNorthEast.x && chunkIndex.y >= _chunkIndexNorthEast.y) _chunkIndexNorthEast = chunkIndex; } } private void UpdateNavigationBounds() { var cellSouthWest = HexGrid.GetHexAtOffset(_chunkIndexSouthWest * ChunkSize); // Chunks have their cells ordered from south west (0,0) to north east (ChunkSize, ChunkSize). For the // north east cell we have to add the chunk size to get to the actual corner cell. var cellNorthEast = HexGrid.GetHexAtOffset(_chunkIndexNorthEast * ChunkSize + Vector2.One * (ChunkSize - 1)); var centerCell = HexGrid.GetHexAtOffset(((cellNorthEast.OffsetCoords - cellSouthWest.OffsetCoords) / 2).Round()); var numCells = ChunkSize * Math.Max(NumChunkColumns, NumChunkRows); HexGrid.SetBoundsOffset(cellSouthWest, ChunkSize * new Vector2(NumChunkColumns, NumChunkRows)); } public override void _Process(float delta) { var oldState = State; UpdateGenerationState(); if (oldState != GenerationState.Done && State == GenerationState.Done) { UpdateWorldViewTexture(); EmitSignal("OnTilesChanged", _removedChunkIndices.ToArray(), _addedChunkIndices.ToArray()); } } private void UpdateGenerationState() { if (State == GenerationState.Heightmap) { var numChunksGeneratingHeightmap = 0; foreach (var chunkIndex in _addedChunkIndices) { var chunk = _cachedWorldChunks[chunkIndex]; if (chunk.HeightMapFrameCount > 0) numChunksGeneratingHeightmap++; } if (numChunksGeneratingHeightmap == 0) { // assign height map images foreach (var chunkIndex in _addedChunkIndices) { var chunk = _cachedWorldChunks[chunkIndex]; chunk.SetHeightmap(chunk.HeightmapOffscreenViewport.GetTexture()); } State = GenerationState.TileType; } } else if (State == GenerationState.TileType) { var numChunksGeneratingTileType = 0; foreach (var chunkIndex in _addedChunkIndices) { var chunk = _cachedWorldChunks[chunkIndex]; if (chunk.TileTypeMapFrameCount > 0) numChunksGeneratingTileType++; } if (numChunksGeneratingTileType == 0) State = GenerationState.Objects; } else if (State == GenerationState.Objects) { // generate objects foreach (var chunkIndex in _addedChunkIndices) PopulateChunk(_cachedWorldChunks[chunkIndex]); State = GenerationState.Done; } } }