Infinite world gen

This commit is contained in:
2025-08-29 13:35:03 +08:00
parent 9408651ea8
commit 862f11d445
3 changed files with 286 additions and 130 deletions

View File

@@ -5,6 +5,7 @@
[node name="Player" type="CharacterBody2D"] [node name="Player" type="CharacterBody2D"]
script = ExtResource("1_08t41") script = ExtResource("1_08t41")
MinZoom = 0.1
MaxZoom = 5.0 MaxZoom = 5.0
[node name="Sprite2D" type="Sprite2D" parent="."] [node name="Sprite2D" type="Sprite2D" parent="."]

View File

@@ -34,6 +34,7 @@ public partial class Player : CharacterBody2D
_cameraTargetZoom = _camera.Zoom; _cameraTargetZoom = _camera.Zoom;
AddToGroup(ItemPickup.PickupGroupName); AddToGroup(ItemPickup.PickupGroupName);
AddToGroup(NaturalResourceGenerator.ChunkTrackerGroupName);
} }
public override void _Input(InputEvent @event) public override void _Input(InputEvent @event)

View File

@@ -1,153 +1,307 @@
using System;
using Godot; using Godot;
using System.Collections.Generic; using System.Collections.Generic;
using AceFieldNewHorizon.Scripts.Entities;
using AceFieldNewHorizon.Scripts.Tiles; using AceFieldNewHorizon.Scripts.Tiles;
namespace AceFieldNewHorizon.Scripts.System; namespace AceFieldNewHorizon.Scripts.System;
public partial class NaturalResourceGenerator : Node2D public partial class NaturalResourceGenerator : Node2D
{ {
[Export] public GridManager Grid { get; set; } public const string ChunkTrackerGroupName = "NrgTrackingTarget";
[Export] public BuildingRegistry Registry { get; set; }
[Export] public int MapWidth = 100;
[Export] public int MapHeight = 100;
[Export] public float StoneDensity = 0.1f; // 10% chance for stone
[Export] public float IronDensity = 0.03f; // 3% chance for iron (within stone)
[Export] public int MinStoneVeinSize = 1;
[Export] public int MaxStoneVeinSize = 5;
[Export] public int MinIronVeinSize = 1;
[Export] public int MaxIronVeinSize = 3;
[Export] public int Seed;
private RandomNumberGenerator _rng; [Export] public GridManager Grid { get; set; }
private readonly List<Vector2I> _groundTiles = []; [Export] public BuildingRegistry Registry { get; set; }
private readonly List<Vector2I> _stoneTiles = [];
private readonly List<Vector2I> _ironTiles = [];
public override void _Ready() [Export] public int ChunkSize = 16;
{ [Export] public int LoadDistance = 2; // Number of chunks to load in each direction
_rng = new RandomNumberGenerator(); [Export] public float StoneDensity = 0.1f;
_rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi()); [Export] public float IronDensity = 0.03f;
[Export] public int MinStoneVeinSize = 1;
GenerateTerrain(); [Export] public int MaxStoneVeinSize = 5;
PlaceResources(); [Export] public int MinIronVeinSize = 1;
} [Export] public int MaxIronVeinSize = 3;
[Export] public int Seed;
private void GenerateTerrain() private const string LogPrefix = "[NaturalGeneration]";
{
// First pass: Generate base ground tiles
for (int x = 0; x < MapWidth; x++)
{
for (int y = 0; y < MapHeight; y++)
{
var cell = new Vector2I(x, y);
_groundTiles.Add(cell);
PlaceTile("ground", cell);
}
}
}
private void PlaceResources() private RandomNumberGenerator _rng;
{ private readonly Dictionary<Vector2I, ChunkData> _loadedChunks = new();
// Create a copy of ground tiles for iteration private Vector2I _lastPlayerChunk = new(-1000, -1000);
var groundTilesToProcess = new List<Vector2I>(_groundTiles);
// Place stone veins
foreach (var cell in groundTilesToProcess)
{
if (_rng.Randf() < StoneDensity)
{
var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize);
PlaceVein(cell, "stone", veinSize, _stoneTiles);
}
}
// Create a copy of stone tiles for iteration private Player _player;
var stoneTilesToProcess = new List<Vector2I>(_stoneTiles);
// Place iron veins within stone
foreach (var stoneCell in stoneTilesToProcess)
{
if (_rng.Randf() < IronDensity)
{
var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize);
PlaceVein(stoneCell, "ore_iron", veinSize, _ironTiles);
}
}
}
private void PlaceVein(Vector2I startCell, string tileType, int maxVeinSize, ICollection<Vector2I> tileList) public override void _Ready()
{ {
var queue = new Queue<Vector2I>(); _rng = new RandomNumberGenerator();
var placed = new HashSet<Vector2I>(); _rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi());
queue.Enqueue(startCell); }
int placedCount = 0; public override void _Process(double delta)
{
while (queue.Count > 0 && placedCount < maxVeinSize) _player = GetTree().GetFirstNodeInGroup(ChunkTrackerGroupName) as Player;
{ if (_player != null)
var cell = queue.Dequeue(); {
if (placed.Contains(cell)) continue; UpdatePlayerPosition(_player.GlobalPosition);
if (!IsInBounds(cell)) continue; }
else
{
GD.PrintErr($"{LogPrefix} Player not found in group: {ChunkTrackerGroupName}");
}
}
switch (tileType) public void UpdatePlayerPosition(Vector2 playerPosition)
{ {
// For iron, make sure we're placing on stone var playerChunk = WorldToChunkCoords(playerPosition);
case "ore_iron" when !_stoneTiles.Contains(cell): if (playerChunk == _lastPlayerChunk) return;
continue; _lastPlayerChunk = playerChunk;
// Remove from previous layer if needed
case "ore_iron" when _stoneTiles.Contains(cell):
_stoneTiles.Remove(cell);
break;
case "stone" when _groundTiles.Contains(cell):
_groundTiles.Remove(cell);
break;
}
PlaceTile(tileType, cell); // Unload chunks outside load distance
tileList.Add(cell); var chunksToRemove = new List<Vector2I>();
placed.Add(cell); foreach (var chunkPos in _loadedChunks.Keys)
placedCount++; {
if (Mathf.Abs(chunkPos.X - playerChunk.X) > LoadDistance ||
Mathf.Abs(chunkPos.Y - playerChunk.Y) > LoadDistance)
{
chunksToRemove.Add(chunkPos);
}
}
// Add adjacent cells to queue foreach (var chunkPos in chunksToRemove)
for (var dx = -1; dx <= 1; dx++) {
{ UnloadChunk(chunkPos);
for (var dy = -1; dy <= 1; dy++) }
{
if (dx == 0 && dy == 0) continue; // Skip self
var neighbor = new Vector2I(cell.X + dx, cell.Y + dy);
if (!placed.Contains(neighbor) && IsInBounds(neighbor))
{
queue.Enqueue(neighbor);
}
}
}
}
}
private bool IsInBounds(Vector2I cell) // Load chunks around player
{ for (int x = -LoadDistance; x <= LoadDistance; x++)
return cell.X >= 0 && cell.X < MapWidth && {
cell.Y >= 0 && cell.Y < MapHeight; for (int y = -LoadDistance; y <= LoadDistance; y++)
} {
var chunkPos = new Vector2I(playerChunk.X + x, playerChunk.Y + y);
if (!_loadedChunks.ContainsKey(chunkPos))
{
GenerateChunk(chunkPos);
}
}
}
}
private void PlaceTile(string tileType, Vector2I cell) private void GenerateChunk(Vector2I chunkPos)
{ {
var building = Registry.GetBuilding(tileType); GD.Print($"{LogPrefix} Generating chunk at {chunkPos}");
if (building == null) return; var chunkData = new ChunkData();
var chunkWorldPos = ChunkToWorldCoords(chunkPos);
var scene = building.Scene; // First, place ground tiles
var instance = (BaseTile)scene.Instantiate(); for (int x = 0; x < ChunkSize; x++)
{
// Match PlacementManager's positioning logic for (int y = 0; y < ChunkSize; y++)
var rotatedSize = building.GetRotatedSize(0f); {
var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
instance.Position = GridUtils.GridToWorld(cell) + offset; if (!PlaceTile("ground", cell))
{
instance.ZIndex = (int)building.Layer; GD.PrintErr($"{LogPrefix} Failed to place ground at {cell}");
AddChild(instance); }
}
}
// Make sure to use the building's size from the registry // Then generate stone veins
Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer); var stoneVeins = 0;
} for (var x = 0; x < ChunkSize; x++)
{
for (var y = 0; y < ChunkSize; y++)
{
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
if (_rng.Randf() < StoneDensity)
{
var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize);
GD.Print($"{LogPrefix} Attempting to place stone vein at {cell} with size {veinSize}");
PlaceVein(cell, "stone", veinSize, chunkData.StoneTiles);
stoneVeins++;
}
}
}
GD.Print($"{LogPrefix} Placed {stoneVeins} stone veins in chunk {chunkPos}");
// Generate iron veins within stone
int ironVeins = 0;
foreach (var stoneCell in new List<Vector2I>(chunkData.StoneTiles))
{
if (_rng.Randf() < IronDensity)
{
var veinSize = _rng.RandiRange(MinIronVeinSize, MaxIronVeinSize);
GD.Print($"{LogPrefix} Attempting to place iron vein at {stoneCell} with size {veinSize}");
PlaceVein(stoneCell, "ore_iron", veinSize, chunkData.IronTiles);
ironVeins++;
}
}
GD.Print($"{LogPrefix} Placed {ironVeins} iron veins in chunk {chunkPos}");
_loadedChunks[chunkPos] = chunkData;
}
private void UnloadChunk(Vector2I chunkPos)
{
GD.Print($"{LogPrefix} Unloading chunk at {chunkPos}");
if (!_loadedChunks.TryGetValue(chunkPos, out var chunkData)) return;
// Remove all tiles in this chunk
var chunkWorldPos = ChunkToWorldCoords(chunkPos);
for (int x = 0; x < ChunkSize; x++)
{
for (int y = 0; y < ChunkSize; y++)
{
var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y);
// Free a 1x1 area for each cell
Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground);
}
}
_loadedChunks.Remove(chunkPos);
}
private Vector2I WorldToChunkCoords(Vector2 worldPos)
{
var cell = GridUtils.WorldToGrid(worldPos);
return new Vector2I(
(int)Mathf.Floor((float)cell.X / ChunkSize),
(int)Mathf.Floor((float)cell.Y / ChunkSize)
);
}
private Vector2I ChunkToWorldCoords(Vector2I chunkPos)
{
return new Vector2I(
chunkPos.X * ChunkSize,
chunkPos.Y * ChunkSize
);
}
private bool IsInLoadedChunk(Vector2I cell)
{
// Check the chunk where the cell is located
var chunkPos = new Vector2I(
(int)Mathf.Floor((float)cell.X / ChunkSize),
(int)Mathf.Floor((float)cell.Y / ChunkSize)
);
// Also check adjacent chunks since veins can cross chunk boundaries
for (var dx = -1; dx <= 1; dx++)
{
for (var dy = -1; dy <= 1; dy++)
{
var checkPos = new Vector2I(chunkPos.X + dx, chunkPos.Y + dy);
if (_loadedChunks.ContainsKey(checkPos))
{
return true;
}
}
}
return false;
}
private void PlaceVein(Vector2I startCell, string tileType, int maxSize, List<Vector2I> tileList)
{
GD.Print($"{LogPrefix} Starting to place vein of type {tileType} at {startCell} with max size {maxSize}");
var placed = new HashSet<Vector2I>();
var queue = new Queue<Vector2I>();
queue.Enqueue(startCell);
int placedCount = 0;
while (queue.Count > 0 && placedCount < maxSize)
{
var cell = queue.Dequeue();
// Skip if already placed or out of bounds
if (placed.Contains(cell))
{
GD.Print($"{LogPrefix} Skipping cell {cell} - already placed");
continue;
}
if (!IsInLoadedChunk(cell))
{
GD.Print($"{LogPrefix} Skipping cell {cell} - out of bounds");
continue;
}
// Try to place the tile
if (PlaceTile(tileType, cell))
{
tileList.Add(cell);
placed.Add(cell);
placedCount++;
GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell} ({placedCount}/{maxSize})");
// Add adjacent cells to queue
var directions = new[] { Vector2I.Up, Vector2I.Right, Vector2I.Down, Vector2I.Left };
foreach (var dir in directions)
{
var newCell = cell + dir;
if (!placed.Contains(newCell))
{
queue.Enqueue(newCell);
}
}
}
else
{
GD.Print($"{LogPrefix} Failed to place {tileType} at {cell}");
}
}
GD.Print($"{LogPrefix} Finished placing vein - placed {placedCount}/{maxSize} {tileType} tiles");
}
private bool PlaceTile(string tileType, Vector2I cell)
{
try
{
// First, remove any existing tile at this position
Grid.FreeArea(cell, Vector2I.One, 0f, GridLayer.Ground);
var building = Registry.GetBuilding(tileType);
if (building == null)
{
GD.PrintErr($"{LogPrefix} Building type not found in registry: {tileType}");
return false;
}
var scene = building.Scene;
if (scene == null)
{
GD.PrintErr($"{LogPrefix} Scene is null for building type: {tileType}");
return false;
}
var instance = scene.Instantiate() as Node2D;
if (instance == null)
{
GD.PrintErr($"{LogPrefix} Failed to instantiate scene for: {tileType}");
return false;
}
instance.GlobalPosition = GridUtils.GridToWorld(cell);
instance.ZIndex = (int)building.Layer;
AddChild(instance);
Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer);
GD.Print($"{LogPrefix} Successfully placed {tileType} at {cell}");
return true;
}
catch (Exception e)
{
GD.PrintErr($"{LogPrefix} Error placing {tileType} at {cell}: {e.Message}");
return false;
}
}
} }
public class ChunkData
{
public List<Vector2I> StoneTiles { get; } = [];
public List<Vector2I> IronTiles { get; } = [];
}