453 lines
17 KiB
C#
453 lines
17 KiB
C#
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 = 10;
|
|
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<Vector2> _addedChunkIndices = new();
|
|
private readonly Godot.Collections.Dictionary<Vector2, WorldChunk> _cachedWorldChunks;
|
|
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;
|
|
|
|
// delegate void OnCoordClicked(Vector2 world_pos);
|
|
|
|
// other members
|
|
private Vector2 _centerPlaneCoord;
|
|
private Array<Spatial> _grassAssets;
|
|
private ImageTexture _heightmapTexture;
|
|
|
|
private OpenSimplexNoise _noiseGenerator = new();
|
|
private Array<Spatial> _rockAssets;
|
|
private TileInstanceManager _tileInstanceManager;
|
|
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.Done;
|
|
public Vector2 WorldTextureCoordinateOffset;
|
|
|
|
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();
|
|
|
|
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);
|
|
|
|
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<Spatial> 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);
|
|
|
|
var treeAsset = SelectAsset(offsetCoord, _treeAssets, environmentRandom, 0.05);
|
|
if (treeAsset != null) chunk.Entities.AddChild(treeAsset);
|
|
// 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 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<Vector2, WorldChunk> 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<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()));
|
|
|
|
// 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<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);
|
|
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);
|
|
|
|
WorldTextureCoordinateOffset = chunkIndexSouthWest * worldChunkSize;
|
|
|
|
EmitSignal("OnWorldViewTileTypeImageChanged", _tileTypeMapImage);
|
|
EmitSignal("OnHeightmapImageChanged", _heightmapImage);
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
|
|
// 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);
|
|
} |