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 availableBuildings = new List(); [Header("Foundation Settings")] public GameObject foundationPrefab; // 토대 프리팹 (Inspector에서 할당) private List placedBuildings = new List(); private List placedFoundations = new List(); // Level 1 건물 캐시 private List _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 ); } /// /// 건설 가능한 건물 목록 반환 (Level 1만, 캐싱됨) /// public List GetBuildableBuildings() { if (_buildableCacheDirty || _buildableBuildingsCache == null) { _buildableBuildingsCache = new List(); 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; } /// /// Get the grid bounds for a building at a given position /// Useful for preview visualization /// 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; } /// /// 건물 배치 요청 (클라이언트에서 호출) /// 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(); // Add FogOfWarVisibility component to hide buildings in unexplored areas if (buildingObj.GetComponent() == null) { var visibility = buildingObj.AddComponent(); visibility.showInExploredAreas = true; // Buildings remain visible in explored areas visibility.updateInterval = 0.2f; } if (netObj != null) { // 건물의 소유자를 설정 netObj.SpawnWithOwnership(requestingClientId); Building building = buildingObj.GetComponent(); if (building == null) { building = buildingObj.AddComponent(); } // 건물 초기화 building.Initialize(data, gridPosition, rotation, requestingClientId); placedBuildings.Add(building); } else { Debug.LogError($"[BuildingManager] NetworkObject 컴포넌트가 없습니다! (Prefab: {data.prefab.name})"); Destroy(buildingObj); } } public void RemoveBuilding(Building building) { if (placedBuildings.Contains(building)) { placedBuildings.Remove(building); } } /// /// 토대 제거 (건설 완료 또는 취소 시) /// public void RemoveFoundation(BuildingFoundation foundation) { if (placedFoundations.Contains(foundation)) { placedFoundations.Remove(foundation); } } /// /// 토대 등록 (업그레이드 등으로 생성된 토대) /// public void RegisterFoundation(BuildingFoundation foundation) { if (!placedFoundations.Contains(foundation)) { placedFoundations.Add(foundation); } } /// /// 사전 배치 건물 등록 (씬에 미리 있는 건물용) /// public void RegisterPrePlacedBuilding(Building building) { if (!placedBuildings.Contains(building)) { placedBuildings.Add(building); } } /// /// 특정 위치에 건물이 있는지 확인 /// public Building GetBuildingAtPosition(Vector3Int gridPosition) { foreach (var building in placedBuildings) { if (building != null && building.gridPosition == gridPosition) { return building; } } return null; } /// /// 특정 소유자의 모든 건물 가져오기 /// public List GetBuildingsByOwner(ulong ownerId) { List ownedBuildings = new List(); foreach (var building in placedBuildings) { if (building != null && building.GetOwnerId() == ownerId) { ownedBuildings.Add(building); } } return ownedBuildings; } /// /// 건물 토대 배치 요청 (클라이언트에서 호출) /// 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("[BuildingManager] foundationPrefab이 설정되지 않았습니다!"); 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(); if (netObj != null) { // 스폰 먼저 실행 (서버 소유권으로 스폰) // 소유자 정보는 _ownerId NetworkVariable에 별도로 저장하므로 NetworkObject 소유권은 서버 유지 netObj.Spawn(); // 스폰 후에 초기화 (OnNetworkSpawn 이후에 호출되어 타이밍 문제 해결) BuildingFoundation foundation = foundationObj.GetComponent(); 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() == null) { var visibility = foundationObj.AddComponent(); visibility.showInExploredAreas = true; // Foundations remain visible in explored areas visibility.updateInterval = 0.2f; } placedFoundations.Add(foundation); // 토대 목록에 추가 } else { Destroy(foundationObj); } } /// /// 플레이어의 팀 가져오기 /// 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(); if (teamMember != null) { return teamMember.GetTeam(); } } } } // 기본값: 플레이어 팀 return TeamType.Player; } /// /// 건설 완료 시 완성된 건물 생성 (BuildingFoundation에서 호출) /// [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($"[BuildingManager] 건물 데이터를 찾을 수 없습니다: {buildingDataName}"); return; } Vector3 worldPosition = GridToWorld(gridPosition); // 완성된 건물 생성 GameObject buildingObj = Instantiate(data.prefab, worldPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0)); NetworkObject netObj = buildingObj.GetComponent(); // Add FogOfWarVisibility component to hide buildings in unexplored areas if (buildingObj.GetComponent() == null) { var visibility = buildingObj.AddComponent(); visibility.showInExploredAreas = true; // Buildings remain visible in explored areas visibility.updateInterval = 0.2f; } if (netObj != null) { netObj.SpawnWithOwnership(ownerId); Building building = buildingObj.GetComponent(); if (building == null) { building = buildingObj.AddComponent(); } // 건물 초기화 building.Initialize(data, gridPosition, rotation, ownerId); placedBuildings.Add(building); } else { Debug.LogError($"[BuildingManager] NetworkObject 컴포넌트가 없습니다!"); Destroy(buildingObj); } } #region Upgrade System /// /// ID로 TowerData 찾기 /// public TowerData GetTowerDataById(int towerId) { foreach (var data in availableBuildings) { if (data != null && data.id == towerId) { return data; } } return null; } /// /// 건물 업그레이드 (서버에서만 호출) /// public void UpgradeBuilding(Building building, TowerData upgradeData) { if (!IsServer) return; if (building == null || upgradeData == null) return; if (upgradeData.prefab == null) { Debug.LogError($"[BuildingManager] 업그레이드 프리팹이 없습니다: {upgradeData.buildingName}"); return; } // 기존 건물 정보 저장 Vector3Int gridPos = building.gridPosition; int rotation = building.rotation; ulong ownerId = building.GetOwnerId(); TeamType team = building.GetTeam(); // 기존 건물 제거 placedBuildings.Remove(building); // FogOfWar 시스템에서 제거 if (building.buildingData != null && building.buildingData.providesVision) { FogOfWarSystem.Instance?.UnregisterVisionProvider(building); } // 기존 건물 파괴 building.NetworkObject.Despawn(true); // 새 건물 생성 Vector3 worldPosition = GridToWorld(gridPos); GameObject newBuildingObj = Instantiate(upgradeData.prefab, worldPosition + upgradeData.placementOffset, Quaternion.Euler(0, rotation * 90f, 0)); NetworkObject netObj = newBuildingObj.GetComponent(); // FogOfWarVisibility 추가 if (newBuildingObj.GetComponent() == null) { var visibility = newBuildingObj.AddComponent(); visibility.showInExploredAreas = true; visibility.updateInterval = 0.2f; } if (netObj != null) { netObj.SpawnWithOwnership(ownerId); Building newBuilding = newBuildingObj.GetComponent(); if (newBuilding == null) { newBuilding = newBuildingObj.AddComponent(); } // 새 건물 초기화 newBuilding.Initialize(upgradeData, gridPos, rotation, ownerId, team); placedBuildings.Add(newBuilding); Debug.Log($"[BuildingManager] 건물 업그레이드 완료: {upgradeData.buildingName}"); } else { Debug.LogError($"[BuildingManager] 업그레이드 프리팹에 NetworkObject가 없습니다!"); Destroy(newBuildingObj); } } #endregion } }