전장의 안개 기능 개선

미탐험 구역의 모든 오브젝트는 보이지 않음
적이 시야를 제공하는 문제 수정
높은 장애물은 더 먼 거리에서부터 보일 수 있음
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

@@ -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);