전장의 안개 기능 개선
미탐험 구역의 모든 오브젝트는 보이지 않음 적이 시야를 제공하는 문제 수정 높은 장애물은 더 먼 거리에서부터 보일 수 있음
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user