Files
Northbound/Assets/Scripts/FogOfWarSystem.cs
2026-01-27 12:38:18 +09:00

397 lines
13 KiB
C#

using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
namespace Northbound
{
/// <summary>
/// 전장의 안개 시스템 - 플레이어별 시야 관리
/// </summary>
public class FogOfWarSystem : NetworkBehaviour
{
public static FogOfWarSystem Instance { get; private set; }
[Header("Grid Settings")]
public int gridWidth = 100;
public int gridHeight = 100;
public float cellSize = 1f;
public Vector3 worldOrigin = Vector3.zero;
[Header("Visibility Settings")]
public float updateInterval = 0.2f;
// 서버: 각 플레이어별 가시성 맵
private Dictionary<ulong, FogOfWarData> _serverFogData = new Dictionary<ulong, FogOfWarData>();
// 클라이언트: 로컬 플레이어의 가시성 맵
private FogOfWarData _localFogData;
private List<IVisionProvider> _visionProviders = new List<IVisionProvider>();
private float _updateTimer;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
public override void OnNetworkSpawn()
{
base.OnNetworkSpawn();
if (IsClient && !IsServer)
{
// 클라이언트는 로컬 데이터 초기화
_localFogData = new FogOfWarData(gridWidth, gridHeight);
Debug.Log($"<color=cyan>[FogOfWar] 클라이언트 {NetworkManager.LocalClientId} 초기화</color>");
}
}
private void Update()
{
if (!IsServer) return;
_updateTimer += Time.deltaTime;
if (_updateTimer >= updateInterval)
{
_updateTimer = 0f;
UpdateAllVision();
}
}
/// <summary>
/// 시야 제공자 등록 (플레이어, 건물 등)
/// </summary>
public void RegisterVisionProvider(IVisionProvider provider)
{
if (!_visionProviders.Contains(provider))
{
_visionProviders.Add(provider);
Debug.Log($"<color=cyan>[FogOfWar] Vision Provider 등록: {provider.GetTransform().name} (Owner: {provider.GetOwnerId()})</color>");
}
}
/// <summary>
/// 시야 제공자 등록 해제
/// </summary>
public void UnregisterVisionProvider(IVisionProvider provider)
{
_visionProviders.Remove(provider);
}
/// <summary>
/// 플레이어별 FogOfWar 데이터 가져오기
/// </summary>
public FogOfWarData GetPlayerFogData(ulong clientId)
{
// 클라이언트는 자신의 로컬 데이터 반환
if (IsClient && !IsServer)
{
return _localFogData;
}
// 서버는 해당 클라이언트의 데이터 반환
if (!_serverFogData.ContainsKey(clientId))
{
_serverFogData[clientId] = new FogOfWarData(gridWidth, gridHeight);
}
return _serverFogData[clientId];
}
/// <summary>
/// 모든 시야 업데이트 (서버만)
/// </summary>
private void UpdateAllVision()
{
// 각 플레이어의 현재 시야 초기화
foreach (var fogData in _serverFogData.Values)
{
fogData.ClearCurrentVision();
}
// 모든 시야 제공자의 시야 범위 계산
foreach (var provider in _visionProviders)
{
if (provider == null || !provider.IsActive()) continue;
ulong ownerId = provider.GetOwnerId();
if (!_serverFogData.ContainsKey(ownerId))
{
_serverFogData[ownerId] = new FogOfWarData(gridWidth, gridHeight);
}
Vector3 position = provider.GetTransform().position;
float visionRange = provider.GetVisionRange();
RevealArea(ownerId, position, visionRange);
}
// 각 클라이언트에게 시야 데이터 전송
foreach (var kvp in _serverFogData)
{
ulong clientId = kvp.Key;
FogOfWarData fogData = kvp.Value;
// 압축된 데이터로 전송
byte[] visibleData = fogData.GetVisibleData();
byte[] exploredData = fogData.GetExploredData();
SendFogDataToClientRpc(visibleData, exploredData,
new ClientRpcParams
{
Send = new ClientRpcSendParams
{
TargetClientIds = new ulong[] { clientId }
}
});
}
}
/// <summary>
/// 클라이언트에게 안개 데이터 전송
/// </summary>
[ClientRpc]
private void SendFogDataToClientRpc(byte[] visibleData, byte[] exploredData, ClientRpcParams clientRpcParams = default)
{
if (_localFogData == null)
{
_localFogData = new FogOfWarData(gridWidth, gridHeight);
}
_localFogData.SetVisibleData(visibleData);
_localFogData.SetExploredData(exploredData);
}
/// <summary>
/// 특정 영역을 밝힘 (서버만)
/// </summary>
private void RevealArea(ulong clientId, Vector3 worldPosition, float radius)
{
FogOfWarData fogData = _serverFogData[clientId];
Vector2Int gridPos = WorldToGrid(worldPosition);
int cellRadius = Mathf.CeilToInt(radius / cellSize);
for (int x = -cellRadius; x <= cellRadius; x++)
{
for (int y = -cellRadius; y <= cellRadius; y++)
{
if (x * x + y * y > cellRadius * cellRadius) continue;
int gridX = gridPos.x + x;
int gridY = gridPos.y + y;
if (gridX < 0 || gridX >= gridWidth || gridY < 0 || gridY >= gridHeight)
continue;
// 현재 시야에 표시 + 방문한 적 있음으로 기록
fogData.SetVisible(gridX, gridY, true);
fogData.SetExplored(gridX, gridY, true);
}
}
}
/// <summary>
/// 월드 좌표를 그리드 좌표로 변환
/// </summary>
public Vector2Int WorldToGrid(Vector3 worldPos)
{
Vector3 localPos = worldPos - worldOrigin;
int x = Mathf.FloorToInt(localPos.x / cellSize);
int z = Mathf.FloorToInt(localPos.z / cellSize);
return new Vector2Int(x, z);
}
/// <summary>
/// 그리드 좌표를 월드 좌표로 변환
/// </summary>
public Vector3 GridToWorld(int x, int y)
{
return worldOrigin + new Vector3(x * cellSize, 0, y * cellSize);
}
/// <summary>
/// 특정 위치가 플레이어에게 보이는지 확인
/// </summary>
public FogOfWarState GetVisibilityState(ulong clientId, Vector3 worldPosition)
{
FogOfWarData fogData = GetPlayerFogData(clientId);
if (fogData == null)
return FogOfWarState.Unexplored;
Vector2Int gridPos = WorldToGrid(worldPosition);
return fogData.GetState(gridPos.x, gridPos.y);
}
private void OnDrawGizmos()
{
// 그리드 시각화
Gizmos.color = Color.yellow;
Vector3 size = new Vector3(gridWidth * cellSize, 0.1f, gridHeight * cellSize);
Gizmos.DrawWireCube(worldOrigin + size / 2f, size);
}
}
/// <summary>
/// 플레이어별 안개 데이터
/// </summary>
public class FogOfWarData
{
private bool[,] _explored; // 한번이라도 방문한 적이 있는가
private bool[,] _currentlyVisible; // 현재 보이는가
private int _width;
private int _height;
public FogOfWarData(int width, int height)
{
_width = width;
_height = height;
_explored = new bool[width, height];
_currentlyVisible = new bool[width, height];
}
public void SetExplored(int x, int y, bool value)
{
if (IsValidCoord(x, y))
_explored[x, y] = value;
}
public void SetVisible(int x, int y, bool value)
{
if (IsValidCoord(x, y))
_currentlyVisible[x, y] = value;
}
public void ClearCurrentVision()
{
System.Array.Clear(_currentlyVisible, 0, _currentlyVisible.Length);
}
public FogOfWarState GetState(int x, int y)
{
if (!IsValidCoord(x, y))
return FogOfWarState.Unexplored;
if (_currentlyVisible[x, y])
return FogOfWarState.Visible;
if (_explored[x, y])
return FogOfWarState.Explored;
return FogOfWarState.Unexplored;
}
private bool IsValidCoord(int x, int y)
{
return x >= 0 && x < _explored.GetLength(0) &&
y >= 0 && y < _explored.GetLength(1);
}
/// <summary>
/// Visible 데이터를 바이트 배열로 압축
/// </summary>
public byte[] GetVisibleData()
{
int totalCells = _width * _height;
int byteCount = (totalCells + 7) / 8; // 8비트 = 1바이트
byte[] data = new byte[byteCount];
for (int i = 0; i < totalCells; i++)
{
int x = i % _width;
int y = i / _width;
if (_currentlyVisible[x, y])
{
int byteIndex = i / 8;
int bitIndex = i % 8;
data[byteIndex] |= (byte)(1 << bitIndex);
}
}
return data;
}
/// <summary>
/// Explored 데이터를 바이트 배열로 압축
/// </summary>
public byte[] GetExploredData()
{
int totalCells = _width * _height;
int byteCount = (totalCells + 7) / 8;
byte[] data = new byte[byteCount];
for (int i = 0; i < totalCells; i++)
{
int x = i % _width;
int y = i / _width;
if (_explored[x, y])
{
int byteIndex = i / 8;
int bitIndex = i % 8;
data[byteIndex] |= (byte)(1 << bitIndex);
}
}
return data;
}
/// <summary>
/// 바이트 배열에서 Visible 데이터 복원
/// </summary>
public void SetVisibleData(byte[] data)
{
System.Array.Clear(_currentlyVisible, 0, _currentlyVisible.Length);
int totalCells = _width * _height;
for (int i = 0; i < totalCells && i / 8 < data.Length; i++)
{
int byteIndex = i / 8;
int bitIndex = i % 8;
bool isVisible = (data[byteIndex] & (1 << bitIndex)) != 0;
int x = i % _width;
int y = i / _width;
_currentlyVisible[x, y] = isVisible;
}
}
/// <summary>
/// 바이트 배열에서 Explored 데이터 복원
/// </summary>
public void SetExploredData(byte[] data)
{
int totalCells = _width * _height;
for (int i = 0; i < totalCells && i / 8 < data.Length; i++)
{
int byteIndex = i / 8;
int bitIndex = i % 8;
bool isExplored = (data[byteIndex] & (1 << bitIndex)) != 0;
int x = i % _width;
int y = i / _width;
// Explored는 누적 (한번 탐색하면 계속 유지)
if (isExplored)
_explored[x, y] = true;
}
}
}
/// <summary>
/// 안개 상태
/// </summary>
public enum FogOfWarState
{
Unexplored, // 방문한 적 없음 (완전히 어두움)
Explored, // 방문했지만 현재 시야 밖 (지형/건물만 보임)
Visible // 현재 시야 안 (모두 보임)
}
}