714 lines
25 KiB
C#
714 lines
25 KiB
C#
using System.Collections.Generic;
|
|
using Unity.Netcode;
|
|
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>
|
|
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;
|
|
|
|
[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;
|
|
|
|
[Header("Editor Settings")]
|
|
[Tooltip("Disable fog of war in Unity Editor for easier development")]
|
|
public bool disableInEditor = false;
|
|
|
|
// 서버: 각 플레이어별 가시성 맵
|
|
private Dictionary<ulong, FogOfWarData> _serverFogData = new Dictionary<ulong, FogOfWarData>();
|
|
|
|
// 클라이언트: 로컬 플레이어의 가시성 맵
|
|
private FogOfWarData _localFogData;
|
|
|
|
private List<IVisionProvider> _visionProviders = new List<IVisionProvider>();
|
|
private float _updateTimer;
|
|
private LineOfSightCalculator _losCalculator;
|
|
|
|
private void Awake()
|
|
{
|
|
if (Instance != null && Instance != this)
|
|
{
|
|
Destroy(gameObject);
|
|
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;
|
|
|
|
// 호스트 자신의 데이터도 초기화 (OnClientConnected가 호스트에게는 호출되지 않을 수 있음)
|
|
ulong hostClientId = NetworkManager.Singleton.LocalClientId;
|
|
if (!_serverFogData.ContainsKey(hostClientId))
|
|
{
|
|
_serverFogData[hostClientId] = new FogOfWarData(gridWidth, gridHeight);
|
|
}
|
|
}
|
|
|
|
// 클라이언트는 로컬 데이터 초기화 (호스트 포함)
|
|
if (IsClient)
|
|
{
|
|
_localFogData = new FogOfWarData(gridWidth, gridHeight);
|
|
}
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|
|
|
|
public override void OnNetworkDespawn()
|
|
{
|
|
base.OnNetworkDespawn();
|
|
|
|
if (IsServer && NetworkManager.Singleton != null)
|
|
{
|
|
NetworkManager.Singleton.OnClientConnectedCallback -= OnClientConnected;
|
|
}
|
|
}
|
|
|
|
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);
|
|
// 즉시 시야 업데이트 트리거
|
|
_updateTimer = updateInterval;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 시야 제공자 등록 해제
|
|
/// </summary>
|
|
public void UnregisterVisionProvider(IVisionProvider provider)
|
|
{
|
|
_visionProviders.Remove(provider);
|
|
}
|
|
|
|
/// <summary>
|
|
/// 플레이어별 FogOfWar 데이터 가져오기
|
|
/// </summary>
|
|
public FogOfWarData GetPlayerFogData(ulong clientId)
|
|
{
|
|
// 클라이언트(호스트 포함)는 자신의 로컬 데이터 반환
|
|
// 서버에서 계산된 시야 데이터는 ClientRpc로 동기화됨
|
|
if (IsClient)
|
|
{
|
|
if (_localFogData == null)
|
|
{
|
|
_localFogData = new FogOfWarData(gridWidth, gridHeight);
|
|
}
|
|
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();
|
|
TeamType providerTeam = provider.GetTeam();
|
|
|
|
if (!_serverFogData.ContainsKey(ownerId))
|
|
{
|
|
_serverFogData[ownerId] = new FogOfWarData(gridWidth, gridHeight);
|
|
}
|
|
|
|
Vector3 position = provider.GetTransform().position;
|
|
float visionRange = provider.GetVisionRange();
|
|
|
|
// 같은 팀의 모든 멤버에게 시야 공유
|
|
RevealAreaForTeam(providerTeam, position, visionRange);
|
|
}
|
|
|
|
// 각 클라이언트에게 시야 데이터 전송
|
|
foreach (var kvp in _serverFogData)
|
|
{
|
|
ulong clientId = kvp.Key;
|
|
FogOfWarData fogData = kvp.Value;
|
|
|
|
// 해당 클라이언트가 여전히 연결되어 있는지 확인
|
|
// 호스트의 경우 ConnectedClients에 없을 수 있으므로 별도 체크
|
|
bool isHost = (clientId == NetworkManager.Singleton.LocalClientId);
|
|
bool isConnected = NetworkManager.Singleton.ConnectedClients.ContainsKey(clientId);
|
|
|
|
if (!isHost && !isConnected)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
// 압축된 데이터로 전송
|
|
byte[] visibleData = fogData.GetVisibleData();
|
|
byte[] exploredData = fogData.GetExploredData();
|
|
|
|
SendFogDataToClientRpc(visibleData, exploredData,
|
|
new ClientRpcParams
|
|
{
|
|
Send = new ClientRpcSendParams
|
|
{
|
|
TargetClientIds = new ulong[] { clientId }
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 같은 팀의 모든 멤버에게 시야 공개
|
|
/// </summary>
|
|
private void RevealAreaForTeam(TeamType team, Vector3 worldPosition, float radius)
|
|
{
|
|
// 해당 팀의 모든 멤버 찾기
|
|
foreach (var kvp in _serverFogData)
|
|
{
|
|
ulong clientId = kvp.Key;
|
|
|
|
// 클라이언트의 팀 확인
|
|
if (GetClientTeam(clientId) == team)
|
|
{
|
|
RevealArea(clientId, worldPosition, radius);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 클라이언트의 팀 가져오기
|
|
/// </summary>
|
|
private TeamType GetClientTeam(ulong clientId)
|
|
{
|
|
if (NetworkManager.Singleton == null) return TeamType.Neutral;
|
|
|
|
// 연결된 클라이언트에서 플레이어 오브젝트 찾기
|
|
if (NetworkManager.Singleton.SpawnManager.SpawnedObjects != null)
|
|
{
|
|
foreach (var netObj in NetworkManager.Singleton.SpawnManager.SpawnedObjects.Values)
|
|
{
|
|
var playerController = netObj.GetComponent<NetworkPlayerController>();
|
|
if (playerController != null && playerController.OwnerPlayerId == clientId)
|
|
{
|
|
return playerController.GetTeam();
|
|
}
|
|
}
|
|
}
|
|
|
|
return TeamType.Neutral;
|
|
}
|
|
|
|
/// <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>
|
|
/// 특정 영역을 밝힘 (서버만) - Line-of-Sight 지원
|
|
/// </summary>
|
|
private void RevealArea(ulong clientId, Vector3 worldPosition, float radius)
|
|
{
|
|
FogOfWarData fogData = _serverFogData[clientId];
|
|
Vector2Int gridPos = WorldToGrid(worldPosition);
|
|
|
|
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++)
|
|
{
|
|
// Basic circular range check
|
|
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;
|
|
|
|
// 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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
{
|
|
#if UNITY_EDITOR
|
|
if (disableInEditor)
|
|
return FogOfWarState.Visible;
|
|
#endif
|
|
|
|
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 // 현재 시야 안 (모두 보임)
|
|
}
|
|
} |