Files
Northbound/Assets/Scripts/BuildingManager.cs
dal4segno 979f2402c7 타워 이외의 건물을 건설할 수 없는 문제 수정
Lv1만 보이게 하면서, 보이는 index와 실제 index가 달라져서 발생
2026-02-25 16:38:31 +09:00

521 lines
19 KiB
C#

using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using Northbound.Data;
namespace Northbound
{
public class BuildingManager : NetworkBehaviour
{
public static BuildingManager Instance { get; private set; }
[Header("Settings")]
public float gridSize = 1f;
public LayerMask groundLayer;
[Header("Building Database")]
public List<TowerData> availableBuildings = new List<TowerData>();
[Header("Foundation Settings")]
public GameObject foundationPrefab; // 토대 프리팹 (Inspector에서 할당)
private List<Building> placedBuildings = new List<Building>();
private List<BuildingFoundation> placedFoundations = new List<BuildingFoundation>();
// Level 1 건물 캐시
private List<TowerData> _buildableBuildingsCache;
private bool _buildableCacheDirty = true;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
}
public Vector3 SnapToGrid(Vector3 worldPosition)
{
return new Vector3(
Mathf.Round(worldPosition.x / gridSize) * gridSize,
worldPosition.y,
Mathf.Round(worldPosition.z / gridSize) * gridSize
);
}
/// <summary>
/// 건설 가능한 건물 목록 반환 (Level 1만, 캐싱됨)
/// </summary>
public List<TowerData> GetBuildableBuildings()
{
if (_buildableCacheDirty || _buildableBuildingsCache == null)
{
_buildableBuildingsCache = new List<TowerData>();
foreach (var building in availableBuildings)
{
if (building != null && building.level == 1)
{
_buildableBuildingsCache.Add(building);
}
}
_buildableCacheDirty = false;
}
return _buildableBuildingsCache;
}
public Vector3Int WorldToGrid(Vector3 worldPosition)
{
return new Vector3Int(
Mathf.RoundToInt(worldPosition.x / gridSize),
Mathf.RoundToInt(worldPosition.y / gridSize),
Mathf.RoundToInt(worldPosition.z / gridSize)
);
}
public Vector3 GridToWorld(Vector3Int gridPosition)
{
return new Vector3(
gridPosition.x * gridSize,
gridPosition.y * gridSize,
gridPosition.z * gridSize
);
}
public bool IsValidPlacement(TowerData data, Vector3 position, int rotation, out Vector3 groundPosition)
{
groundPosition = position;
// Ground check
if (!CheckGround(position, out groundPosition))
{
return false;
}
// IMPORTANT: Snap to grid BEFORE checking overlap!
// Otherwise we check the raw cursor position which might overlap
Vector3 snappedPosition = SnapToGrid(groundPosition);
groundPosition = snappedPosition; // Update groundPosition to snapped value
// Overlap check using GRID SIZE from TowerData (not actual colliders)
Vector3 gridSize = data.GetSize(rotation);
// 프리팹의 실제 배치 위치 계산 (placementOffset 포함)
Vector3 actualBuildingPosition = snappedPosition + data.placementOffset;
// Bounds의 중심을 실제 건물/토대 위치를 기준으로 계산
Vector3 boundsCenter = actualBuildingPosition + Vector3.up * gridSize.y * 0.5f;
// Shrink bounds slightly to allow buildings to touch without overlapping
// This prevents Bounds.Intersects() from returning true for adjacent buildings
Vector3 shrunkSize = gridSize - Vector3.one * 0.01f;
Bounds checkBounds = new Bounds(boundsCenter, shrunkSize);
// 기존 건물과의 충돌 체크
foreach (var building in placedBuildings)
{
if (building == null) continue;
// Compare grid bounds, not collider bounds
Bounds buildingGridBounds = building.GetGridBounds();
if (checkBounds.Intersects(buildingGridBounds))
{
return false;
}
}
// 토대와의 충돌 체크
foreach (var foundation in placedFoundations)
{
if (foundation == null) continue;
Bounds foundationGridBounds = foundation.GetGridBounds();
if (checkBounds.Intersects(foundationGridBounds))
{
return false;
}
}
// 물리적 충돌 체크 (플레이어, 유닛, 기타 오브젝트와의 충돌)
// Physics.OverlapBox를 사용하여 해당 위치에 다른 Collider가 있는지 확인
Collider[] colliders = Physics.OverlapBox(
boundsCenter,
shrunkSize * 0.5f,
Quaternion.Euler(0, rotation * 90f, 0),
~groundLayer // groundLayer를 제외한 모든 레이어와 충돌 검사
);
// 충돌한 Collider가 있으면 배치 불가
if (colliders.Length > 0)
{
return false;
}
return true;
}
/// <summary>
/// Get the grid bounds for a building at a given position
/// Useful for preview visualization
/// </summary>
public Bounds GetPlacementBounds(TowerData data, Vector3 position, int rotation)
{
Vector3 gridSize = data.GetSize(rotation);
// position은 이미 placementOffset이 적용된 위치
return new Bounds(position + Vector3.up * gridSize.y * 0.5f, gridSize);
}
private bool CheckGround(Vector3 position, out Vector3 groundPosition)
{
groundPosition = position;
// Raycast down to find ground
if (Physics.Raycast(position + Vector3.up * 10f, Vector3.down, out RaycastHit hit, 20f, groundLayer))
{
groundPosition = hit.point;
return true;
}
return false;
}
/// <summary>
/// 건물 배치 요청 (클라이언트에서 호출)
/// </summary>
public void RequestPlaceBuilding(string buildingDataName, Vector3 position, int rotation)
{
if (!NetworkManager.Singleton.IsClient)
{
Debug.LogWarning("[BuildingManager] 클라이언트가 아닙니다.");
return;
}
ulong clientId = NetworkManager.Singleton.LocalClientId;
PlaceBuildingServerRpc(buildingDataName, position, rotation, clientId);
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void PlaceBuildingServerRpc(string buildingDataName, Vector3 position, int rotation, ulong requestingClientId)
{
// 보안 검증 1: 유효한 클라이언트인지 확인
if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(requestingClientId))
{
return;
}
// 보안 검증 2: 건물 데이터 찾기 (이름으로 검색)
TowerData data = availableBuildings.Find(b => b != null && b.name == buildingDataName);
if (data == null)
{
Debug.LogWarning($"[BuildingManager] 건물 데이터를 찾을 수 없습니다: {buildingDataName}");
return;
}
// 보안 검증 3: 프리팹 확인
if (data.prefab == null)
{
Debug.LogWarning($"[BuildingManager] 건물 프리팹이 없습니다: {buildingDataName}");
return;
}
// 자원 확인 및 소비
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager == null)
{
return;
}
if (!coreResourceManager.CanAfford(data.mana))
{
return;
}
coreResourceManager.SpendResources(data.mana);
// 배치 가능 여부 확인
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
{
return;
}
Vector3Int gridPosition = WorldToGrid(snappedPosition);
// 건물 생성
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)
{
// 건물의 소유자를 설정
netObj.SpawnWithOwnership(requestingClientId);
Building building = buildingObj.GetComponent<Building>();
if (building == null)
{
building = buildingObj.AddComponent<Building>();
}
// 건물 초기화
building.Initialize(data, gridPosition, rotation, requestingClientId);
placedBuildings.Add(building);
}
else
{
Debug.LogError($"<color=red>[BuildingManager] NetworkObject 컴포넌트가 없습니다! (Prefab: {data.prefab.name})</color>");
Destroy(buildingObj);
}
}
public void RemoveBuilding(Building building)
{
if (placedBuildings.Contains(building))
{
placedBuildings.Remove(building);
}
}
/// <summary>
/// 토대 제거 (건설 완료 또는 취소 시)
/// </summary>
public void RemoveFoundation(BuildingFoundation foundation)
{
if (placedFoundations.Contains(foundation))
{
placedFoundations.Remove(foundation);
}
}
/// <summary>
/// 사전 배치 건물 등록 (씬에 미리 있는 건물용)
/// </summary>
public void RegisterPrePlacedBuilding(Building building)
{
if (!placedBuildings.Contains(building))
{
placedBuildings.Add(building);
}
}
/// <summary>
/// 특정 위치에 건물이 있는지 확인
/// </summary>
public Building GetBuildingAtPosition(Vector3Int gridPosition)
{
foreach (var building in placedBuildings)
{
if (building != null && building.gridPosition == gridPosition)
{
return building;
}
}
return null;
}
/// <summary>
/// 특정 소유자의 모든 건물 가져오기
/// </summary>
public List<Building> GetBuildingsByOwner(ulong ownerId)
{
List<Building> ownedBuildings = new List<Building>();
foreach (var building in placedBuildings)
{
if (building != null && building.GetOwnerId() == ownerId)
{
ownedBuildings.Add(building);
}
}
return ownedBuildings;
}
/// <summary>
/// 건물 토대 배치 요청 (클라이언트에서 호출)
/// </summary>
public void RequestPlaceFoundation(string buildingDataName, Vector3 position, int rotation)
{
if (!NetworkManager.Singleton.IsClient)
{
Debug.LogWarning("[BuildingManager] 클라이언트가 아닙니다.");
return;
}
ulong clientId = NetworkManager.Singleton.LocalClientId;
PlaceFoundationServerRpc(buildingDataName, position, rotation, clientId);
}
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
private void PlaceFoundationServerRpc(string buildingDataName, Vector3 position, int rotation, ulong requestingClientId)
{
// 보안 검증 1: 유효한 클라이언트인지 확인
if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(requestingClientId))
{
return;
}
// 보안 검증 2: 건물 데이터 찾기 (이름으로 검색)
TowerData data = availableBuildings.Find(b => b != null && b.name == buildingDataName);
if (data == null)
{
Debug.LogWarning($"[BuildingManager] 건물 데이터를 찾을 수 없습니다: {buildingDataName}");
return;
}
// 자원 확인 및 소비
var coreResourceManager = CoreResourceManager.Instance;
if (coreResourceManager == null)
{
return;
}
if (!coreResourceManager.CanAfford(data.mana))
{
return;
}
coreResourceManager.SpendResources(data.mana);
// 토대 프리팹 확인
if (foundationPrefab == null)
{
Debug.LogError("<color=red>[BuildingManager] foundationPrefab이 설정되지 않았습니다!</color>");
return;
}
// 배치 가능 여부 확인
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
{
// 자원 환불
coreResourceManager.AddResources(data.mana);
return;
}
Vector3Int gridPosition = WorldToGrid(snappedPosition);
// 플레이어 팀 가져오기
TeamType playerTeam = GetPlayerTeam(requestingClientId);
// 토대 생성
GameObject foundationObj = Instantiate(foundationPrefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
NetworkObject netObj = foundationObj.GetComponent<NetworkObject>();
if (netObj != null)
{
// 스폰 먼저 실행 (서버 소유권으로 스폰)
// 소유자 정보는 _ownerId NetworkVariable에 별도로 저장하므로 NetworkObject 소유권은 서버 유지
netObj.Spawn();
// 스폰 후에 초기화 (OnNetworkSpawn 이후에 호출되어 타이밍 문제 해결)
BuildingFoundation foundation = foundationObj.GetComponent<BuildingFoundation>();
if (foundation == null)
{
Destroy(foundationObj);
return;
}
// Initialize에서 NetworkVariable을 설정하고 데이터를 로드함
foundation.Initialize(data, gridPosition, rotation, requestingClientId, playerTeam);
// 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;
}
placedFoundations.Add(foundation); // 토대 목록에 추가
}
else
{
Destroy(foundationObj);
}
}
/// <summary>
/// 플레이어의 팀 가져오기
/// </summary>
private TeamType GetPlayerTeam(ulong playerId)
{
// 플레이어의 NetworkObject 찾기
if (NetworkManager.Singleton != null && NetworkManager.Singleton.ConnectedClients != null)
{
if (NetworkManager.Singleton.ConnectedClients.TryGetValue(playerId, out var client))
{
if (client.PlayerObject != null)
{
var teamMember = client.PlayerObject.GetComponent<ITeamMember>();
if (teamMember != null)
{
return teamMember.GetTeam();
}
}
}
}
// 기본값: 플레이어 팀
return TeamType.Player;
}
/// <summary>
/// 건설 완료 시 완성된 건물 생성 (BuildingFoundation에서 호출)
/// </summary>
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void SpawnCompletedBuildingServerRpc(string buildingDataName, Vector3Int gridPosition, int rotation, ulong ownerId, TeamType team)
{
// TowerData 찾기
TowerData data = availableBuildings.Find(b => b.name == buildingDataName);
if (data == null || data.prefab == null)
{
Debug.LogError($"<color=red>[BuildingManager] 건물 데이터를 찾을 수 없습니다: {buildingDataName}</color>");
return;
}
Vector3 worldPosition = GridToWorld(gridPosition);
// 완성된 건물 생성
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);
Building building = buildingObj.GetComponent<Building>();
if (building == null)
{
building = buildingObj.AddComponent<Building>();
}
// 건물 초기화
building.Initialize(data, gridPosition, rotation, ownerId);
placedBuildings.Add(building);
}
else
{
Debug.LogError($"<color=red>[BuildingManager] NetworkObject 컴포넌트가 없습니다!</color>");
Destroy(buildingObj);
}
}
}
}