using Unity.Netcode; using UnityEngine; namespace Northbound { /// /// Controls object visibility based on fog of war state /// Attach to buildings, obstacles, enemies, or any object that should be hidden in fog /// public class FogOfWarVisibility : MonoBehaviour { [Header("Visibility Settings")] [Tooltip("Always show this object regardless of fog state (for core, important structures)")] public bool alwaysVisible = false; [Tooltip("Show this object in explored areas (greyed out) or only when visible")] public bool showInExploredAreas = false; [Tooltip("Update frequency for checking fog state (seconds)")] public float updateInterval = 0.2f; [Tooltip("Renderers to show/hide (auto-detected if empty)")] public Renderer[] renderers; [Header("Extended Visibility")] [Tooltip("Enable visibility from farther away based on object height")] public bool enableDistantVisibility = true; [Tooltip("Base visibility range (additional range beyond player vision)")] public float baseVisibilityRange = 10f; [Tooltip("Visibility range multiplier per unit of height (default: 2x vision per 1m height)")] public float heightVisibilityMultiplier = 2.0f; [Tooltip("Minimum height to get extended visibility (meters)")] public float minHeightForDistantVisibility = 3.0f; [Header("Explored State Visual (Optional)")] [Tooltip("Apply grey/desaturated material when in explored state")] public bool useExploredMaterial = false; [Tooltip("Material to use in explored state (optional)")] public Material exploredMaterial; private Material[] _originalMaterials; private bool _isVisible = false; private float _updateTimer; private ulong _localPlayerId; private bool _isInitialized = false; private float _objectHeight = 0f; private void Start() { // Auto-detect renderers if not set if (renderers == null || renderers.Length == 0) { renderers = GetComponentsInChildren(); } if (renderers == null || renderers.Length == 0) { // Debug.LogWarning($"[FogOfWarVisibility] {gameObject.name}: No renderers found! Component will not work."); return; } // Store original materials for explored state if (useExploredMaterial && renderers != null && renderers.Length > 0) { _originalMaterials = new Material[renderers.Length]; for (int i = 0; i < renderers.Length; i++) { if (renderers[i] != null) { _originalMaterials[i] = renderers[i].sharedMaterial; } } } // Calculate object height for distant visibility _objectHeight = CalculateObjectHeight(); // CRITICAL: Start hidden and stay hidden until fog system confirms visibility // Force initial hide - don't use SetVisible because _isVisible defaults to false // which would cause early return // 단, alwaysVisible이면 항상 보임 if (alwaysVisible) { _isVisible = false; SetVisible(true); } else { _isVisible = true; // Set to true first so SetVisible(false) actually runs SetVisible(false); } } /// /// Calculate the height of this object for distant visibility /// private float CalculateObjectHeight() { // Try to get height from Building component var building = GetComponent(); if (building != null && building.buildingData != null) { return building.buildingData.height; } // Fallback: Use renderer bounds if (renderers != null && renderers.Length > 0) { float maxHeight = 0f; foreach (var renderer in renderers) { if (renderer != null) { maxHeight = Mathf.Max(maxHeight, renderer.bounds.size.y); } } return maxHeight; } // Last resort: Use collider bounds var collider = GetComponent(); if (collider != null) { return collider.bounds.size.y; } return 0f; } private void Update() { // Only run on clients, not on dedicated server if (NetworkManager.Singleton != null && NetworkManager.Singleton.IsServer && !NetworkManager.Singleton.IsClient) { // Dedicated server - don't process visibility return; } // Initialize when network is ready if (!_isInitialized) { if (NetworkManager.Singleton != null && NetworkManager.Singleton.IsClient) { _localPlayerId = GetLocalPlayerId(); if (_localPlayerId == ulong.MaxValue) { // Local player not found yet - stay hidden SetVisible(false); return; } _isInitialized = true; // Force immediate visibility update on initialization UpdateVisibility(); } else { // Network not ready - stay hidden SetVisible(false); return; } } _updateTimer += Time.deltaTime; if (_updateTimer >= updateInterval) { _updateTimer = 0f; UpdateVisibility(); } } /// /// 로컬 플레이어의 실제 ID 가져오기 /// private ulong GetLocalPlayerId() { if (NetworkManager.Singleton == null) return ulong.MaxValue; // 방법 1: SpawnManager에서 찾기 NetworkObject localPlayer = null; if (NetworkManager.Singleton.SpawnManager != null) { localPlayer = NetworkManager.Singleton.SpawnManager.GetLocalPlayerObject(); } // 방법 2: LocalClient에서 찾기 if (localPlayer == null && NetworkManager.Singleton.LocalClient != null) { localPlayer = NetworkManager.Singleton.LocalClient.PlayerObject; } // 방법 3: 직접 검색 (IsLocalPlayer인 플레이어 찾기) if (localPlayer == null) { var allPlayers = FindObjectsByType(FindObjectsSortMode.None); foreach (var player in allPlayers) { if (player.IsLocalPlayer) { localPlayer = player.GetComponent(); break; } } } if (localPlayer == null) return ulong.MaxValue; var playerController = localPlayer.GetComponent(); if (playerController == null) return ulong.MaxValue; return playerController.OwnerPlayerId; } private void UpdateVisibility() { // 항상 보이는 객체는 fog 체크 안함 if (alwaysVisible) { SetVisible(true); return; } var fogSystem = FogOfWarSystem.Instance; if (fogSystem == null) { // No fog system - stay hidden for safety SetVisible(false); return; } // Wait for fog data to be initialized var fogData = fogSystem.GetPlayerFogData(_localPlayerId); if (fogData == null) { // Fog data not ready yet - stay hidden SetVisible(false); return; } FogOfWarState fogState = fogSystem.GetVisibilityState(_localPlayerId, transform.position); // Check for distant visibility based on height bool isDistantVisible = false; if (enableDistantVisibility && _objectHeight >= minHeightForDistantVisibility) { isDistantVisible = CheckDistantVisibility(fogSystem); } switch (fogState) { case FogOfWarState.Visible: // Currently visible - show with original materials SetVisible(true); SetExploredVisual(false); break; case FogOfWarState.Explored: // Previously seen but not currently visible // BUT: Tall objects can be seen from farther away if (showInExploredAreas || isDistantVisible) { SetVisible(true); SetExploredVisual(true); } else { SetVisible(false); } break; case FogOfWarState.Unexplored: // Never seen - hide unless tall enough to see from distance if (isDistantVisible) { SetVisible(true); SetExploredVisual(true); // Show as explored/distant } else { SetVisible(false); } break; default: // Unknown state - hide completely SetVisible(false); break; } } /// /// Check if this object should be visible from a distance based on its height /// private bool CheckDistantVisibility(FogOfWarSystem fogSystem) { // Find the closest vision provider (player) if (NetworkManager.Singleton == null || !NetworkManager.Singleton.IsClient) return false; // Get local player object var localPlayer = NetworkManager.Singleton.LocalClient?.PlayerObject; if (localPlayer == null) return false; // Calculate distance to player float distanceToPlayer = Vector3.Distance(transform.position, localPlayer.transform.position); // Calculate extended visibility range based on height // Taller objects can be seen from farther away // Formula: Base range + (height - minHeight) * multiplier float extendedRange = 0f; if (_objectHeight >= minHeightForDistantVisibility) { extendedRange = (_objectHeight - minHeightForDistantVisibility) * heightVisibilityMultiplier; } // Total range = player vision + base visibility + height bonus float totalRange = baseVisibilityRange + extendedRange; return distanceToPlayer <= totalRange; } private void SetVisible(bool visible) { if (_isVisible == visible) return; _isVisible = visible; if (renderers == null || renderers.Length == 0) return; foreach (var renderer in renderers) { if (renderer != null) { renderer.enabled = visible; } } } private void SetExploredVisual(bool explored) { if (!useExploredMaterial || renderers == null || exploredMaterial == null) return; for (int i = 0; i < renderers.Length; i++) { if (renderers[i] != null) { if (explored) { renderers[i].material = exploredMaterial; } else if (_originalMaterials != null && i < _originalMaterials.Length) { renderers[i].material = _originalMaterials[i]; } } } } private void OnDrawGizmosSelected() { // Visualize fog state check position Gizmos.color = _isVisible ? Color.green : Color.red; Gizmos.DrawWireSphere(transform.position, 0.5f); } } }