390 lines
13 KiB
C#
390 lines
13 KiB
C#
using System.Collections.Generic;
|
|
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Generates underground terrain using chunk-based system.
|
|
/// Replaces individual MineableBlock NetworkObjects with MineableChunk NetworkObjects.
|
|
/// </summary>
|
|
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<Vector3Int, MineableChunk> _chunkRegistry = new Dictionary<Vector3Int, MineableChunk>();
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate all chunks for the underground area
|
|
/// </summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Generate block state for a chunk
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Spawn a chunk NetworkObject
|
|
/// </summary>
|
|
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<NetworkObject>();
|
|
netObj.Spawn();
|
|
|
|
// Initialize chunk with block data
|
|
MineableChunk chunk = chunkObj.GetComponent<MineableChunk>();
|
|
if (chunk != null)
|
|
{
|
|
chunk.InitializeBlocks(state);
|
|
|
|
// Register in dictionary
|
|
ChunkCoord coord = ChunkCoord.FromGridPos(chunkGridOrigin);
|
|
_chunkRegistry[coord.chunkPos] = chunk;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 3D Perlin noise sampling for terrain shape
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Separate 3D noise for resource distribution (more spread out)
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Get chunk at grid position
|
|
/// </summary>
|
|
public MineableChunk GetChunkAtGrid(Vector3Int gridPos)
|
|
{
|
|
ChunkCoord coord = ChunkCoord.FromGridPos(gridPos);
|
|
_chunkRegistry.TryGetValue(coord.chunkPos, out MineableChunk chunk);
|
|
return chunk;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get chunk at world position
|
|
/// </summary>
|
|
public MineableChunk GetChunkAtWorld(Vector3 worldPos)
|
|
{
|
|
ChunkCoord coord = ChunkCoord.FromWorldPos(worldPos);
|
|
_chunkRegistry.TryGetValue(coord.chunkPos, out MineableChunk chunk);
|
|
return chunk;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if block at grid position is solid (for cross-chunk face culling)
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reveal blocks in radius around a world position (for fog of war)
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get all active chunks
|
|
/// </summary>
|
|
public IEnumerable<MineableChunk> GetAllChunks()
|
|
{
|
|
return _chunkRegistry.Values;
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region Gizmos
|
|
|
|
private void OnDrawGizmosSelected()
|
|
{
|
|
BuildManager bm = BuildManager.Instance;
|
|
if (bm == null) bm = FindFirstObjectByType<BuildManager>();
|
|
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
|
|
}
|