using System.Collections.Generic; using Unity.Netcode; using UnityEngine; /// /// Generates underground terrain using chunk-based system. /// Replaces individual MineableBlock NetworkObjects with MineableChunk NetworkObjects. /// public class ChunkedUndergroundGenerator : NetworkBehaviour { public static ChunkedUndergroundGenerator Instance { get; private set; } [Header("Generation Range (in blocks)")] [SerializeField] private Vector3Int generationRange = new Vector3Int(20, 30, 10); [SerializeField] private float noiseScale = 0.12f; [Header("Thresholds (0 to 1)")] [SerializeField, Range(0, 1)] private float hollowThreshold = 0.35f; [SerializeField, Range(0, 1)] private float baseResourceThreshold = 0.85f; [Header("Resource Distribution")] [SerializeField] private float resourceNoiseScale = 0.25f; // Larger = more spread out [SerializeField, Range(0, 1)] private float resourceSpawnChance = 0.3f; // Additional random chance [Header("Depth Settings")] [SerializeField] private bool increaseResourceWithDepth = true; [SerializeField] private float depthFactor = 0.003f; [Header("Block Health")] [SerializeField] private byte normalBlockHealth = 100; [SerializeField] private byte resourceBlockHealth = 150; [Header("Chunk Prefab")] [SerializeField] private GameObject chunkPrefab; [Header("Organization")] [SerializeField] private string containerName = "UndergroundChunks"; private Transform _chunkContainer; // Chunk registry for neighbor queries private Dictionary _chunkRegistry = new Dictionary(); // Noise seeds private float _seedX, _seedY, _seedZ; private float _resourceSeedX, _resourceSeedY, _resourceSeedZ; private void Awake() { if (Instance == null) { Instance = this; } else { Destroy(gameObject); return; } _seedX = Random.Range(0f, 99999f); _seedY = Random.Range(0f, 99999f); _seedZ = Random.Range(0f, 99999f); // Separate seeds for resource distribution _resourceSeedX = Random.Range(0f, 99999f); _resourceSeedY = Random.Range(0f, 99999f); _resourceSeedZ = Random.Range(0f, 99999f); // Create container for hierarchy organization _chunkContainer = new GameObject(containerName).transform; } public override void OnNetworkSpawn() { // Register with MineableChunk for neighbor queries MineableChunk.SetChunkManager(this); if (IsServer) { GenerateChunks(); } } /// /// Generate all chunks for the underground area /// private void GenerateChunks() { Vector3Int originGrid = BuildManager.Instance.WorldToGrid3D(transform.position); // Calculate chunk range int chunkSizeX = Mathf.CeilToInt((float)generationRange.x / ChunkCoord.CHUNK_SIZE); int chunkSizeY = Mathf.CeilToInt((float)generationRange.y / ChunkCoord.CHUNK_SIZE); int chunkSizeZ = Mathf.CeilToInt((float)generationRange.z / ChunkCoord.CHUNK_SIZE); Debug.Log($"[ChunkedGenerator] Generating {chunkSizeX}x{chunkSizeY}x{chunkSizeZ} chunks = {chunkSizeX * chunkSizeY * chunkSizeZ} total"); int chunksGenerated = 0; int chunksSkipped = 0; // Generate chunks for (int cx = 0; cx < chunkSizeX; cx++) { for (int cy = 0; cy < chunkSizeY; cy++) { for (int cz = 0; cz < chunkSizeZ; cz++) { // Calculate chunk grid origin (going downward for Y) Vector3Int chunkGridOrigin = originGrid + new Vector3Int( cx * ChunkCoord.CHUNK_SIZE, -cy * ChunkCoord.CHUNK_SIZE, // Negative Y (underground) cz * ChunkCoord.CHUNK_SIZE ); // Generate block data for this chunk ChunkState state = GenerateChunkState(chunkGridOrigin); // Check if chunk has any blocks bool hasBlocks = false; for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) { if (!state.blocks[i].IsEmpty) { hasBlocks = true; break; } } if (!hasBlocks) { chunksSkipped++; continue; } // Spawn chunk SpawnChunk(chunkGridOrigin, state); chunksGenerated++; } } } Debug.Log($"[ChunkedGenerator] Generated {chunksGenerated} chunks, skipped {chunksSkipped} empty chunks"); } /// /// Generate block state for a chunk /// private ChunkState GenerateChunkState(Vector3Int chunkGridOrigin) { ChunkState state = ChunkState.CreateEmpty(); for (int lx = 0; lx < ChunkCoord.CHUNK_SIZE; lx++) { for (int ly = 0; ly < ChunkCoord.CHUNK_SIZE; ly++) { for (int lz = 0; lz < ChunkCoord.CHUNK_SIZE; lz++) { Vector3Int gridPos = chunkGridOrigin + new Vector3Int(lx, ly, lz); int index = ChunkCoord.LocalToIndex(lx, ly, lz); // Check if within generation range Vector3Int originGrid = BuildManager.Instance.WorldToGrid3D(transform.position); Vector3Int offset = gridPos - originGrid; if (offset.x < 0 || offset.x >= generationRange.x || offset.y > 0 || offset.y <= -generationRange.y || offset.z < 0 || offset.z >= generationRange.z) { state.blocks[index] = BlockData.Empty; continue; } // Sample noise float noise = Get3DNoise(gridPos.x, gridPos.y, gridPos.z); // Check hollow threshold if (noise < hollowThreshold) { state.blocks[index] = BlockData.Empty; continue; } // Determine block type using separate resource noise for better distribution float resourceNoise = GetResourceNoise(gridPos.x, gridPos.y, gridPos.z); float currentThreshold = baseResourceThreshold; if (increaseResourceWithDepth) { currentThreshold += gridPos.y * depthFactor; // Lower threshold = more resources at depth } // Resource spawns only if: resource noise > threshold AND random chance passes bool isResource = resourceNoise > currentThreshold && Random.value < resourceSpawnChance; if (isResource) { state.blocks[index] = BlockData.Resource(resourceBlockHealth); } else { state.blocks[index] = BlockData.Normal(normalBlockHealth); } } } } return state; } /// /// Spawn a chunk NetworkObject /// private void SpawnChunk(Vector3Int chunkGridOrigin, ChunkState state) { if (chunkPrefab == null) { Debug.LogError("[ChunkedGenerator] Chunk prefab not assigned!"); return; } // Calculate world position for chunk Vector3 worldPos = BuildManager.Instance.GridToWorld(chunkGridOrigin); // Instantiate chunk GameObject chunkObj = Instantiate(chunkPrefab, worldPos, Quaternion.identity, _chunkContainer); // Spawn on network NetworkObject netObj = chunkObj.GetComponent(); netObj.Spawn(); // Initialize chunk with block data MineableChunk chunk = chunkObj.GetComponent(); if (chunk != null) { chunk.InitializeBlocks(state); // Register in dictionary ChunkCoord coord = ChunkCoord.FromGridPos(chunkGridOrigin); _chunkRegistry[coord.chunkPos] = chunk; } } /// /// 3D Perlin noise sampling for terrain shape /// private float Get3DNoise(int x, int y, int z) { float xCoord = (x + _seedX + 10000f) * noiseScale; float yCoord = (y + _seedY + 10000f) * noiseScale; float zCoord = (z + _seedZ + 10000f) * noiseScale; float ab = Mathf.PerlinNoise(xCoord, yCoord); float bc = Mathf.PerlinNoise(yCoord, zCoord); float ac = Mathf.PerlinNoise(xCoord, zCoord); return (ab + bc + ac) / 3f; } /// /// Separate 3D noise for resource distribution (more spread out) /// private float GetResourceNoise(int x, int y, int z) { float xCoord = (x + _resourceSeedX + 10000f) * resourceNoiseScale; float yCoord = (y + _resourceSeedY + 10000f) * resourceNoiseScale; float zCoord = (z + _resourceSeedZ + 10000f) * resourceNoiseScale; float ab = Mathf.PerlinNoise(xCoord, yCoord); float bc = Mathf.PerlinNoise(yCoord, zCoord); float ac = Mathf.PerlinNoise(xCoord, zCoord); return (ab + bc + ac) / 3f; } #region Public Query Methods /// /// Get chunk at grid position /// public MineableChunk GetChunkAtGrid(Vector3Int gridPos) { ChunkCoord coord = ChunkCoord.FromGridPos(gridPos); _chunkRegistry.TryGetValue(coord.chunkPos, out MineableChunk chunk); return chunk; } /// /// Get chunk at world position /// public MineableChunk GetChunkAtWorld(Vector3 worldPos) { ChunkCoord coord = ChunkCoord.FromWorldPos(worldPos); _chunkRegistry.TryGetValue(coord.chunkPos, out MineableChunk chunk); return chunk; } /// /// Check if block at grid position is solid (for cross-chunk face culling) /// public bool IsBlockSolid(Vector3Int gridPos) { MineableChunk chunk = GetChunkAtGrid(gridPos); if (chunk == null) return false; Vector3Int localPos = ChunkCoord.GridToLocal(gridPos); int index = ChunkCoord.LocalToIndex(localPos); BlockData block = chunk.GetBlock(index); return !block.IsEmpty && block.IsDiscovered; } /// /// Reveal blocks in radius around a world position (for fog of war) /// public void RevealBlocksInRadius(Vector3 worldPos, float radius) { if (!IsServer) return; // Find all chunks that could be affected foreach (var chunk in _chunkRegistry.Values) { if (chunk == null) continue; // Quick bounds check - chunk center distance float chunkDist = Vector3.Distance(worldPos, chunk.transform.position); float maxChunkRadius = ChunkCoord.CHUNK_SIZE * 0.866f; // diagonal if (chunkDist <= radius + maxChunkRadius) { chunk.RevealBlocksInRadius(worldPos, radius); } } } /// /// Get all active chunks /// public IEnumerable GetAllChunks() { return _chunkRegistry.Values; } #endregion #region Gizmos private void OnDrawGizmosSelected() { BuildManager bm = BuildManager.Instance; if (bm == null) bm = FindFirstObjectByType(); if (bm == null) return; Vector3Int originGrid = bm.WorldToGrid3D(transform.position); // Draw chunk boundaries int chunkSizeX = Mathf.CeilToInt((float)generationRange.x / ChunkCoord.CHUNK_SIZE); int chunkSizeY = Mathf.CeilToInt((float)generationRange.y / ChunkCoord.CHUNK_SIZE); int chunkSizeZ = Mathf.CeilToInt((float)generationRange.z / ChunkCoord.CHUNK_SIZE); Gizmos.color = new Color(0, 1, 0, 0.3f); for (int cx = 0; cx < chunkSizeX; cx++) { for (int cy = 0; cy < chunkSizeY; cy++) { for (int cz = 0; cz < chunkSizeZ; cz++) { Vector3Int chunkGridOrigin = originGrid + new Vector3Int( cx * ChunkCoord.CHUNK_SIZE, -cy * ChunkCoord.CHUNK_SIZE, cz * ChunkCoord.CHUNK_SIZE ); Vector3 worldPos = bm.GridToWorld(chunkGridOrigin); // Offset to center of chunk worldPos += new Vector3( (ChunkCoord.CHUNK_SIZE - 1) * 0.5f, (ChunkCoord.CHUNK_SIZE - 1) * 0.5f, (ChunkCoord.CHUNK_SIZE - 1) * 0.5f ); Gizmos.DrawWireCube(worldPos, Vector3.one * ChunkCoord.CHUNK_SIZE); } } } } #endregion }