452 lines
18 KiB
C#
452 lines
18 KiB
C#
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<BuildingData> availableBuildings = new List<BuildingData>();
|
|
|
|
[Header("Foundation Settings")]
|
|
public GameObject foundationPrefab; // 토대 프리팹 (Inspector에서 할당)
|
|
|
|
private List<Building> placedBuildings = new List<Building>();
|
|
private List<BuildingFoundation> placedFoundations = new List<BuildingFoundation>();
|
|
|
|
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)
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Get the grid bounds for a building at a given position
|
|
/// Useful for preview visualization
|
|
/// </summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// 건물 배치 요청 (클라이언트에서 호출)
|
|
/// </summary>
|
|
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($"<color=red>[BuildingManager] 유효하지 않은 클라이언트 ID: {requestingClientId}</color>");
|
|
return;
|
|
}
|
|
|
|
// 보안 검증 2: 건물 인덱스 유효성 확인
|
|
if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count)
|
|
{
|
|
Debug.LogWarning($"<color=red>[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex} (클라이언트: {requestingClientId})</color>");
|
|
return;
|
|
}
|
|
|
|
BuildingData data = availableBuildings[buildingIndex];
|
|
|
|
// 보안 검증 3: 건물 데이터 유효성 확인
|
|
if (data == null || data.prefab == null)
|
|
{
|
|
Debug.LogWarning($"<color=red>[BuildingManager] 건물 데이터가 유효하지 않습니다. (클라이언트: {requestingClientId})</color>");
|
|
return;
|
|
}
|
|
|
|
// 배치 가능 여부 확인
|
|
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
|
|
{
|
|
Debug.LogWarning($"<color=yellow>[BuildingManager] 건물 배치 불가능 위치 (클라이언트: {requestingClientId})</color>");
|
|
return;
|
|
}
|
|
|
|
Vector3Int gridPosition = WorldToGrid(snappedPosition);
|
|
|
|
// 건물 생성
|
|
GameObject buildingObj = Instantiate(data.prefab, snappedPosition + data.placementOffset, Quaternion.Euler(0, rotation * 90f, 0));
|
|
NetworkObject netObj = buildingObj.GetComponent<NetworkObject>();
|
|
|
|
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);
|
|
|
|
Debug.Log($"<color=green>[BuildingManager] {data.buildingName} 건설 완료 (소유자: {requestingClientId}, 위치: {gridPosition})</color>");
|
|
}
|
|
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);
|
|
Debug.Log($"<color=yellow>[BuildingManager] 건물 제거됨: {building.buildingData?.buildingName ?? "Unknown"}</color>");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 토대 제거 (건설 완료 또는 취소 시)
|
|
/// </summary>
|
|
public void RemoveFoundation(BuildingFoundation foundation)
|
|
{
|
|
if (placedFoundations.Contains(foundation))
|
|
{
|
|
placedFoundations.Remove(foundation);
|
|
Debug.Log($"<color=yellow>[BuildingManager] 토대 제거됨: {foundation.buildingData?.buildingName ?? "Unknown"}</color>");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 사전 배치 건물 등록 (씬에 미리 있는 건물용)
|
|
/// </summary>
|
|
public void RegisterPrePlacedBuilding(Building building)
|
|
{
|
|
if (!placedBuildings.Contains(building))
|
|
{
|
|
placedBuildings.Add(building);
|
|
Debug.Log($"<color=cyan>[BuildingManager] 사전 배치 건물 등록: {building.buildingData?.buildingName ?? building.gameObject.name}</color>");
|
|
}
|
|
}
|
|
|
|
/// <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(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($"<color=red>[BuildingManager] 유효하지 않은 클라이언트 ID: {requestingClientId}</color>");
|
|
return;
|
|
}
|
|
|
|
// 보안 검증 2: 건물 인덱스 유효성 확인
|
|
if (buildingIndex < 0 || buildingIndex >= availableBuildings.Count)
|
|
{
|
|
Debug.LogWarning($"<color=red>[BuildingManager] 유효하지 않은 건물 인덱스: {buildingIndex}</color>");
|
|
return;
|
|
}
|
|
|
|
BuildingData data = availableBuildings[buildingIndex];
|
|
|
|
// 보안 검증 3: 건물 데이터 유효성 확인
|
|
if (data == null)
|
|
{
|
|
Debug.LogWarning($"<color=red>[BuildingManager] 건물 데이터가 유효하지 않습니다.</color>");
|
|
return;
|
|
}
|
|
|
|
// 토대 프리팹 확인
|
|
if (foundationPrefab == null)
|
|
{
|
|
Debug.LogError("<color=red>[BuildingManager] foundationPrefab이 설정되지 않았습니다!</color>");
|
|
return;
|
|
}
|
|
|
|
// 배치 가능 여부 확인
|
|
if (!IsValidPlacement(data, position, rotation, out Vector3 snappedPosition))
|
|
{
|
|
Debug.LogWarning($"<color=yellow>[BuildingManager] 토대 배치 불가능 위치</color>");
|
|
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)
|
|
{
|
|
netObj.SpawnWithOwnership(requestingClientId);
|
|
|
|
BuildingFoundation foundation = foundationObj.GetComponent<BuildingFoundation>();
|
|
if (foundation != null)
|
|
{
|
|
foundation.Initialize(data, gridPosition, rotation, requestingClientId, playerTeam);
|
|
placedFoundations.Add(foundation); // 토대 목록에 추가
|
|
Debug.Log($"<color=yellow>[BuildingManager] {data.buildingName} 토대 생성 (소유자: {requestingClientId}, 위치: {gridPosition})</color>");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("<color=red>[BuildingManager] BuildingFoundation 컴포넌트가 없습니다!</color>");
|
|
netObj.Despawn(true);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError("<color=red>[BuildingManager] NetworkObject 컴포넌트가 없습니다!</color>");
|
|
Destroy(foundationObj);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 플레이어의 팀 가져오기
|
|
/// </summary>
|
|
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<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)
|
|
{
|
|
// BuildingData 찾기
|
|
BuildingData 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>();
|
|
|
|
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);
|
|
|
|
Debug.Log($"<color=green>[BuildingManager] {data.buildingName} 건설 완료! (소유자: {ownerId}, 위치: {gridPosition}, 팀: {team})</color>");
|
|
}
|
|
else
|
|
{
|
|
Debug.LogError($"<color=red>[BuildingManager] NetworkObject 컴포넌트가 없습니다!</color>");
|
|
Destroy(buildingObj);
|
|
}
|
|
}
|
|
}
|
|
}
|