374 lines
13 KiB
C#
374 lines
13 KiB
C#
using Unity.Netcode;
|
|
using UnityEngine;
|
|
|
|
namespace Northbound
|
|
{
|
|
/// <summary>
|
|
/// Controls object visibility based on fog of war state
|
|
/// Attach to buildings, obstacles, enemies, or any object that should be hidden in fog
|
|
/// </summary>
|
|
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<Renderer>();
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Calculate the height of this object for distant visibility
|
|
/// </summary>
|
|
private float CalculateObjectHeight()
|
|
{
|
|
// Try to get height from Building component
|
|
var building = GetComponent<Building>();
|
|
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<Collider>();
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 로컬 플레이어의 실제 ID 가져오기
|
|
/// </summary>
|
|
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<NetworkPlayerController>(FindObjectsSortMode.None);
|
|
foreach (var player in allPlayers)
|
|
{
|
|
if (player.IsLocalPlayer)
|
|
{
|
|
localPlayer = player.GetComponent<NetworkObject>();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (localPlayer == null) return ulong.MaxValue;
|
|
|
|
var playerController = localPlayer.GetComponent<NetworkPlayerController>();
|
|
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 || !fogSystem.gameObject.activeInHierarchy)
|
|
{
|
|
// No fog system or disabled - always visible
|
|
SetVisible(true);
|
|
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;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Check if this object should be visible from a distance based on its height
|
|
/// </summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|