2023-10-05 18:17:48 +02:00
|
|
|
using Godot;
|
|
|
|
using System;
|
|
|
|
using System.Collections.Generic;
|
|
|
|
using System.Diagnostics;
|
2023-10-23 21:22:53 +02:00
|
|
|
using System.Linq;
|
2023-10-14 21:54:06 +02:00
|
|
|
using Godot.Collections;
|
2023-10-05 18:17:48 +02:00
|
|
|
|
|
|
|
public class World : Spatial
|
|
|
|
{
|
2023-10-23 21:22:53 +02:00
|
|
|
public enum GenerationState
|
|
|
|
{
|
|
|
|
Undefined,
|
|
|
|
Heightmap,
|
|
|
|
TileType,
|
|
|
|
Objects,
|
|
|
|
Done
|
|
|
|
}
|
|
|
|
public GenerationState State = GenerationState.Done;
|
2023-10-30 22:20:32 +01:00
|
|
|
public Vector2 CenterChunkIndex = Vector2.Zero;
|
2023-10-23 21:22:53 +02:00
|
|
|
|
2023-10-05 18:17:48 +02:00
|
|
|
// referenced scenes
|
|
|
|
private PackedScene _worldChunkScene = GD.Load<PackedScene>("res://scenes/WorldChunk.tscn");
|
|
|
|
|
|
|
|
// constants
|
2023-10-08 21:38:49 +02:00
|
|
|
public const int ChunkSize = 16;
|
2023-10-30 22:20:32 +01:00
|
|
|
public const int NumChunkRows = 3;
|
|
|
|
public const int NumChunkColumns = NumChunkRows;
|
2023-10-23 21:22:53 +02:00
|
|
|
public int Seed = 0;
|
2023-10-05 18:17:48 +02:00
|
|
|
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
|
2023-10-14 21:54:06 +02:00
|
|
|
[Signal]
|
|
|
|
delegate void OnTilesChanged(Array<Vector2> removedChunkIndices, Array<Vector2> addedChunkIndices);
|
|
|
|
|
2023-10-05 18:17:48 +02:00
|
|
|
// delegate void OnCoordClicked(Vector2 world_pos);
|
|
|
|
|
|
|
|
// other members
|
|
|
|
private Vector2 _centerPlaneCoord;
|
2023-10-23 21:22:53 +02:00
|
|
|
private Rect2 _centerChunkRect = new Rect2();
|
2023-10-14 21:54:06 +02:00
|
|
|
private Godot.Collections.Dictionary<Vector2, WorldChunk> _cachedWorldChunks;
|
|
|
|
private List<Vector2> _activeChunkIndices = new();
|
|
|
|
private List<Vector2> _addedChunkIndices = new();
|
|
|
|
private List<Vector2> _removedChunkIndices = new();
|
2023-10-05 18:17:48 +02:00
|
|
|
|
2023-10-23 21:22:53 +02:00
|
|
|
private OpenSimplexNoise noiseGenerator = new();
|
|
|
|
|
2023-10-05 18:17:48 +02:00
|
|
|
public World()
|
|
|
|
{
|
|
|
|
Debug.Assert(ChunkSize % 2 == 0);
|
|
|
|
|
2023-10-14 21:54:06 +02:00
|
|
|
_cachedWorldChunks = new Godot.Collections.Dictionary<Vector2, WorldChunk>();
|
2023-10-05 18:17:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// Called when the node enters the scene tree for the first time.
|
|
|
|
public override void _Ready()
|
|
|
|
{
|
|
|
|
Chunks = (Spatial)FindNode("Chunks");
|
|
|
|
Debug.Assert(Chunks != null);
|
2023-10-23 21:22:53 +02:00
|
|
|
|
|
|
|
InitNoiseGenerator();
|
2023-10-05 18:17:48 +02:00
|
|
|
|
|
|
|
SetCenterPlaneCoord(Vector2.Zero);
|
|
|
|
}
|
|
|
|
|
2023-10-23 21:22:53 +02:00
|
|
|
public void InitNoiseGenerator()
|
|
|
|
{
|
|
|
|
noiseGenerator = new OpenSimplexNoise();
|
|
|
|
|
|
|
|
noiseGenerator.Seed = Seed;
|
|
|
|
noiseGenerator.Octaves = 1;
|
|
|
|
noiseGenerator.Period = 10;
|
|
|
|
noiseGenerator.Persistence = 0.5f;
|
|
|
|
noiseGenerator.Lacunarity = 2;
|
|
|
|
}
|
|
|
|
|
2023-10-05 18:17:48 +02:00
|
|
|
public WorldChunk GetOrCreateWorldChunk(int xIndex, int yIndex, Color debugColor)
|
|
|
|
{
|
2023-10-08 21:38:49 +02:00
|
|
|
if (IsTileCached(xIndex, yIndex))
|
2023-10-05 18:17:48 +02:00
|
|
|
{
|
2023-10-14 21:54:06 +02:00
|
|
|
WorldChunk cachedChunk = _cachedWorldChunks[new Vector2(xIndex, yIndex)];
|
2023-10-05 18:17:48 +02:00
|
|
|
return cachedChunk;
|
|
|
|
}
|
|
|
|
|
|
|
|
return CreateWorldChunk(xIndex, yIndex, debugColor);
|
|
|
|
}
|
|
|
|
|
2023-10-08 21:38:49 +02:00
|
|
|
private bool IsTileCached(int xIndex, int yIndex)
|
|
|
|
{
|
2023-10-14 21:54:06 +02:00
|
|
|
return _cachedWorldChunks.ContainsKey(new Vector2(xIndex, yIndex));
|
2023-10-08 21:38:49 +02:00
|
|
|
}
|
|
|
|
|
2023-10-05 18:17:48 +02:00
|
|
|
private WorldChunk CreateWorldChunk(int xIndex, int yIndex, Color debugColor)
|
|
|
|
{
|
|
|
|
WorldChunk result = (WorldChunk)_worldChunkScene.Instance();
|
2023-10-23 21:22:53 +02:00
|
|
|
result.SetSize(ChunkSize);
|
2023-10-05 18:17:48 +02:00
|
|
|
|
|
|
|
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;
|
|
|
|
|
2023-10-08 21:38:49 +02:00
|
|
|
result.ChunkAddress = new Vector2(xIndex, yIndex);
|
2023-10-05 18:17:48 +02:00
|
|
|
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);
|
2023-10-14 21:54:06 +02:00
|
|
|
Vector2 chunkIndex = new Vector2(xIndex, yIndex);
|
2023-10-08 21:38:49 +02:00
|
|
|
_cachedWorldChunks.Add(chunkIndex, result);
|
2023-10-05 18:17:48 +02:00
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
public void UpdateCenterChunkFromPlaneCoord(Vector2 planeCoord)
|
|
|
|
{
|
2023-10-23 21:22:53 +02:00
|
|
|
if (State != GenerationState.Done)
|
|
|
|
{
|
|
|
|
GD.PrintErr("Cannot update chunk to new planeCoord " + planeCoord + ": Chunk generation not yet finished!");
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
|
2023-10-08 21:38:49 +02:00
|
|
|
// mark all chunks as retired
|
2023-10-14 21:54:06 +02:00
|
|
|
Godot.Collections.Dictionary<Vector2, WorldChunk> oldCachedChunks = new(_cachedWorldChunks);
|
2023-10-08 21:38:49 +02:00
|
|
|
|
|
|
|
// set new center chunk
|
|
|
|
var chunkIndex = GetChunkTupleFromPlaneCoord(planeCoord);
|
2023-10-30 22:20:32 +01:00
|
|
|
CenterChunkIndex = new Vector2(chunkIndex.Item1, chunkIndex.Item2);
|
|
|
|
|
2023-10-05 18:17:48 +02:00
|
|
|
WorldChunk currentChunk = GetOrCreateWorldChunk(chunkIndex.Item1, chunkIndex.Item2,
|
|
|
|
new Color(GD.Randf(), GD.Randf(), GD.Randf()));
|
2023-10-23 21:22:53 +02:00
|
|
|
_centerChunkRect = currentChunk.PlaneRect;
|
2023-10-08 21:38:49 +02:00
|
|
|
|
|
|
|
// load or create adjacent chunks
|
2023-10-14 21:54:06 +02:00
|
|
|
_activeChunkIndices = new List<Vector2>();
|
|
|
|
_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));
|
2023-10-08 21:38:49 +02:00
|
|
|
|
2023-10-14 21:54:06 +02:00
|
|
|
_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));
|
2023-10-08 21:38:49 +02:00
|
|
|
|
2023-10-14 21:54:06 +02:00
|
|
|
_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));
|
2023-10-08 21:38:49 +02:00
|
|
|
|
2023-10-30 22:20:32 +01:00
|
|
|
Debug.Assert(_activeChunkIndices.Count == NumChunkRows * NumChunkColumns);
|
|
|
|
|
2023-10-14 21:54:06 +02:00
|
|
|
foreach(Vector2 activeChunkIndex in _activeChunkIndices)
|
2023-10-08 21:38:49 +02:00
|
|
|
{
|
2023-10-14 21:54:06 +02:00
|
|
|
GetOrCreateWorldChunk((int) activeChunkIndex.x, (int) activeChunkIndex.y, new Color(GD.Randf(), GD.Randf(), GD.Randf()));
|
2023-10-08 21:38:49 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
// 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);
|
|
|
|
}
|
|
|
|
}
|
2023-10-23 21:22:53 +02:00
|
|
|
|
|
|
|
if (_addedChunkIndices.Count > 0)
|
|
|
|
{
|
|
|
|
State = GenerationState.Heightmap;
|
|
|
|
}
|
2023-10-08 21:38:49 +02:00
|
|
|
}
|
|
|
|
|
2023-10-14 21:54:06 +02:00
|
|
|
private void RemoveChunk(Vector2 cachedChunkKey)
|
2023-10-08 21:38:49 +02:00
|
|
|
{
|
|
|
|
_cachedWorldChunks.Remove(cachedChunkKey);
|
|
|
|
_removedChunkIndices.Add(cachedChunkKey);
|
|
|
|
|
|
|
|
foreach (WorldChunk chunk in Chunks.GetChildren())
|
|
|
|
{
|
2023-10-14 21:54:06 +02:00
|
|
|
if (chunk.ChunkAddress == new Vector2(cachedChunkKey.x, cachedChunkKey.y))
|
2023-10-08 21:38:49 +02:00
|
|
|
{
|
|
|
|
chunk.QueueFree();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
private Tuple<int, int> GetChunkTupleFromPlaneCoord(Vector2 planeCoord)
|
|
|
|
{
|
|
|
|
HexCell centerOffsetCoord = HexGrid.GetHexAt(planeCoord);
|
|
|
|
Vector2 chunkIndexFloat = (centerOffsetCoord.OffsetCoords / (float)ChunkSize).Floor();
|
|
|
|
Tuple<int, int> chunkIndex = new Tuple<int, int>((int)chunkIndexFloat.x, (int)chunkIndexFloat.y);
|
|
|
|
return chunkIndex;
|
2023-10-05 18:17:48 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
public void SetCenterPlaneCoord(Vector2 centerPlaneCoord)
|
|
|
|
{
|
2023-10-23 21:22:53 +02:00
|
|
|
if (!_centerChunkRect.HasPoint(centerPlaneCoord))
|
2023-10-05 18:17:48 +02:00
|
|
|
{
|
|
|
|
UpdateCenterChunkFromPlaneCoord(centerPlaneCoord);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-10-23 21:22:53 +02:00
|
|
|
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());
|
|
|
|
}
|
|
|
|
}
|
2023-10-05 18:17:48 +02:00
|
|
|
}
|