지하 최적화

블록 프리팹 단위 -> 블록 청크 단위 스폰
기타 건설, 조준 관련 사이드이펙트 버그 수정
This commit is contained in:
2026-01-21 21:34:05 +09:00
parent db5db4b106
commit 59246a67bd
24 changed files with 2622 additions and 127 deletions

View File

@@ -0,0 +1,389 @@
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
}