GodotComponentTest/scenes/World.cs

363 lines
13 KiB
C#
Raw Normal View History

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
2023-10-08 21:38:49 +02:00
public const int ChunkSize = 16;
public const int NumChunkRows = 3;
public const int NumChunkColumns = NumChunkRows;
private List<Vector2> _activeChunkIndices = new();
private readonly List<Vector2> _addedChunkIndices = new();
private readonly Godot.Collections.Dictionary<Vector2, WorldChunk> _cachedWorldChunks;
private Rect2 _centerChunkRect;
// delegate void OnCoordClicked(Vector2 world_pos);
// other members
private Vector2 _centerPlaneCoord;
private readonly List<Vector2> _removedChunkIndices = new();
private TileInstanceManager _tileInstanceManager;
2023-11-01 17:43:47 +01:00
private readonly Image _heightmapImage = new();
private ImageTexture _heightmapTexture;
private readonly Image _tileTypeMapImage = new();
private ImageTexture _viewTileTypeTexture;
// referenced scenes
private readonly PackedScene _worldChunkScene = GD.Load<PackedScene>("res://scenes/WorldChunk.tscn");
public Vector2 CenterChunkIndex = Vector2.Zero;
public Spatial Chunks;
public Color DebugColor;
public HexGrid HexGrid = new();
2023-11-01 17:43:47 +01:00
private OpenSimplexNoise _noiseGenerator = new();
public int Seed = 0;
public GenerationState State = GenerationState.Done;
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);
_tileInstanceManager = (TileInstanceManager)FindNode("TileInstanceManager");
Debug.Assert(_tileInstanceManager != null);
InitNoiseGenerator();
SetCenterPlaneCoord(Vector2.Zero);
}
public void InitNoiseGenerator()
{
2023-11-01 17:43:47 +01:00
_noiseGenerator = new OpenSimplexNoise();
2023-11-01 17:43:47 +01:00
_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)
{
2023-10-08 21:38:49 +02:00
if (IsTileCached(xIndex, yIndex))
{
var cachedChunk = _cachedWorldChunks[new Vector2(xIndex, yIndex)];
return cachedChunk;
}
return CreateWorldChunk(xIndex, yIndex, debugColor);
}
2023-10-08 21:38:49 +02:00
private bool IsTileCached(int xIndex, int yIndex)
{
return _cachedWorldChunks.ContainsKey(new Vector2(xIndex, yIndex));
2023-10-08 21:38:49 +02:00
}
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);
2023-10-08 21:38:49 +02:00
_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;
}
2023-10-08 21:38:49 +02:00
// mark all chunks as retired
Godot.Collections.Dictionary<Vector2, WorldChunk> oldCachedChunks = new(_cachedWorldChunks);
2023-10-08 21:38:49 +02:00
// 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;
2023-10-08 21:38:49 +02:00
// load or create adjacent chunks
_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));
_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()));
2023-10-08 21:38:49 +02:00
// unload retired chunks
_removedChunkIndices.Clear();
_addedChunkIndices.Clear();
2023-10-08 21:38:49 +02:00
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;
2023-10-08 21:38:49 +02:00
}
}
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();
2023-11-01 17:43:47 +01:00
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);
2023-10-08 21:38:49 +02:00
}
private void RemoveChunk(Vector2 cachedChunkKey)
2023-10-08 21:38:49 +02:00
{
_cachedWorldChunks.Remove(cachedChunkKey);
_removedChunkIndices.Add(cachedChunkKey);
2023-10-08 21:38:49 +02:00
foreach (WorldChunk chunk in Chunks.GetChildren())
if (chunk.ChunkIndex == new Vector2(cachedChunkKey.x, cachedChunkKey.y))
2023-10-08 21:38:49 +02:00
chunk.QueueFree();
}
private Tuple<int, int> GetChunkTupleFromPlaneCoord(Vector2 planeCoord)
{
var centerOffsetCoord = HexGrid.GetHexAt(planeCoord);
var chunkIndexFloat = (centerOffsetCoord.OffsetCoords / ChunkSize).Floor();
var chunkIndex = new Tuple<int, int>((int)chunkIndexFloat.x, (int)chunkIndexFloat.y);
2023-10-08 21:38:49 +02:00
return chunkIndex;
}
public void SetCenterPlaneCoord(Vector2 centerPlaneCoord)
{
if (!_centerChunkRect.HasPoint(centerPlaneCoord)) UpdateCenterChunkFromPlaneCoord(centerPlaneCoord);
}
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);
var chunkIndexSouthWest = Vector2.Inf;
var 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;
_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);
_tileInstanceManager.WorldTextureCoordinateOffset = chunkIndexSouthWest * worldChunkSize;
EmitSignal("OnWorldViewTileTypeImageChanged", _tileTypeMapImage);
EmitSignal("OnHeightmapImageChanged", _heightmapImage);
}
public override void _Process(float delta)
{
2023-11-01 17:43:47 +01:00
var oldState = State;
2023-11-01 17:43:47 +01:00
UpdateGenerationState();
2023-11-01 17:43:47 +01:00
if (oldState != GenerationState.Done && State == GenerationState.Done)
{
UpdateWorldViewTexture();
EmitSignal("OnTilesChanged", _removedChunkIndices.ToArray(), _addedChunkIndices.ToArray());
}
}
2023-11-01 17:43:47 +01:00
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());
}
2023-11-01 17:43:47 +01:00
State = GenerationState.TileType;
}
}
else if (State == GenerationState.TileType)
{
2023-11-01 17:43:47 +01:00
var numChunksGeneratingTileType = 0;
foreach (var chunkIndex in _addedChunkIndices)
{
var chunk = _cachedWorldChunks[chunkIndex];
2023-11-01 17:43:47 +01:00
if (chunk.TileTypeMapFrameCount > 0) numChunksGeneratingTileType++;
}
2023-11-01 17:43:47 +01:00
if (numChunksGeneratingTileType == 0)
{
State = GenerationState.Objects;
}
}
else if (State == GenerationState.Objects)
{
// generate objects
State = GenerationState.Done;
}
}
// 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);
}