using System.Collections.Generic; using Unity.Netcode; using UnityEngine; 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(); 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 ); } 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(BuildingData 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 BuildingData (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) { Debug.Log($"[BuildingManager] 물리적 충돌 감지: {colliders.Length}개의 오브젝트와 겹침"); foreach (var col in colliders) { Debug.Log($" - {col.gameObject.name} (Layer: {LayerMask.LayerToName(col.gameObject.layer)})"); } return false; } return true; } /// /// Get the grid bounds for a building at a given position /// Useful for preview visualization /// public Bounds GetPlacementBounds(BuildingData 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(int buildingIndex, Vector3 position, int rotation) { if (!NetworkManager.Singleton.IsClient) { Debug.LogWarning("[BuildingManager] 클라이언트가 아닙니다."); return; } ulong clientId = NetworkManager.Singleton.LocalClientId; PlaceBuildingServerRpc(buildingIndex, position, rotation, clientId); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void PlaceBuildingServerRpc(int buildingIndex, Vector3 position, int rotation, ulong requestingClientId) { // 보안 검증 1: 유효한 클라이언트인지 확인 if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(requestingClientId)) { Debug.LogWarning($"[BuildingManager] 유효하지 않은 클라이언트 ID: {requestingClientId}"); return; } // 보안 검증 2: 건물 인덱스 유효성 확인 if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count) { Debug.LogWarning($"[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex} (클라이언트: {requestingClientId})"); return; } BuildingData data = availableBuildings[buildingIndex]; // 보안 검증 3: 건물 데이터 유효성 확인 if (data == null || data.prefab == null) { Debug.LogWarning($"[BuildingManager] 건물 데이터가 유효하지 않습니다. (클라이언트: {requestingClientId})"); return; } // 배치 가능 여부 확인 if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition)) { Debug.LogWarning($"[BuildingManager] 건물 배치 불가능 위치 (클라이언트: {requestingClientId})"); return; } Vector3Int gridPosition = WorldToGrid(snappedPosition); // 건물 생성 GameObject buildingObj = Instantiate(data.prefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0)); NetworkObject netObj = buildingObj.GetComponent(); 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); Debug.Log($"[BuildingManager] {data.buildingName} 건설 완료 (소유자: {requestingClientId}, 위치: {gridPosition})"); } else { Debug.LogError($"[BuildingManager] NetworkObject 컴포넌트가 없습니다! (Prefab: {data.prefab.name})"); Destroy(buildingObj); } } public void RemoveBuilding(Building building) { if (placedBuildings.Contains(building)) { placedBuildings.Remove(building); Debug.Log($"[BuildingManager] 건물 제거됨: {building.buildingData?.buildingName ?? "Unknown"}"); } } /// /// 토대 제거 (건설 완료 또는 취소 시) /// public void RemoveFoundation(BuildingFoundation foundation) { if (placedFoundations.Contains(foundation)) { placedFoundations.Remove(foundation); Debug.Log($"[BuildingManager] 토대 제거됨: {foundation.buildingData?.buildingName ?? "Unknown"}"); } } /// /// 사전 배치 건물 등록 (씬에 미리 있는 건물용) /// public void RegisterPrePlacedBuilding(Building building) { if (!placedBuildings.Contains(building)) { placedBuildings.Add(building); Debug.Log($"[BuildingManager] 사전 배치 건물 등록: {building.buildingData?.buildingName ?? building.gameObject.name}"); } } /// /// 특정 위치에 건물이 있는지 확인 /// 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(int buildingIndex, Vector3 position, int rotation) { if (!NetworkManager.Singleton.IsClient) { Debug.LogWarning("[BuildingManager] 클라이언트가 아닙니다."); return; } ulong clientId = NetworkManager.Singleton.LocalClientId; PlaceFoundationServerRpc(buildingIndex, position, rotation, clientId); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void PlaceFoundationServerRpc(int buildingIndex, Vector3 position, int rotation, ulong requestingClientId) { // 보안 검증 1: 유효한 클라이언트인지 확인 if (!NetworkManager.Singleton.ConnectedClients.ContainsKey(requestingClientId)) { Debug.LogWarning($"[BuildingManager] 유효하지 않은 클라이언트 ID: {requestingClientId}"); return; } // 보안 검증 2: 건물 인덱스 유효성 확인 if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count) { Debug.LogWarning($"[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex}"); return; } BuildingData data = availableBuildings[buildingIndex]; // 보안 검증 3: 건물 데이터 유효성 확인 if (data == null) { Debug.LogWarning($"[BuildingManager] 건물 데이터가 유효하지 않습니다."); return; } // 토대 프리팹 확인 if (foundationPrefab == null) { Debug.LogError("[BuildingManager] foundationPrefab이 설정되지 않았습니다!"); return; } // 배치 가능 여부 확인 if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition)) { Debug.LogWarning($"[BuildingManager] 토대 배치 불가능 위치"); 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) { netObj.SpawnWithOwnership(requestingClientId); BuildingFoundation foundation = foundationObj.GetComponent(); if (foundation != null) { foundation.Initialize(data, gridPosition, rotation, requestingClientId, playerTeam); placedFoundations.Add(foundation); // 토대 목록에 추가 Debug.Log($"[BuildingManager] {data.buildingName} 토대 생성 (소유자: {requestingClientId}, 위치: {gridPosition})"); } else { Debug.LogError("[BuildingManager] BuildingFoundation 컴포넌트가 없습니다!"); netObj.Despawn(true); } } else { Debug.LogError("[BuildingManager] NetworkObject 컴포넌트가 없습니다!"); Destroy(foundationObj); } } /// /// 플레이어의 팀 가져오기 /// private TeamType GetPlayerTeam(ulong playerId) { // 플레이어의 NetworkObject 찾기 if (NetworkManager.Singleton != null && NetworkManager.Singleton.SpawnManager != null) { if (NetworkManager.Singleton.SpawnManager.SpawnedObjects.TryGetValue(playerId, out NetworkObject playerNetObj)) { var teamMember = playerNetObj.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) { // BuildingData 찾기 BuildingData 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(); 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); Debug.Log($"[BuildingManager] {data.buildingName} 건설 완료! (소유자: {ownerId}, 위치: {gridPosition}, 팀: {team})"); } else { Debug.LogError($"[BuildingManager] NetworkObject 컴포넌트가 없습니다!"); Destroy(buildingObj); } } } }