전장의 안개 기능 개선

미탐험 구역의 모든 오브젝트는 보이지 않음
적이 시야를 제공하는 문제 수정
높은 장애물은 더 먼 거리에서부터 보일 수 있음
This commit is contained in:
2026-01-30 16:04:22 +09:00
parent 6df3e5d396
commit a9a744589d
22 changed files with 1298 additions and 55 deletions

View File

@@ -205,6 +205,14 @@ namespace Northbound
GameObject buildingObj = Instantiate(data.prefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
NetworkObject netObj = buildingObj.GetComponent<NetworkObject>();
// Add FogOfWarVisibility component to hide buildings in unexplored areas
if (buildingObj.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = buildingObj.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = true; // Buildings remain visible in explored areas
visibility.updateInterval = 0.2f;
}
if (netObj != null)
{
// 건물의 소유자를 설정
@@ -359,6 +367,14 @@ namespace Northbound
GameObject foundationObj = Instantiate(foundationPrefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
NetworkObject netObj = foundationObj.GetComponent<NetworkObject>();
// Add FogOfWarVisibility component to hide foundations in unexplored areas
if (foundationObj.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = foundationObj.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = true; // Foundations remain visible in explored areas
visibility.updateInterval = 0.2f;
}
if (netObj != null)
{
netObj.SpawnWithOwnership(requestingClientId);
@@ -425,6 +441,14 @@ namespace Northbound
GameObject buildingObj = Instantiate(data.prefab, worldPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
NetworkObject netObj = buildingObj.GetComponent<NetworkObject>();
// Add FogOfWarVisibility component to hide buildings in unexplored areas
if (buildingObj.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = buildingObj.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = true; // Buildings remain visible in explored areas
visibility.updateInterval = 0.2f;
}
if (netObj != null)
{
netObj.SpawnWithOwnership(ownerId);

View File

@@ -0,0 +1,170 @@
using UnityEngine;
using UnityEditor;
namespace Northbound.Editor
{
/// <summary>
/// Editor utility to add FogOfWarVisibility to all obstacles and buildings
/// </summary>
public static class FogOfWarVisibilitySetup
{
[MenuItem("Tools/Fog of War/Add Visibility to All Obstacles")]
public static void AddVisibilityToObstacles()
{
string[] obstaclePaths = new string[]
{
"Assets/Prefabs/Obstacles/Rock1.prefab",
"Assets/Prefabs/Obstacles/Rock2.prefab",
"Assets/Prefabs/Obstacles/Rock3.prefab",
"Assets/Prefabs/Obstacles/Tree1.prefab",
"Assets/Prefabs/Obstacles/Tree2.prefab"
};
int count = 0;
foreach (string path in obstaclePaths)
{
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab != null)
{
// Check if already has component
if (prefab.GetComponent<FogOfWarVisibility>() == null)
{
// Add component
var visibility = prefab.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = false; // Hide obstacles when not visible
visibility.updateInterval = 0.2f;
EditorUtility.SetDirty(prefab);
count++;
Debug.Log($"Added FogOfWarVisibility to {prefab.name}");
}
else
{
Debug.Log($"{prefab.name} already has FogOfWarVisibility");
}
}
else
{
Debug.LogWarning($"Could not find prefab at {path}");
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"<color=green>FogOfWarVisibility added to {count} obstacle prefabs!</color>");
}
[MenuItem("Tools/Fog of War/Add Visibility to All Buildings")]
public static void AddVisibilityToBuildings()
{
// Find all Building prefabs
string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Prefabs" });
int count = 0;
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab != null && prefab.GetComponent<Building>() != null)
{
// Check if already has component
if (prefab.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = prefab.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = true; // Show buildings in explored areas (greyed out)
visibility.updateInterval = 0.2f;
EditorUtility.SetDirty(prefab);
count++;
Debug.Log($"Added FogOfWarVisibility to building: {prefab.name}");
}
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"<color=green>FogOfWarVisibility added to {count} building prefabs!</color>");
}
[MenuItem("Tools/Fog of War/Add Visibility to Scene Obstacles")]
public static void AddVisibilityToSceneObstacles()
{
GameObject[] allObjects = GameObject.FindObjectsOfType<GameObject>();
int count = 0;
foreach (GameObject obj in allObjects)
{
// Skip if it's a prefab instance - those will be handled by prefab updates
if (PrefabUtility.IsPartOfPrefabInstance(obj))
continue;
// Check if it has renderers but no FogOfWarVisibility
if (obj.GetComponent<Renderer>() != null && obj.GetComponent<FogOfWarVisibility>() == null)
{
// Check if it's an obstacle (has MeshCollider and no other components)
if (obj.name.Contains("Rock") || obj.name.Contains("Tree") || obj.name.Contains("Obstacle"))
{
var visibility = obj.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = false;
visibility.updateInterval = 0.2f;
EditorUtility.SetDirty(obj);
count++;
Debug.Log($"Added FogOfWarVisibility to scene object: {obj.name}");
}
}
}
Debug.Log($"<color=green>FogOfWarVisibility added to {count} scene objects!</color>");
}
[MenuItem("Tools/Fog of War/Remove Visibility from All")]
public static void RemoveVisibilityFromAll()
{
if (!EditorUtility.DisplayDialog("Remove FogOfWarVisibility",
"This will remove FogOfWarVisibility from all prefabs and scene objects. Continue?",
"Yes", "Cancel"))
{
return;
}
// Remove from prefabs
string[] guids = AssetDatabase.FindAssets("t:Prefab", new[] { "Assets/Prefabs" });
int prefabCount = 0;
foreach (string guid in guids)
{
string path = AssetDatabase.GUIDToAssetPath(guid);
GameObject prefab = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (prefab != null)
{
var visibility = prefab.GetComponent<FogOfWarVisibility>();
if (visibility != null)
{
Object.DestroyImmediate(visibility, true);
EditorUtility.SetDirty(prefab);
prefabCount++;
}
}
}
// Remove from scene objects
FogOfWarVisibility[] sceneComponents = GameObject.FindObjectsOfType<FogOfWarVisibility>();
int sceneCount = sceneComponents.Length;
foreach (var comp in sceneComponents)
{
Object.DestroyImmediate(comp);
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
Debug.Log($"<color=yellow>Removed FogOfWarVisibility from {prefabCount} prefabs and {sceneCount} scene objects</color>");
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 5d15a430ea206c447ad60e74293d5dc1

View File

@@ -22,6 +22,15 @@ public class EnemyPortal : MonoBehaviour
foreach (GameObject obj in Enemies)
{
GameObject enemy = Instantiate(obj, transform);
// Add FogOfWarVisibility component to hide enemies in unexplored areas
if (enemy.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = enemy.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = false; // Enemies hidden when not visible
visibility.updateInterval = 0.2f;
}
enemy.GetComponent<NetworkObject>().Spawn();
Debug.Log(enemy);
}

View File

@@ -7,7 +7,7 @@ namespace Northbound
/// 적대 유닛 (적대세력 또는 몬스터)
/// </summary>
[RequireComponent(typeof(Collider))]
public class EnemyUnit : NetworkBehaviour, IDamageable, ITeamMember, IVisionProvider
public class EnemyUnit : NetworkBehaviour, IDamageable, ITeamMember
{
[Header("Team Settings")]
[Tooltip("이 유닛의 팀 (Hostile = 적대세력, Monster = 몬스터)")]
@@ -15,7 +15,6 @@ namespace Northbound
[Header("Combat")]
public int maxHealth = 100;
public float visionRange = 10f;
[Header("Visual")]
public GameObject damageEffectPrefab;
@@ -41,20 +40,12 @@ namespace Northbound
{
_currentHealth.Value = maxHealth;
_team.Value = enemyTeam;
// FogOfWar 시스템에 등록
FogOfWarSystem.Instance?.RegisterVisionProvider(this);
}
}
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
if (IsServer)
{
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
}
}
#region IDamageable Implementation
@@ -97,9 +88,6 @@ namespace Northbound
// 파괴 이펙트
ShowDestroyEffectClientRpc();
// FogOfWar 시스템에서 제거
FogOfWarSystem.Instance?.UnregisterVisionProvider(this);
// 네트워크 오브젝트 파괴
Invoke(nameof(DespawnUnit), 0.5f);
}
@@ -146,28 +134,8 @@ namespace Northbound
#endregion
#region IVisionProvider Implementation
public ulong GetOwnerId() => OwnerClientId;
public float GetVisionRange() => visionRange;
public Transform GetTransform() => transform;
public bool IsActive() => IsSpawned && _currentHealth.Value > 0;
#endregion
private void OnDrawGizmosSelected()
{
// 팀 색상으로 시야 범위 표시
Color teamColor = Application.isPlaying
? TeamManager.GetTeamColor(_team.Value)
: TeamManager.GetTeamColor(enemyTeam);
Gizmos.color = new Color(teamColor.r, teamColor.g, teamColor.b, 0.3f);
Gizmos.DrawWireSphere(transform.position, visionRange);
#if UNITY_EDITOR
if (Application.isPlaying)
{

View File

@@ -14,6 +14,14 @@ namespace Northbound
public float updateInterval = 0.1f;
public float fogHeight = 5f; // 안개 높이 (지형보다 약간 위)
[Header("Smoothing")]
[Tooltip("Enable smooth circular vision edges")]
public bool enableSmoothing = true;
[Tooltip("Smoothing strength (higher = smoother but more blurry)")]
[Range(1, 5)]
public int smoothingPasses = 2;
[Header("Colors")]
public Color unexploredColor = new Color(0, 0, 0, 1f); // 완전히 어두움
public Color exploredColor = new Color(0, 0, 0, 0.6f); // 반투명
@@ -51,11 +59,12 @@ namespace Northbound
return;
}
// 텍스처 생성
// 텍스처 생성 (Bilinear filtering for smooth edges)
_fogTexture = new Texture2D(fogSystem.gridWidth, fogSystem.gridHeight)
{
filterMode = FilterMode.Bilinear,
wrapMode = TextureWrapMode.Clamp
wrapMode = TextureWrapMode.Clamp,
anisoLevel = 0 // Disable anisotropic filtering for fog overlay
};
_colors = new Color[fogSystem.gridWidth * fogSystem.gridHeight];
@@ -133,10 +142,66 @@ namespace Northbound
}
}
// Apply smoothing if enabled
if (enableSmoothing)
{
SmoothFogTexture(fogSystem.gridWidth, fogSystem.gridHeight);
}
_fogTexture.SetPixels(_colors);
_fogTexture.Apply();
}
/// <summary>
/// Apply box blur smoothing to create smooth circular vision edges
/// </summary>
private void SmoothFogTexture(int width, int height)
{
Color[] smoothed = new Color[_colors.Length];
for (int pass = 0; pass < smoothingPasses; pass++)
{
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
int index = y * width + x;
// Box blur: average with neighbors
float r = 0, g = 0, b = 0, a = 0;
int samples = 0;
// Sample 3x3 kernel
for (int dy = -1; dy <= 1; dy++)
{
for (int dx = -1; dx <= 1; dx++)
{
int nx = x + dx;
int ny = y + dy;
if (nx >= 0 && nx < width && ny >= 0 && ny < height)
{
int nIndex = ny * width + nx;
Color c = _colors[nIndex];
r += c.r;
g += c.g;
b += c.b;
a += c.a;
samples++;
}
}
}
// Average
smoothed[index] = new Color(r / samples, g / samples, b / samples, a / samples);
}
}
// Copy smoothed back to colors for next pass
System.Array.Copy(smoothed, _colors, _colors.Length);
}
}
private void CreatePlaneMesh(FogOfWarSystem fogSystem)
{
MeshFilter meshFilter = GetComponent<MeshFilter>();

View File

@@ -4,6 +4,179 @@ using UnityEngine;
namespace Northbound
{
/// <summary>
/// Helper class for efficient line-of-sight calculations using sector-based raycasting
/// </summary>
public class LineOfSightCalculator
{
private struct SectorData
{
public float angle; // Angle in degrees (0-360)
public float blockedDistance; // Distance where vision is blocked (float.MaxValue if clear)
public bool hasObstacle; // True if this sector has an obstacle
public float obstacleHeight; // Height of blocking obstacle
public Vector3 obstaclePosition; // World position of obstacle hit point
}
private FogOfWarSystem _fogSystem;
private SectorData[] _sectors;
private int _sectorCount;
public LineOfSightCalculator(FogOfWarSystem fogSystem)
{
_fogSystem = fogSystem;
RecalculateSectors();
}
/// <summary>
/// Recalculate sector count based on angular step
/// </summary>
public void RecalculateSectors()
{
_sectorCount = Mathf.CeilToInt(360f / _fogSystem.raycastAngularStep);
_sectors = new SectorData[_sectorCount];
for (int i = 0; i < _sectorCount; i++)
{
_sectors[i].angle = i * _fogSystem.raycastAngularStep;
_sectors[i].blockedDistance = float.MaxValue;
_sectors[i].hasObstacle = false;
_sectors[i].obstacleHeight = 0f;
_sectors[i].obstaclePosition = Vector3.zero;
}
}
/// <summary>
/// Perform raycasting to determine blocked sectors with height awareness
/// </summary>
public void CalculateVisibleSectors(Vector3 viewerPosition, float visionRange)
{
// Reset sectors
for (int i = 0; i < _sectorCount; i++)
{
_sectors[i].blockedDistance = float.MaxValue;
_sectors[i].hasObstacle = false;
_sectors[i].obstacleHeight = 0f;
_sectors[i].obstaclePosition = Vector3.zero;
}
// Raycast origin at viewer eye height
Vector3 rayOrigin = viewerPosition + Vector3.up * _fogSystem.viewerEyeHeight;
for (int i = 0; i < _sectorCount; i++)
{
float angleRad = _sectors[i].angle * Mathf.Deg2Rad;
Vector3 direction = new Vector3(Mathf.Cos(angleRad), 0, Mathf.Sin(angleRad));
if (Physics.Raycast(rayOrigin, direction, out RaycastHit hit, visionRange, _fogSystem.visionBlockingLayers))
{
_sectors[i].blockedDistance = hit.distance;
_sectors[i].hasObstacle = true;
_sectors[i].obstaclePosition = hit.point;
// Determine obstacle height
if (_fogSystem.enableHeightBlocking)
{
_sectors[i].obstacleHeight = GetObstacleHeight(hit.collider);
}
}
}
}
/// <summary>
/// Get the height of an obstacle for vision blocking calculations
/// </summary>
private float GetObstacleHeight(Collider obstacle)
{
// Check if it's a building
Building building = obstacle.GetComponent<Building>();
if (building != null && building.buildingData != null)
{
return building.buildingData.height;
}
// For non-buildings (rocks, trees, terrain), use collider bounds
return obstacle.bounds.size.y;
}
/// <summary>
/// Check if a grid cell is visible from viewer position (with height awareness)
/// </summary>
public bool IsCellVisible(Vector3 viewerPosition, Vector2Int cellGridPos, float visionRange)
{
Vector3 cellWorldPos = _fogSystem.GridToWorld(cellGridPos.x, cellGridPos.y);
Vector3 viewerEye = viewerPosition + Vector3.up * _fogSystem.viewerEyeHeight;
Vector3 toCell = cellWorldPos - viewerPosition;
float horizontalDistance = new Vector2(toCell.x, toCell.z).magnitude;
// Outside vision range
if (horizontalDistance > visionRange)
return false;
// Calculate angle to cell
float angle = Mathf.Atan2(toCell.z, toCell.x) * Mathf.Rad2Deg;
if (angle < 0) angle += 360f;
// Find corresponding sector
int sectorIndex = Mathf.FloorToInt(angle / _fogSystem.raycastAngularStep) % _sectorCount;
// Check if blocked by obstacle in this sector
if (_sectors[sectorIndex].hasObstacle)
{
// Dynamic tolerance to handle large mesh colliders
// Large rocks can be hit several units before their center
// Use cell size + generous buffer for very large obstacles
float distanceTolerance = _fogSystem.cellSize * 5.0f + 3.0f;
// Height-based blocking check
if (_fogSystem.enableHeightBlocking && _sectors[sectorIndex].obstacleHeight >= _fogSystem.minBlockingHeight)
{
float obstacleDistance = _sectors[sectorIndex].blockedDistance;
// If cell is beyond obstacle (with tolerance), check if obstacle blocks the sight line
if (horizontalDistance > obstacleDistance + distanceTolerance)
{
// Calculate sight line angle to cell
float verticalAngleToCell = Mathf.Atan2(cellWorldPos.y - viewerEye.y, horizontalDistance);
// Calculate obstacle top height
float obstacleTopHeight = _sectors[sectorIndex].obstaclePosition.y + _sectors[sectorIndex].obstacleHeight;
// Calculate angle to obstacle top
float verticalAngleToObstacleTop = Mathf.Atan2(obstacleTopHeight - viewerEye.y, obstacleDistance);
// If sight line passes below obstacle top, it's blocked
if (verticalAngleToCell <= verticalAngleToObstacleTop)
return false;
}
}
else
{
// Simple distance-based blocking (no height consideration)
// Use tolerance to allow objects at the obstacle position to be visible
if (horizontalDistance > _sectors[sectorIndex].blockedDistance + distanceTolerance)
return false;
}
}
// Detailed raycasting for edge cases
if (_fogSystem.useDetailedRaycasting)
{
float distanceTolerance = _fogSystem.cellSize * 3.0f + 2.0f;
int prevSector = (sectorIndex - 1 + _sectorCount) % _sectorCount;
int nextSector = (sectorIndex + 1) % _sectorCount;
if (_sectors[prevSector].hasObstacle && horizontalDistance > _sectors[prevSector].blockedDistance + distanceTolerance)
return false;
if (_sectors[nextSector].hasObstacle && horizontalDistance > _sectors[nextSector].blockedDistance + distanceTolerance)
return false;
}
return true;
}
}
/// <summary>
/// 전장의 안개 시스템 - 플레이어별 시야 관리
/// </summary>
@@ -19,7 +192,31 @@ namespace Northbound
[Header("Visibility Settings")]
public float updateInterval = 0.2f;
[Header("Line of Sight Settings")]
[Tooltip("Enable line-of-sight blocking by obstacles")]
public bool enableLineOfSight = true;
[Tooltip("Layers that block vision (buildings, obstacles, terrain)")]
public LayerMask visionBlockingLayers = ~0;
[Tooltip("Angular resolution for raycasting (degrees). Lower = more accurate, higher = better performance")]
[Range(1f, 15f)]
public float raycastAngularStep = 6f;
[Tooltip("Use detailed raycasting (more accurate but slower)")]
public bool useDetailedRaycasting = false;
[Header("Height-Based Visibility")]
[Tooltip("Enable height-based vision blocking (tall obstacles block vision)")]
public bool enableHeightBlocking = true;
[Tooltip("Viewer eye height for line-of-sight calculations")]
public float viewerEyeHeight = 1.5f;
[Tooltip("Minimum obstacle height to block vision")]
public float minBlockingHeight = 2.0f;
// 서버: 각 플레이어별 가시성 맵
private Dictionary<ulong, FogOfWarData> _serverFogData = new Dictionary<ulong, FogOfWarData>();
@@ -28,6 +225,7 @@ namespace Northbound
private List<IVisionProvider> _visionProviders = new List<IVisionProvider>();
private float _updateTimer;
private LineOfSightCalculator _losCalculator;
private void Awake()
{
@@ -37,12 +235,19 @@ namespace Northbound
return;
}
Instance = this;
_losCalculator = new LineOfSightCalculator(this);
}
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (IsServer)
{
// Server: Register client connected callback to initialize fog data
NetworkManager.Singleton.OnClientConnectedCallback += OnClientConnected;
}
if (IsClient && !IsServer)
{
// 클라이언트는 로컬 데이터 초기화
@@ -51,6 +256,28 @@ namespace Northbound
}
}
private void OnClientConnected(ulong clientId)
{
if (!IsServer) return;
// Ensure fog data exists for this client
if (!_serverFogData.ContainsKey(clientId))
{
_serverFogData[clientId] = new FogOfWarData(gridWidth, gridHeight);
Debug.Log($"<color=cyan>[FogOfWar] 클라이언트 {clientId} 안개 데이터 초기화</color>");
}
}
public override void OnNetworkDespawn()
{
base.OnNetworkDespawn();
if (IsServer && NetworkManager.Singleton != null)
{
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
}
}
private void Update()
{
if (!IsServer) return;
@@ -167,7 +394,7 @@ namespace Northbound
}
/// <summary>
/// 특정 영역을 밝힘 (서버만)
/// 특정 영역을 밝힘 (서버만) - Line-of-Sight 지원
/// </summary>
private void RevealArea(ulong clientId, Vector3 worldPosition, float radius)
{
@@ -176,11 +403,19 @@ namespace Northbound
int cellRadius = Mathf.CeilToInt(radius / cellSize);
// Line-of-sight raycasting if enabled
if (enableLineOfSight)
{
_losCalculator.CalculateVisibleSectors(worldPosition, radius);
}
for (int x = -cellRadius; x <= cellRadius; x++)
{
for (int y = -cellRadius; y <= cellRadius; y++)
{
if (x * x + y * y > cellRadius * cellRadius) continue;
// Basic circular range check
if (x * x + y * y > cellRadius * cellRadius)
continue;
int gridX = gridPos.x + x;
int gridY = gridPos.y + y;
@@ -188,6 +423,14 @@ namespace Northbound
if (gridX < 0 || gridX >= gridWidth || gridY < 0 || gridY >= gridHeight)
continue;
// Line-of-sight check
if (enableLineOfSight)
{
Vector2Int cellPos = new Vector2Int(gridX, gridY);
if (!_losCalculator.IsCellVisible(worldPosition, cellPos, radius))
continue;
}
// 현재 시야에 표시 + 방문한 적 있음으로 기록
fogData.SetVisible(gridX, gridY, true);
fogData.SetExplored(gridX, gridY, true);

View File

@@ -0,0 +1,349 @@
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("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;
[Tooltip("Enable debug logging for this object")]
public bool debugLogging = false;
[Header("Height-Based Distant Visibility")]
[Tooltip("Enable visibility from farther away based on object height")]
public bool enableDistantVisibility = true;
[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 _localClientId;
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 (debugLogging)
{
Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Auto-detected {renderers.Length} renderers");
}
}
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();
if (debugLogging)
{
Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Object height = {_objectHeight}m");
}
// CRITICAL: Start hidden and stay hidden until fog system confirms visibility
if (debugLogging)
{
Debug.Log($"[FogOfWarVisibility] {gameObject.name}: START - Setting all {renderers.Length} renderers to HIDDEN");
}
// Force initial hide - don't use SetVisible because _isVisible defaults to false
// which would cause early return
_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)
{
_localClientId = NetworkManager.Singleton.LocalClientId;
_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();
}
}
private void UpdateVisibility()
{
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(_localClientId);
if (fogData == null)
{
// Fog data not ready yet - stay hidden
SetVisible(false);
return;
}
FogOfWarState fogState = fogSystem.GetVisibilityState(_localClientId, transform.position);
// Check for distant visibility based on height
bool isDistantVisible = false;
if (enableDistantVisibility && _objectHeight >= minHeightForDistantVisibility)
{
isDistantVisible = CheckDistantVisibility(fogSystem);
}
if (debugLogging)
{
Debug.Log($"[FogOfWarVisibility] {gameObject.name} at {transform.position}: State={fogState}, DistantVisible={isDistantVisible}, Height={_objectHeight}m");
}
switch (fogState)
{
case FogOfWarState.Visible:
// Currently visible - show with original materials
if (debugLogging) Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Setting VISIBLE");
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)
{
if (debugLogging) Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Setting EXPLORED (visible) - distantVisible={isDistantVisible}");
SetVisible(true);
SetExploredVisual(true);
}
else
{
if (debugLogging) Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Setting EXPLORED (hidden)");
SetVisible(false);
}
break;
case FogOfWarState.Unexplored:
// Never seen - hide unless tall enough to see from distance
if (isDistantVisible)
{
if (debugLogging) Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Setting UNEXPLORED but DISTANT VISIBLE");
SetVisible(true);
SetExploredVisual(true); // Show as explored/distant
}
else
{
if (debugLogging) Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Setting UNEXPLORED (hidden)");
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 = (_objectHeight - minHeightForDistantVisibility) * heightVisibilityMultiplier;
// Get player's vision range (assume average vision provider has ~15 unit range)
float baseVisionRange = 15f; // You can make this configurable
float totalRange = baseVisionRange + extendedRange;
if (debugLogging)
{
Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Distance={distanceToPlayer:F1}m, ExtendedRange={totalRange:F1}m (height bonus: +{extendedRange:F1}m)");
}
return distanceToPlayer <= totalRange;
}
private void SetVisible(bool visible)
{
if (_isVisible == visible) return;
_isVisible = visible;
if (renderers == null || renderers.Length == 0)
{
if (debugLogging)
{
Debug.LogWarning($"[FogOfWarVisibility] {gameObject.name}: SetVisible({visible}) called but no renderers!");
}
return;
}
if (debugLogging)
{
Debug.Log($"[FogOfWarVisibility] {gameObject.name}: SetVisible({visible}) - updating {renderers.Length} renderers");
}
foreach (var renderer in renderers)
{
if (renderer != null)
{
renderer.enabled = visible;
if (debugLogging)
{
Debug.Log($"[FogOfWarVisibility] {gameObject.name}: Renderer '{renderer.name}' 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);
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 760137a2fd0da7f458ac4b0ee7f485d6

View File

@@ -254,6 +254,14 @@ namespace Northbound
obstacle.transform.localScale *= scale;
}
// Add FogOfWarVisibility component to hide obstacles in unexplored areas
if (obstacle.GetComponent<FogOfWarVisibility>() == null)
{
var visibility = obstacle.AddComponent<FogOfWarVisibility>();
visibility.showInExploredAreas = false; // Obstacles hidden when not visible
visibility.updateInterval = 0.2f;
}
_spawnedPositions.Add(randomPos);
Debug.Log($"<color=green>[Spawn] 장애물 배치 성공: {obstacleEntry.prefab.name} at {randomPos}</color>");
return true;