777 lines
28 KiB
C#
777 lines
28 KiB
C#
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 readonly System.Collections.Generic.Dictionary<int, HexTile3D.TileTypeInfo> _tileTypeInfos = new();
|
|
|
|
private readonly Godot.Collections.Dictionary<Vector2, WorldChunk> _cachedWorldChunks;
|
|
private readonly List<Vector2> _addedChunkIndices = new();
|
|
private readonly List<WorldChunk> _deactivatedWorldChunks = new();
|
|
private readonly Image _heightmapImage = new();
|
|
private readonly List<Vector2> _removedChunkIndices = new();
|
|
private readonly Image _tileTypeMapImage = new();
|
|
|
|
// referenced scenes
|
|
private readonly PackedScene _worldChunkScene = GD.Load<PackedScene>("res://scenes/WorldChunk.tscn");
|
|
|
|
private List<Vector2> _activeChunkIndices = new();
|
|
private Rect2 _centerChunkRect;
|
|
private readonly List<Spatial> _removedSpatialNodes = new();
|
|
|
|
// delegate void OnCoordClicked(Vector2 world_pos);
|
|
|
|
// other members
|
|
private Vector2 _chunkIndexNorthEast;
|
|
private Vector2 _chunkIndexSouthWest;
|
|
private Array<Spatial> _grassAssets;
|
|
private ImageTexture _heightmapTexture;
|
|
|
|
private OpenSimplexNoise _noiseGenerator = new();
|
|
private Array<Spatial> _rockAssets;
|
|
private MultiMeshInstance _tileMultiMeshInstance;
|
|
private int _usedTileMeshInstances;
|
|
private Array<Spatial> _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<Vector2> removedChunkIndices, Array<Vector2> 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<Vector2, WorldChunk>();
|
|
}
|
|
|
|
// 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<Spatial>("Assets").Visible = false;
|
|
|
|
_rockAssets = new Array<Spatial>();
|
|
foreach (Spatial asset in GetNode<Node>("Assets/Rocks").GetChildren()) {
|
|
_rockAssets.Add(asset);
|
|
}
|
|
|
|
_grassAssets = new Array<Spatial>();
|
|
foreach (Spatial asset in GetNode<Node>("Assets/Grass").GetChildren()) {
|
|
_grassAssets.Add(asset);
|
|
}
|
|
|
|
_treeAssets = new Array<Spatial>();
|
|
foreach (Spatial asset in GetNode<Node>("Assets/Trees").GetChildren()) {
|
|
_treeAssets.Add(asset);
|
|
}
|
|
|
|
_tileTypeInfos[0] = new HexTile3D.TileTypeInfo("Undefined", Colors.Black, 0xffff);
|
|
LoadTileTypeInfo("DeepWater", (int)Entity.EntityMaskEnum.Water);
|
|
LoadTileTypeInfo("Water", (int)Entity.EntityMaskEnum.Water);
|
|
LoadTileTypeInfo("LightWater", (int)Entity.EntityMaskEnum.Water);
|
|
LoadTileTypeInfo("Sand", (int)Entity.EntityMaskEnum.Ground);
|
|
LoadTileTypeInfo("Grass", (int)Entity.EntityMaskEnum.Ground);
|
|
LoadTileTypeInfo("Forest", (int)Entity.EntityMaskEnum.Ground);
|
|
LoadTileTypeInfo("Rock", (int)Entity.EntityMaskEnum.Ground);
|
|
LoadTileTypeInfo("Sand", (int)Entity.EntityMaskEnum.Ground);
|
|
}
|
|
|
|
private void LoadTileTypeInfo(string tileTypeName, ushort tileTypeMask) {
|
|
ShaderMaterial worldTileTypeShaderMaterial =
|
|
GD.Load<ShaderMaterial>("res://materials/WorldTileTypeMaterial.tres");
|
|
|
|
Color tileTypeColor = (Color)worldTileTypeShaderMaterial.GetShaderParam(tileTypeName + "Color");
|
|
_tileTypeInfos[tileTypeColor.ToRgba32()] =
|
|
new HexTile3D.TileTypeInfo(tileTypeName, tileTypeColor, tileTypeMask);
|
|
}
|
|
|
|
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<Spatial> 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)) {
|
|
Vector2 textureCoord = new(textureCoordU, textureCoordV);
|
|
Vector2 offsetCoord = chunk.ChunkIndex * ChunkSize + textureCoord;
|
|
|
|
HexTile3D.TileTypeInfo tileTypeInfo = GetTileTypeInfoAtOffset(offsetCoord);
|
|
|
|
if (tileTypeInfo.Name == "Rock") {
|
|
Spatial rockAsset = SelectAsset(textureCoord, _rockAssets, environmentRandom, 0.15);
|
|
if (rockAsset != null) {
|
|
chunk.Entities.AddChild(rockAsset);
|
|
MarkCellUnwalkable(HexGrid.GetHexAtOffset(offsetCoord));
|
|
}
|
|
} else if (tileTypeInfo.Name == "Grass" || tileTypeInfo.Name == "DarkGrass") {
|
|
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)
|
|
} else if (tileTypeInfo.Name == "Water" || tileTypeInfo.Name == "LightWater" ||
|
|
tileTypeInfo.Name == "DeepWater") {
|
|
Spatial rockAsset = SelectAsset(textureCoord, _rockAssets, environmentRandom, 0.01);
|
|
if (rockAsset != null) {
|
|
chunk.Entities.AddChild(rockAsset);
|
|
MarkCellUnwalkable(HexGrid.GetHexAtOffset(offsetCoord));
|
|
}
|
|
// {
|
|
// 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<Vector2, WorldChunk> oldCachedChunks = new(_cachedWorldChunks);
|
|
|
|
// set new center chunk
|
|
CenterChunkIndex = GetChunkTupleFromPlaneCoord(planeCoord);
|
|
|
|
// load or create adjacent chunks
|
|
_activeChunkIndices = new List<Vector2>();
|
|
_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()));
|
|
}
|
|
|
|
Debug.Assert(_activeChunkIndices.Count == NumChunkRows * NumChunkColumns);
|
|
|
|
foreach (Vector2 chunkKey in _activeChunkIndices) {
|
|
if (!oldCachedChunks.ContainsKey(chunkKey)) {
|
|
ActivateChunk(_cachedWorldChunks[chunkKey], chunkKey);
|
|
State = GenerationState.Heightmap;
|
|
}
|
|
}
|
|
|
|
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);
|
|
|
|
UpdateChunkBounds();
|
|
|
|
UpdateNavigationBounds();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
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, (uint)Texture.FlagsEnum.ConvertToLinear);
|
|
|
|
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;
|
|
Color tileTypeColor = GetTileTypeColorAtOffset(nextOffset);
|
|
|
|
if (_tileTypeInfos.TryGetValue(tileTypeColor.ToRgba32(), out HexTile3D.TileTypeInfo info)) {
|
|
int tileTypeMask = info.TileTypeMask;
|
|
if ((entity.EntityMask ^ tileTypeMask) != 0) {
|
|
nextHexCost = 0;
|
|
}
|
|
} else {
|
|
GD.Print("Could not find tile type info for color " + tileTypeColor);
|
|
}
|
|
}
|
|
|
|
return nextHexCost;
|
|
}
|
|
|
|
private HexTile3D.TileTypeInfo GetTileTypeInfoAtOffset(Vector2 offsetCoord) {
|
|
Color tileTypeColor = GetTileTypeColorAtOffset(offsetCoord);
|
|
if (_tileTypeInfos.TryGetValue(tileTypeColor.ToRgba32(), out HexTile3D.TileTypeInfo info)) {
|
|
return info;
|
|
}
|
|
|
|
return _tileTypeInfos[0];
|
|
}
|
|
|
|
public Color GetTileTypeColorAtOffset(Vector2 offsetCoord) {
|
|
Vector2 chunkIndex = GetChunkIndexFromOffsetCoord(offsetCoord);
|
|
|
|
if (!_cachedWorldChunks.ContainsKey(chunkIndex)) {
|
|
return Colors.Black;
|
|
}
|
|
|
|
WorldChunk chunk = _cachedWorldChunks[chunkIndex];
|
|
Vector2 textureCoordinate = offsetCoord - Vector2.One * ChunkSize * chunkIndex;
|
|
|
|
Color tileTypeColor = chunk.TileTypeImage.GetPixel((int)textureCoordinate.x, (int)textureCoordinate.y);
|
|
return tileTypeColor;
|
|
}
|
|
|
|
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<HexCell> FindPath(Entity entity, HexCell startHex, HexCell goalHex) {
|
|
if (State != GenerationState.Done) {
|
|
return new List<HexCell>();
|
|
}
|
|
|
|
Vector2 goalAxialCoords = goalHex.AxialCoords;
|
|
|
|
SimplePriorityQueue<Vector2, float> frontier = new();
|
|
frontier.Enqueue(startHex.AxialCoords, 0);
|
|
System.Collections.Generic.Dictionary<Vector2, Vector2> cameFrom = new();
|
|
System.Collections.Generic.Dictionary<Vector2, float> 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<HexCell> 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<HexCell> 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<NavigationPoint> SmoothPath(Entity entity, List<NavigationPoint> navigationPoints) {
|
|
if (navigationPoints.Count <= 2) {
|
|
return navigationPoints;
|
|
}
|
|
|
|
Vector3 bodyGlobalTranslation = entity.GlobalTranslation;
|
|
List<NavigationPoint> 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) {
|
|
if (State != GenerationState.Done) {
|
|
return;
|
|
}
|
|
|
|
EmitSignal("TileClicked", tile);
|
|
}
|
|
|
|
public void OnTileHovered(HexTile3D tile) {
|
|
if (State != GenerationState.Done) {
|
|
return;
|
|
}
|
|
|
|
EmitSignal("TileHovered", tile);
|
|
HexTile3D.TileTypeInfo tileTypeInfo = GetTileTypeInfoAtOffset(tile.OffsetCoords);
|
|
}
|
|
|
|
public void OnBlockingSpatialRemoved(Spatial spatialNode) {
|
|
if (spatialNode.IsQueuedForDeletion()) {
|
|
return;
|
|
}
|
|
|
|
HexGrid.RemoveObstacle(HexGrid.GetHexAt(new Vector2(spatialNode.GlobalTranslation.x,
|
|
spatialNode.GlobalTranslation.z)));
|
|
_removedSpatialNodes.Add(spatialNode);
|
|
}
|
|
} |