using System; using System.Collections.Generic; using System.Diagnostics; using System.Linq; using Godot; using Godot.Collections; using Priority_Queue; public class World : Spatial { public enum GenerationState { Empty, Heightmap, TileType, Objects, Done } // constants public int ChunkSize = 14; 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 Godot.Collections.Dictionary _cachedWorldChunks; private readonly List _addedChunkIndices = new(); private readonly List _deactivatedWorldChunks = new(); 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; private readonly List _removedSpatialNodes = new(); // 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 MultiMeshInstance _tileMultiMeshInstance; private int _usedTileMeshInstances; 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.Empty; 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); [Signal] public delegate void EntityClicked(Entity entity); // signals [Signal] private delegate void TileClicked(HexTile3D tile3d); [Signal] private delegate void TileHovered(HexTile3D tile3d); 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); _tileMultiMeshInstance = (MultiMeshInstance)FindNode("TileMultiMeshInstance"); Debug.Assert(_tileMultiMeshInstance != null); 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); } } public void InitNoiseGenerator() { _noiseGenerator = new OpenSimplexNoise(); _noiseGenerator.Seed = Seed; _noiseGenerator.Octaves = 1; _noiseGenerator.Period = 10; _noiseGenerator.Persistence = 0.5f; _noiseGenerator.Lacunarity = 2; } public void Reset() { foreach (Spatial chunkChild in Chunks.GetChildren()) { chunkChild.QueueFree(); } // foreach (WorldChunk chunk in _cachedWorldChunks.Values) { // chunk.QueueFree(); // } _cachedWorldChunks.Clear(); _addedChunkIndices.Clear(); _tileMultiMeshInstance.Multimesh.InstanceCount = ChunkSize * ChunkSize * NumChunkColumns * NumChunkRows; _usedTileMeshInstances = 0; State = GenerationState.Empty; } public WorldChunk GetOrCreateWorldChunk(Vector2 chunkIndex, Color debugColor) { WorldChunk chunk; if (IsChunkCached(chunkIndex)) { return _cachedWorldChunks[chunkIndex]; } if (_deactivatedWorldChunks.Count > 0) { chunk = _deactivatedWorldChunks.First(); _deactivatedWorldChunks.RemoveAt(0); } else { chunk = CreateWorldChunk(chunkIndex, debugColor); } _cachedWorldChunks[chunkIndex] = chunk; return chunk; } private bool IsChunkCached(Vector2 chunkIndex) { return _cachedWorldChunks.ContainsKey(chunkIndex); } private WorldChunk CreateWorldChunk(Vector2 chunkIndex, Color debugColor) { WorldChunk result = (WorldChunk)_worldChunkScene.Instance(); result.SetSize(ChunkSize); Chunks.AddChild(result); result.Connect("TileClicked", this, nameof(OnTileClicked)); result.Connect("TileHovered", this, nameof(OnTileHovered)); result.SetSize(ChunkSize); result.InitializeTileInstances(chunkIndex, _tileMultiMeshInstance, _usedTileMeshInstances); _usedTileMeshInstances += result.Tiles.GetChildCount(); result.SetChunkIndex(chunkIndex, HexGrid); result.UpdateTileTransforms(); result.DebugColor = debugColor; result.DebugColor.a = 0.6f; _cachedWorldChunks.Add(chunkIndex, result); return result; } private bool IsColorEqualApprox(Color colorA, Color colorB) { Vector3 colorDifference = new(colorA.r - colorB.r, colorA.g - colorB.g, colorA.b - colorB.b); return colorDifference.LengthSquared() < 0.1 * 0.1; } private Spatial SelectAsset(Vector2 textureCoord, Array assets, Random randomGenerator, double probability) { if (randomGenerator.NextDouble() < 1.0 - probability) { return null; } int assetIndex = randomGenerator.Next(assets.Count); Spatial assetInstance = (Spatial)assets[assetIndex].Duplicate(); Transform assetTransform = Transform.Identity; assetTransform.origin = HexGrid.GetHexCenterVec3FromOffset(textureCoord); // 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) { Random environmentRandom = new(Seed); chunk.CreateUnlockedTileTypeImage(); foreach (int textureCoordU in Enumerable.Range(0, chunk.Size)) { foreach (int textureCoordV in Enumerable.Range(0, chunk.Size)) { Color colorValue = chunk.TileTypeImage.GetPixel(textureCoordU, textureCoordV); Vector2 textureCoord = new(textureCoordU, textureCoordV); Vector2 offsetCoord = chunk.ChunkIndex * ChunkSize + textureCoord; if (IsColorEqualApprox(colorValue, RockColor)) { Spatial rockAsset = SelectAsset(textureCoord, _rockAssets, environmentRandom, 0.15); if (rockAsset != null) { chunk.Entities.AddChild(rockAsset); MarkCellUnwalkable(HexGrid.GetHexAtOffset(offsetCoord)); } } else if (IsColorEqualApprox(colorValue, GrassColor) || IsColorEqualApprox(colorValue, DarkGrassColor)) { Spatial grassAsset = SelectAsset(textureCoord, _grassAssets, environmentRandom, 0.15); if (grassAsset != null) { chunk.Entities.AddChild(grassAsset); } Tree treeAsset = SelectAsset(textureCoord, _treeAssets, environmentRandom, 0.05) as Tree; if (treeAsset != null) { chunk.Entities.AddChild(treeAsset); treeAsset.Connect("EntityClicked", this, nameof(OnEntityClicked)); treeAsset.Connect("TreeChopped", this, nameof(OnBlockingSpatialRemoved)); MarkCellUnwalkable(HexGrid.GetHexAtOffset(offsetCoord)); } // 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); // } } } } 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 && State != GenerationState.Empty) { 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 CenterChunkIndex = GetChunkTupleFromPlaneCoord(planeCoord); WorldChunk currentChunk = GetOrCreateWorldChunk(CenterChunkIndex, new Color(GD.Randf(), GD.Randf(), GD.Randf())); _centerChunkRect = new Rect2( new Vector2(currentChunk.Transform.origin.x, currentChunk.Transform.origin.z) + currentChunk.PlaneRect.Position, currentChunk.PlaneRect.Size); GD.Print("Center Chunk Rect: " + _centerChunkRect.Position + " size: " + _centerChunkRect.Size); // load or create adjacent chunks _activeChunkIndices = new List(); _activeChunkIndices.Add(CenterChunkIndex + new Vector2(-1, -1)); _activeChunkIndices.Add(CenterChunkIndex + new Vector2(0, -1)); _activeChunkIndices.Add(CenterChunkIndex + new Vector2(1, -1)); _activeChunkIndices.Add(CenterChunkIndex + new Vector2(-1, 0)); _activeChunkIndices.Add(CenterChunkIndex); _activeChunkIndices.Add(CenterChunkIndex + new Vector2(+1, 0)); _activeChunkIndices.Add(CenterChunkIndex + new Vector2(-1, +1)); _activeChunkIndices.Add(CenterChunkIndex + new Vector2(0, +1)); _activeChunkIndices.Add(CenterChunkIndex + new Vector2(+1, +1)); // clear unused chunks _deactivatedWorldChunks.Clear(); _addedChunkIndices.Clear(); foreach (Vector2 oldChunkIndex in oldCachedChunks.Keys) { if (!_activeChunkIndices.Contains(oldChunkIndex)) { DeactivateChunk(oldCachedChunks[oldChunkIndex]); } } foreach (Vector2 activeChunkIndex in _activeChunkIndices) { WorldChunk chunk = GetOrCreateWorldChunk(activeChunkIndex, new Color(GD.Randf(), GD.Randf(), GD.Randf())); _cachedWorldChunks[activeChunkIndex] = chunk; } Debug.Assert(_activeChunkIndices.Count == NumChunkRows * NumChunkColumns); foreach (Vector2 chunkKey in _activeChunkIndices) { if (!oldCachedChunks.ContainsKey(chunkKey)) { ActivateChunk(_cachedWorldChunks[chunkKey], chunkKey); State = GenerationState.Heightmap; } } } private void ActivateChunk(WorldChunk chunk, Vector2 chunkIndex) { chunk.SetChunkIndex(chunkIndex, HexGrid); chunk.UpdateTileTransforms(); _addedChunkIndices.Add(chunk.ChunkIndex); GenerateChunkNoiseMap(chunk); } private void DeactivateChunk(WorldChunk chunk) { _cachedWorldChunks.Remove(chunk.ChunkIndex); chunk.ClearContent(); _deactivatedWorldChunks.Add(chunk); } private void GenerateChunkNoiseMap(WorldChunk chunk) { Vector2 chunkIndex = chunk.ChunkIndex; ImageTexture noiseImageTexture = new(); noiseImageTexture.CreateFromImage(_noiseGenerator.GetImage(ChunkSize, ChunkSize, chunkIndex * ChunkSize), 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 Vector2 GetChunkTupleFromPlaneCoord(Vector2 planeCoord) { HexCell hexCell = HexGrid.GetHexAt(planeCoord); return GetChunkIndexFromOffsetCoord(hexCell.OffsetCoords); } private Vector2 GetChunkIndexFromOffsetCoord(Vector2 offsetCoord) { return (offsetCoord / ChunkSize).Floor(); } public void SetCenterPlaneCoord(Vector2 centerPlaneCoord) { if (State != GenerationState.Done) { return; } if (!_centerChunkRect.HasPoint(centerPlaneCoord)) { UpdateCenterChunkFromPlaneCoord(centerPlaneCoord); UpdateChunkBounds(); UpdateNavigationBounds(); } } private void UpdateWorldViewTexture() { int worldChunkSize = ChunkSize; int numWorldChunkRows = NumChunkRows; int numWorldChunkColumns = NumChunkColumns; _heightmapImage.Create(worldChunkSize * numWorldChunkColumns, worldChunkSize * numWorldChunkRows, false, Image.Format.Rgba8); _tileTypeMapImage.Create(worldChunkSize * numWorldChunkColumns, worldChunkSize * numWorldChunkRows, false, Image.Format.Rgba8); foreach (Vector2 chunkIndex in _activeChunkIndices) { WorldChunk worldChunk = GetOrCreateWorldChunk(chunkIndex, 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 (Vector2 chunkIndex in _activeChunkIndices) { 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() { HexCell cellSouthWest = HexGrid.GetHexAtOffset(_chunkIndexSouthWest * ChunkSize); HexGrid.SetBoundsOffset(cellSouthWest, ChunkSize * new Vector2(NumChunkColumns, NumChunkRows)); } public void MarkCellUnwalkable(HexCell cell) { HexGrid.AddObstacle(cell); } public float GetHexCost(Entity entity, HexCell cell) { float nextHexCost = HexGrid.GetHexCost(cell); if (nextHexCost != 0) { Vector2 nextOffset = cell.OffsetCoords; Vector2 chunkIndex = GetChunkIndexFromOffsetCoord(nextOffset); if (!_cachedWorldChunks.ContainsKey(chunkIndex)) { return 0; } WorldChunk chunk = _cachedWorldChunks[chunkIndex]; Vector2 textureCoordinate = nextOffset - Vector2.One * ChunkSize * chunkIndex; Color tileTypeColor = chunk.TileTypeImage.GetPixel((int)textureCoordinate.x, (int)textureCoordinate.y); if (!IsColorEqualApprox(tileTypeColor, GrassColor) && !IsColorEqualApprox(tileTypeColor, DarkGrassColor)) { nextHexCost = 0; } } return nextHexCost; } public float GetMoveCost(Entity entity, HexCell currentHex, HexCell nextHex) { if (GetHexCost(entity, nextHex) == 0) { return 0; } return HexGrid.GetMoveCost(currentHex.AxialCoords, new HexCell(nextHex.AxialCoords - currentHex.AxialCoords).CubeCoords); } public List FindPath(Entity entity, HexCell startHex, HexCell goalHex) { if (State != GenerationState.Done) { return new List(); } Vector2 goalAxialCoords = goalHex.AxialCoords; SimplePriorityQueue frontier = new(); frontier.Enqueue(startHex.AxialCoords, 0); System.Collections.Generic.Dictionary cameFrom = new(); System.Collections.Generic.Dictionary costSoFar = new(); cameFrom.Add(startHex.AxialCoords, startHex.AxialCoords); costSoFar.Add(startHex.AxialCoords, 0); int FindPathCheckedCellCount = 0; while (frontier.Any()) { FindPathCheckedCellCount++; HexCell currentHex = new(frontier.Dequeue()); Vector2 currentAxial = currentHex.AxialCoords; if (currentHex == goalHex) { break; } foreach (HexCell nextHex in currentHex.GetAllAdjacent()) { Vector2 nextAxial = nextHex.AxialCoords; float moveCost = GetMoveCost(entity, currentHex, nextHex); if (nextHex == goalHex && moveCost == 0 && GetHexCost(entity, nextHex) == 0) { // Goal ist an obstacle cameFrom[nextHex.AxialCoords] = currentHex.AxialCoords; frontier.Clear(); break; } if (moveCost == 0) { continue; } moveCost += costSoFar[currentHex.AxialCoords]; if (!costSoFar.ContainsKey(nextHex.AxialCoords) || moveCost < costSoFar[nextHex.AxialCoords]) { costSoFar[nextHex.AxialCoords] = moveCost; float priority = moveCost + nextHex.DistanceTo(goalHex); frontier.Enqueue(nextHex.AxialCoords, priority); cameFrom[nextHex.AxialCoords] = currentHex.AxialCoords; } } } // GD.Print("Checked Cell Count: " + FindPathCheckedCellCount); List result = new(); if (!cameFrom.ContainsKey(goalHex.AxialCoords)) { GD.Print("Failed to find path from " + startHex + " to " + goalHex); return result; } if (HexGrid.GetHexCost(goalAxialCoords) != 0) { result.Add(goalHex); } HexCell pathHex = goalHex; while (pathHex != startHex) { pathHex = new HexCell(cameFrom[pathHex.AxialCoords]); result.Insert(0, pathHex); } return result; } public bool CheckSweptTriangleCellCollision(Entity entity, Vector3 startWorld, Vector3 endWorld, float radius) { Vector2 startPlane = new(startWorld.x, startWorld.z); Vector2 endPlane = new(endWorld.x, endWorld.z); Vector2 directionPlane = (endPlane - startPlane).Normalized(); Vector2 sidePlane = directionPlane.Rotated(Mathf.Pi * 0.5f); List cells = HexGrid.GetCellsForLine(startPlane + directionPlane * radius, endPlane + directionPlane * radius); foreach (HexCell cell in cells) { if (GetHexCost(entity, cell) == 0) { return true; } } cells = HexGrid.GetCellsForLine(startPlane + sidePlane * radius, endPlane + sidePlane * radius); foreach (HexCell cell in cells) { if (GetHexCost(entity, cell) == 0) { return true; } } cells = HexGrid.GetCellsForLine(startPlane - sidePlane * radius, endPlane - sidePlane * radius); foreach (HexCell cell in cells) { if (GetHexCost(entity, cell) == 0) { return true; } } return false; } public List SmoothPath(Entity entity, List navigationPoints) { if (navigationPoints.Count <= 2) { return navigationPoints; } Vector3 bodyGlobalTranslation = entity.GlobalTranslation; List smoothedPath = new(); int startIndex = 0; int endIndex = navigationPoints.Count > 1 ? 1 : 0; smoothedPath.Add(navigationPoints[startIndex]); Vector3 startPoint = navigationPoints[startIndex].WorldPosition; while (endIndex != navigationPoints.Count) { Vector3 endPoint = navigationPoints[endIndex].WorldPosition; if (CheckSweptTriangleCellCollision(entity, startPoint, endPoint, 0.27f)) { if (endIndex - startIndex == 1) { GD.Print("Aborting SmoothPath: input path passes through collision geometry."); entity.GlobalTranslation = bodyGlobalTranslation; return smoothedPath; } smoothedPath.Add(navigationPoints[endIndex - 1]); startIndex = endIndex - 1; startPoint = navigationPoints[startIndex].WorldPosition; entity.GlobalTranslation = startPoint; continue; } if (endIndex == navigationPoints.Count - 1) { break; } endIndex += 1; } smoothedPath.Add(navigationPoints[endIndex]); entity.GlobalTranslation = bodyGlobalTranslation; return smoothedPath; } public override void _Process(float delta) { GenerationState oldState = State; UpdateGenerationState(); if (oldState != GenerationState.Done && State == GenerationState.Done) { UpdateWorldViewTexture(); } while (_removedSpatialNodes.Count > 0) { GD.Print("Queueing deletion of " + _removedSpatialNodes[0]); _removedSpatialNodes[0].QueueFree(); _removedSpatialNodes.RemoveAt(0); } } private void UpdateGenerationState() { if (State == GenerationState.Heightmap) { int numChunksGeneratingHeightmap = 0; foreach (Vector2 chunkIndex in _addedChunkIndices) { WorldChunk chunk = _cachedWorldChunks[chunkIndex]; if (chunk.HeightMapFrameCount > 0) { numChunksGeneratingHeightmap++; } } if (numChunksGeneratingHeightmap == 0) { // assign height map images foreach (Vector2 chunkIndex in _addedChunkIndices) { WorldChunk chunk = _cachedWorldChunks[chunkIndex]; chunk.SetHeightmap(chunk.HeightmapOffscreenViewport.GetTexture()); } State = GenerationState.TileType; } } else if (State == GenerationState.TileType) { int numChunksGeneratingTileType = 0; foreach (Vector2 chunkIndex in _addedChunkIndices) { WorldChunk chunk = _cachedWorldChunks[chunkIndex]; if (chunk.TileTypeMapFrameCount > 0) { numChunksGeneratingTileType++; } } if (numChunksGeneratingTileType == 0) { State = GenerationState.Objects; } } else if (State == GenerationState.Objects) { // generate objects foreach (Vector2 chunkIndex in _addedChunkIndices) { PopulateChunk(_cachedWorldChunks[chunkIndex]); } _addedChunkIndices.Clear(); State = GenerationState.Done; } } private void OnEntityClicked(Entity entity) { EmitSignal("EntityClicked", entity); } public void OnTileClicked(HexTile3D tile) { EmitSignal("TileClicked", tile); } public void OnTileHovered(HexTile3D tile) { EmitSignal("TileHovered", tile); } public void OnBlockingSpatialRemoved(Spatial spatialNode) { if (spatialNode.IsQueuedForDeletion()) { return; } HexGrid.RemoveObstacle(HexGrid.GetHexAt(new Vector2(spatialNode.GlobalTranslation.x, spatialNode.GlobalTranslation.z))); _removedSpatialNodes.Add(spatialNode); } }