From 862f11d4457af71b1912cd80d8f9c483d5c7a69f Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Fri, 29 Aug 2025 13:35:03 +0800 Subject: [PATCH] :sparkles: Infinite world gen --- Scenes/Entities/Player.tscn | 1 + Scripts/Entities/Player.cs | 1 + Scripts/System/NaturalResourceGenerator.cs | 414 ++++++++++++++------- 3 files changed, 286 insertions(+), 130 deletions(-) diff --git a/Scenes/Entities/Player.tscn b/Scenes/Entities/Player.tscn index a0f71f8..342e57f 100644 --- a/Scenes/Entities/Player.tscn +++ b/Scenes/Entities/Player.tscn @@ -5,6 +5,7 @@ [node name="Player" type="CharacterBody2D"] script = ExtResource("1_08t41") +MinZoom = 0.1 MaxZoom = 5.0 [node name="Sprite2D" type="Sprite2D" parent="."] diff --git a/Scripts/Entities/Player.cs b/Scripts/Entities/Player.cs index 4eb532e..03bf51c 100644 --- a/Scripts/Entities/Player.cs +++ b/Scripts/Entities/Player.cs @@ -34,6 +34,7 @@ public partial class Player : CharacterBody2D _cameraTargetZoom = _camera.Zoom; AddToGroup(ItemPickup.PickupGroupName); + AddToGroup(NaturalResourceGenerator.ChunkTrackerGroupName); } public override void _Input(InputEvent @event) diff --git a/Scripts/System/NaturalResourceGenerator.cs b/Scripts/System/NaturalResourceGenerator.cs index f9260eb..d32e12d 100644 --- a/Scripts/System/NaturalResourceGenerator.cs +++ b/Scripts/System/NaturalResourceGenerator.cs @@ -1,153 +1,307 @@ +using System; using Godot; using System.Collections.Generic; +using AceFieldNewHorizon.Scripts.Entities; using AceFieldNewHorizon.Scripts.Tiles; namespace AceFieldNewHorizon.Scripts.System; public partial class NaturalResourceGenerator : Node2D { - [Export] public GridManager Grid { get; set; } - [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; + public const string ChunkTrackerGroupName = "NrgTrackingTarget"; - private RandomNumberGenerator _rng; - private readonly List _groundTiles = []; - private readonly List _stoneTiles = []; - private readonly List _ironTiles = []; + [Export] public GridManager Grid { get; set; } + [Export] public BuildingRegistry Registry { get; set; } - public override void _Ready() - { - _rng = new RandomNumberGenerator(); - _rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi()); - - GenerateTerrain(); - PlaceResources(); - } + [Export] public int ChunkSize = 16; + [Export] public int LoadDistance = 2; // Number of chunks to load in each direction + [Export] public float StoneDensity = 0.1f; + [Export] public float IronDensity = 0.03f; + [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 void GenerateTerrain() - { - // 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 const string LogPrefix = "[NaturalGeneration]"; - private void PlaceResources() - { - // Create a copy of ground tiles for iteration - var groundTilesToProcess = new List(_groundTiles); - - // Place stone veins - foreach (var cell in groundTilesToProcess) - { - if (_rng.Randf() < StoneDensity) - { - var veinSize = _rng.RandiRange(MinStoneVeinSize, MaxStoneVeinSize); - PlaceVein(cell, "stone", veinSize, _stoneTiles); - } - } + private RandomNumberGenerator _rng; + private readonly Dictionary _loadedChunks = new(); + private Vector2I _lastPlayerChunk = new(-1000, -1000); - // Create a copy of stone tiles for iteration - var stoneTilesToProcess = new List(_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 Player _player; - private void PlaceVein(Vector2I startCell, string tileType, int maxVeinSize, ICollection tileList) - { - var queue = new Queue(); - var placed = new HashSet(); - queue.Enqueue(startCell); + public override void _Ready() + { + _rng = new RandomNumberGenerator(); + _rng.Seed = (ulong)(Seed != 0 ? Seed : (int)GD.Randi()); + } - int placedCount = 0; - - while (queue.Count > 0 && placedCount < maxVeinSize) - { - var cell = queue.Dequeue(); - if (placed.Contains(cell)) continue; - if (!IsInBounds(cell)) continue; + public override void _Process(double delta) + { + _player = GetTree().GetFirstNodeInGroup(ChunkTrackerGroupName) as Player; + if (_player != null) + { + UpdatePlayerPosition(_player.GlobalPosition); + } + else + { + GD.PrintErr($"{LogPrefix} Player not found in group: {ChunkTrackerGroupName}"); + } + } - switch (tileType) - { - // For iron, make sure we're placing on stone - case "ore_iron" when !_stoneTiles.Contains(cell): - continue; - // 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; - } + public void UpdatePlayerPosition(Vector2 playerPosition) + { + var playerChunk = WorldToChunkCoords(playerPosition); + if (playerChunk == _lastPlayerChunk) return; + _lastPlayerChunk = playerChunk; - PlaceTile(tileType, cell); - tileList.Add(cell); - placed.Add(cell); - placedCount++; + // Unload chunks outside load distance + var chunksToRemove = new List(); + foreach (var chunkPos in _loadedChunks.Keys) + { + if (Mathf.Abs(chunkPos.X - playerChunk.X) > LoadDistance || + Mathf.Abs(chunkPos.Y - playerChunk.Y) > LoadDistance) + { + chunksToRemove.Add(chunkPos); + } + } - // Add adjacent cells to queue - for (var dx = -1; dx <= 1; dx++) - { - 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); - } - } - } - } - } + foreach (var chunkPos in chunksToRemove) + { + UnloadChunk(chunkPos); + } - private bool IsInBounds(Vector2I cell) - { - return cell.X >= 0 && cell.X < MapWidth && - cell.Y >= 0 && cell.Y < MapHeight; - } + // Load chunks around player + for (int x = -LoadDistance; x <= LoadDistance; x++) + { + 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) - { - var building = Registry.GetBuilding(tileType); - if (building == null) return; + private void GenerateChunk(Vector2I chunkPos) + { + GD.Print($"{LogPrefix} Generating chunk at {chunkPos}"); + var chunkData = new ChunkData(); + var chunkWorldPos = ChunkToWorldCoords(chunkPos); - var scene = building.Scene; - var instance = (BaseTile)scene.Instantiate(); - - // Match PlacementManager's positioning logic - var rotatedSize = building.GetRotatedSize(0f); - var offset = GridUtils.GetCenterOffset(rotatedSize, 0f); - instance.Position = GridUtils.GridToWorld(cell) + offset; - - instance.ZIndex = (int)building.Layer; - AddChild(instance); + // First, place ground tiles + for (int x = 0; x < ChunkSize; x++) + { + for (int y = 0; y < ChunkSize; y++) + { + var cell = new Vector2I(chunkWorldPos.X + x, chunkWorldPos.Y + y); + if (!PlaceTile("ground", cell)) + { + GD.PrintErr($"{LogPrefix} Failed to place ground at {cell}"); + } + } + } - // Make sure to use the building's size from the registry - Grid.OccupyArea(cell, instance, building.Size, 0f, building.Layer); - } + // Then generate stone veins + 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(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 tileList) + { + GD.Print($"{LogPrefix} Starting to place vein of type {tileType} at {startCell} with max size {maxSize}"); + + var placed = new HashSet(); + var queue = new Queue(); + 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 StoneTiles { get; } = []; + public List IronTiles { get; } = []; +} \ No newline at end of file