using System.Collections; using Unity.Netcode; using UnityEngine; /// /// NetworkBehaviour for a mineable chunk containing 4x4x4 blocks. /// Manages block state, mesh generation, damage, and item drops. /// [RequireComponent(typeof(MeshFilter))] [RequireComponent(typeof(MeshRenderer))] [RequireComponent(typeof(MeshCollider))] public class MineableChunk : NetworkBehaviour, ChunkMeshBuilder.INeighborProvider { [Header("Block Settings")] [SerializeField] private byte normalBlockHealth = 100; [SerializeField] private byte resourceBlockHealth = 150; [Header("Drop Settings")] [SerializeField] private ItemData normalDropItem; [SerializeField] private ItemData resourceDropItem; [SerializeField] private GameObject genericDropPrefab; [Header("Materials")] [SerializeField] private Material normalBlockMaterial; [SerializeField] private Material resourceBlockMaterial; [Header("Visual Settings")] [SerializeField] private float meshRebuildDelay = 0.1f; [Header("Highlight Settings")] [SerializeField] private Material highlightMaterial; private GameObject _highlightCube; private MeshRenderer _highlightRenderer; [Header("Fog of War")] [SerializeField] private float visibilityTimeout = 0.5f; // Time before block goes from "visible" to "discovered" [SerializeField] private Color discoveredTint = new Color(0.3f, 0.3f, 0.3f, 1f); // Darkened color for discovered-but-not-visible private float[] _blockVisibilityTime; // Last time each block was visible (local only) private MaterialPropertyBlock _propBlock; // Chunk state synced to all clients private NetworkVariable _networkState = new NetworkVariable( default, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server ); // Local cache for quick access private ChunkState _localState; private ChunkCoord _chunkCoord; private bool _isInitialized = false; // Components private MeshFilter _meshFilter; private MeshRenderer _meshRenderer; private MeshCollider _meshCollider; private Outline _outline; // Mesh rebuild state private bool _meshDirty = false; private Coroutine _rebuildCoroutine; // Highlight state private int _highlightedBlockIndex = -1; // Reference to chunk manager for neighbor queries private static ChunkedUndergroundGenerator _chunkManager; public ChunkCoord Coord => _chunkCoord; public ChunkState State => _localState; // Visibility check interval private float _lastVisibilityCheck; private const float VISIBILITY_CHECK_INTERVAL = 0.3f; private bool _hasVisibleBlocks = false; private void Awake() { _meshFilter = GetComponent(); _meshRenderer = GetComponent(); _meshCollider = GetComponent(); _outline = GetComponent(); // Initialize local state _localState = ChunkState.CreateEmpty(); // Initialize visibility tracking (local only, not networked) _blockVisibilityTime = new float[ChunkState.BLOCKS_PER_CHUNK]; _propBlock = new MaterialPropertyBlock(); // Set materials: 0=normal-visible, 1=resource-visible, 2=normal-dark, 3=resource-dark SetupMaterials(); // Create highlight cube for per-block highlighting CreateHighlightCube(); } private void SetupMaterials() { if (normalBlockMaterial != null && resourceBlockMaterial != null) { // Create darkened versions of materials for discovered-but-not-visible state Material normalDark = new Material(normalBlockMaterial); ApplyDarkTint(normalDark, normalBlockMaterial); Material resourceDark = new Material(resourceBlockMaterial); ApplyDarkTint(resourceDark, resourceBlockMaterial); _meshRenderer.materials = new Material[] { normalBlockMaterial, // Submesh 0: Normal blocks (visible) resourceBlockMaterial, // Submesh 1: Resource blocks (visible) normalDark, // Submesh 2: Normal blocks (discovered/dark) resourceDark // Submesh 3: Resource blocks (discovered/dark) }; } } private void ApplyDarkTint(Material darkMat, Material sourceMat) { // Try URP _BaseColor first, then fallback to _Color if (sourceMat.HasProperty("_BaseColor")) { Color baseColor = sourceMat.GetColor("_BaseColor"); darkMat.SetColor("_BaseColor", baseColor * discoveredTint); } else if (sourceMat.HasProperty("_Color")) { Color baseColor = sourceMat.GetColor("_Color"); darkMat.SetColor("_Color", baseColor * discoveredTint); } } private void CreateHighlightCube() { _highlightCube = GameObject.CreatePrimitive(PrimitiveType.Cube); _highlightCube.name = "BlockHighlight"; _highlightCube.transform.SetParent(transform); _highlightCube.transform.localScale = Vector3.one * 1.02f; // Slightly larger than block // Remove collider - this is visual only var collider = _highlightCube.GetComponent(); if (collider != null) Destroy(collider); // Setup renderer _highlightRenderer = _highlightCube.GetComponent(); if (highlightMaterial != null) { _highlightRenderer.material = highlightMaterial; } else { // Create default highlight material if none assigned var mat = new Material(Shader.Find("Standard")); mat.color = new Color(1f, 1f, 0f, 0.3f); // Yellow transparent mat.SetFloat("_Mode", 3); // Transparent mode mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.DisableKeyword("_ALPHATEST_ON"); mat.EnableKeyword("_ALPHABLEND_ON"); mat.DisableKeyword("_ALPHAPREMULTIPLY_ON"); mat.renderQueue = 3000; _highlightRenderer.material = mat; } _highlightCube.SetActive(false); } public override void OnNetworkSpawn() { // Calculate chunk coordinates from world position _chunkCoord = ChunkCoord.FromWorldPos(transform.position); // Subscribe to state changes _networkState.OnValueChanged += OnChunkStateChanged; // Initial sync SyncLocalState(); RebuildMeshImmediate(); // Find chunk manager if not set if (_chunkManager == null) { _chunkManager = FindFirstObjectByType(); } } private void Update() { // Only check visibility timeout periodically and if we had visible blocks if (!_hasVisibleBlocks) return; if (Time.time - _lastVisibilityCheck < VISIBILITY_CHECK_INTERVAL) return; _lastVisibilityCheck = Time.time; CheckVisibilityTimeout(); } private void CheckVisibilityTimeout() { bool anyStillVisible = false; bool needsRebuild = false; for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) { if (_blockVisibilityTime[i] <= 0) continue; bool wasVisible = (Time.time - _blockVisibilityTime[i]) < visibilityTimeout; if (wasVisible) { anyStillVisible = true; } else if (_blockVisibilityTime[i] > 0) { // Block just transitioned from visible to discovered // Check if it was visible last frame (within timeout + check interval) float timeSinceVisible = Time.time - _blockVisibilityTime[i]; if (timeSinceVisible < visibilityTimeout + VISIBILITY_CHECK_INTERVAL * 2) { needsRebuild = true; } } } _hasVisibleBlocks = anyStillVisible; if (needsRebuild) { ScheduleMeshRebuild(); } } public override void OnNetworkDespawn() { _networkState.OnValueChanged -= OnChunkStateChanged; } /// /// Initialize chunk with generated block data (called by generator on server) /// public void InitializeBlocks(ChunkState initialState) { if (!IsServer) return; _networkState.Value = initialState; SyncLocalState(); RebuildMeshImmediate(); } /// /// Get block data at local coordinates /// public BlockData GetBlock(int x, int y, int z) { if (!ChunkCoord.IsValidLocal(x, y, z)) return BlockData.Empty; if (_localState.blocks == null) return BlockData.Empty; int index = ChunkCoord.LocalToIndex(x, y, z); return _localState.blocks[index]; } /// /// Get block data at local index /// public BlockData GetBlock(int index) { if (index < 0 || index >= ChunkState.BLOCKS_PER_CHUNK) return BlockData.Empty; if (_localState.blocks == null) return BlockData.Empty; return _localState.blocks[index]; } #region Damage System /// /// Request damage to a specific block (called by clients) /// [Rpc(SendTo.Server)] public void DamageBlockServerRpc(int localIndex, byte damage) { if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return; var state = _networkState.Value; BlockData block = state.blocks[localIndex]; if (block.IsEmpty) return; // Apply damage int newHealth = Mathf.Max(0, block.health - damage); block.health = (byte)newHealth; // Update state state.blocks[localIndex] = block; _networkState.Value = state; // Sync to clients UpdateBlockClientRpc(localIndex, block); // Play hit effect PlayHitEffectClientRpc(localIndex); // Check for destruction if (newHealth <= 0) { DestroyBlock(localIndex, state.blocks[localIndex].blockType); } } /// /// Server-side damage application (for direct server calls) /// public void DamageBlock(int localIndex, float damageAmount) { if (!IsServer) return; DamageBlockServerRpc(localIndex, (byte)Mathf.Min(255, damageAmount)); } /// /// Update a single block on all clients /// [ClientRpc] private void UpdateBlockClientRpc(int localIndex, BlockData block) { if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return; _localState.blocks[localIndex] = block; // Schedule mesh rebuild ScheduleMeshRebuild(); } /// /// Play hit visual effect /// [ClientRpc] private void PlayHitEffectClientRpc(int localIndex) { // Get block world position for effect Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex); Vector3 worldPos = _chunkCoord.LocalToWorld(localPos); // TODO: Spawn particle effect at worldPos // For now, just a debug visualization } /// /// Handle block destruction (server-side) /// private void DestroyBlock(int localIndex, byte blockType) { if (!IsServer) return; // Spawn dropped item Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex); Vector3 worldPos = _chunkCoord.LocalToWorld(localPos); SpawnDrop(worldPos, blockType); // Clear block var state = _networkState.Value; state.blocks[localIndex] = BlockData.Empty; _networkState.Value = state; // Full sync and rebuild SyncStateClientRpc(state); } /// /// Spawn item drop at position /// private void SpawnDrop(Vector3 position, byte blockType) { if (genericDropPrefab == null) return; ItemData dropItem = blockType == BlockData.TYPE_RESOURCE ? resourceDropItem : normalDropItem; if (dropItem == null) return; GameObject dropObj = Instantiate(genericDropPrefab, position + Vector3.up * 0.5f, Quaternion.identity); NetworkObject netObj = dropObj.GetComponent(); netObj.Spawn(); if (dropObj.TryGetComponent(out var droppedItem)) { droppedItem.Initialize(dropItem.itemID); } } #endregion #region Fog of War /// /// Check if block is currently visible (in player's sight right now) /// public bool IsBlockCurrentlyVisible(int localIndex) { if (_blockVisibilityTime == null || localIndex < 0 || localIndex >= _blockVisibilityTime.Length) return false; return (Time.time - _blockVisibilityTime[localIndex]) < visibilityTimeout; } /// /// Update local visibility for blocks in range (called by local player) /// public void UpdateLocalVisibility(Vector3 playerPos, float radius) { bool needsRebuild = false; bool anyVisible = false; for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) { BlockData block = _localState.blocks[i]; if (block.IsEmpty || !block.IsDiscovered) continue; Vector3 blockWorldPos = GetBlockWorldPosition(i); bool wasVisible = IsBlockCurrentlyVisible(i); if (Vector3.Distance(playerPos, blockWorldPos) <= radius) { _blockVisibilityTime[i] = Time.time; anyVisible = true; // If block just became visible, need to rebuild mesh if (!wasVisible) { needsRebuild = true; } } } // Track if we have any visible blocks (for timeout checking in Update) _hasVisibleBlocks = anyVisible || _hasVisibleBlocks; if (needsRebuild) { ScheduleMeshRebuild(); } } /// /// Request reveal of a specific block (called by clients) /// [Rpc(SendTo.Server)] public void RevealBlockServerRpc(int localIndex) { if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return; var state = _networkState.Value; BlockData block = state.blocks[localIndex]; if (block.IsEmpty || block.IsDiscovered) return; // Mark as discovered block.IsDiscovered = true; state.blocks[localIndex] = block; _networkState.Value = state; // Sync to clients UpdateBlockClientRpc(localIndex, block); } /// /// Reveal all blocks within radius of a world position /// public void RevealBlocksInRadius(Vector3 worldPos, float radius) { if (!IsServer) return; bool anyRevealed = false; var state = _networkState.Value; for (int i = 0; i < ChunkState.BLOCKS_PER_CHUNK; i++) { BlockData block = state.blocks[i]; if (block.IsEmpty || block.IsDiscovered) continue; Vector3Int localPos = ChunkCoord.IndexToLocal(i); Vector3 blockWorldPos = _chunkCoord.LocalToWorld(localPos); if (Vector3.Distance(worldPos, blockWorldPos) <= radius) { block.IsDiscovered = true; state.blocks[i] = block; anyRevealed = true; } } if (anyRevealed) { _networkState.Value = state; SyncStateClientRpc(state); } } /// /// Check if block at local index is discovered /// public bool IsBlockDiscovered(int localIndex) { if (localIndex < 0 || localIndex >= ChunkState.BLOCKS_PER_CHUNK) return false; return _localState.blocks[localIndex].IsDiscovered; } #endregion #region Mesh Building /// /// Full state sync from server /// [ClientRpc] private void SyncStateClientRpc(ChunkState state) { _localState = state; RebuildMeshImmediate(); } private void OnChunkStateChanged(ChunkState oldState, ChunkState newState) { SyncLocalState(); ScheduleMeshRebuild(); } private void SyncLocalState() { _localState = _networkState.Value; // Ensure blocks array is initialized if (_localState.blocks == null) { _localState = ChunkState.CreateEmpty(); } } private void ScheduleMeshRebuild() { if (_meshDirty) return; _meshDirty = true; if (_rebuildCoroutine != null) { StopCoroutine(_rebuildCoroutine); } _rebuildCoroutine = StartCoroutine(DelayedMeshRebuild()); } private IEnumerator DelayedMeshRebuild() { yield return new WaitForSeconds(meshRebuildDelay); RebuildMeshImmediate(); _meshDirty = false; _rebuildCoroutine = null; } private void RebuildMeshImmediate() { if (_meshFilter == null) return; // Build visual mesh with fog of war (4 submeshes: normal-visible, resource-visible, normal-dark, resource-dark) Mesh visualMesh = ChunkMeshBuilder.BuildMeshWithVisibility( _localState, _chunkCoord, this, true, // only discovered IsBlockCurrentlyVisible // visibility checker ); _meshFilter.mesh = visualMesh; // Build collision mesh (all blocks, including undiscovered, no visibility distinction) if (_meshCollider != null) { Mesh collisionMesh = ChunkMeshBuilder.BuildMesh(_localState, _chunkCoord, this, false); _meshCollider.sharedMesh = null; // Force refresh _meshCollider.sharedMesh = collisionMesh; } } #endregion #region INeighborProvider Implementation /// /// Check if block at world grid position is solid (for cross-chunk face culling) /// public bool IsBlockSolid(Vector3Int gridPos) { // Check if position is in this chunk ChunkCoord coord = ChunkCoord.FromGridPos(gridPos); if (coord == _chunkCoord) { Vector3Int local = ChunkCoord.GridToLocal(gridPos); int index = ChunkCoord.LocalToIndex(local); var block = _localState.blocks[index]; return !block.IsEmpty && block.IsDiscovered; } // Query chunk manager for other chunks if (_chunkManager != null) { return _chunkManager.IsBlockSolid(gridPos); } // Default: assume not solid at chunk boundaries return false; } #endregion #region Interaction Support /// /// Convert world hit point to local block index /// public int WorldPointToBlockIndex(Vector3 worldPoint) { // Convert world point to local space relative to chunk transform Vector3 localPoint = transform.InverseTransformPoint(worldPoint); // Each block is 1 unit, centered at integer coordinates // So block at (0,0,0) spans from -0.5 to 0.5 int lx = Mathf.Clamp(Mathf.RoundToInt(localPoint.x), 0, ChunkCoord.CHUNK_SIZE - 1); int ly = Mathf.Clamp(Mathf.RoundToInt(localPoint.y), 0, ChunkCoord.CHUNK_SIZE - 1); int lz = Mathf.Clamp(Mathf.RoundToInt(localPoint.z), 0, ChunkCoord.CHUNK_SIZE - 1); return ChunkCoord.LocalToIndex(lx, ly, lz); } /// /// Get world position of block at local index /// public Vector3 GetBlockWorldPosition(int localIndex) { Vector3Int localPos = ChunkCoord.IndexToLocal(localIndex); // Convert local position to world using chunk transform return transform.TransformPoint(new Vector3(localPos.x, localPos.y, localPos.z)); } /// /// Set highlight state for visual feedback /// public void SetHighlight(bool isOn, int blockIndex = -1) { _highlightedBlockIndex = isOn ? blockIndex : -1; // Use per-block highlight cube instead of outline if (_highlightCube != null) { if (isOn && blockIndex >= 0) { // Position highlight at specific block Vector3Int localPos = ChunkCoord.IndexToLocal(blockIndex); _highlightCube.transform.localPosition = new Vector3(localPos.x, localPos.y, localPos.z); _highlightCube.SetActive(true); } else { _highlightCube.SetActive(false); } } // Disable outline component if present (we use highlight cube instead) if (_outline != null) { _outline.enabled = false; } } /// /// Get the currently highlighted block index /// public int HighlightedBlockIndex => _highlightedBlockIndex; #endregion #region Static Registration public static void SetChunkManager(ChunkedUndergroundGenerator manager) { _chunkManager = manager; } #endregion }