using UnityEngine; using Unity.Netcode; using UnityEngine.InputSystem; using UnityEngine.EventSystems; using UnityEngine.UI; using System.Collections.Generic; public class BuildManager : NetworkBehaviour { public static BuildManager Instance; [System.Serializable] public struct TurretData { public string turretName; public GameObject finalPrefab; public GameObject ghostPrefab; public float buildTime; } [Header("Uniform Grid Settings")] public float cellSize = 1f; [SerializeField] private float pivotOffset = 0.5f; // 블록/터널 에셋의 중심점 [Header("Tunnel Settings")] public int tunnelLengthInBlocks = 3; [Header("Prefabs & Layers")] [SerializeField] private GameObject constructionSitePrefab; [SerializeField] private List turretLibrary = new List(); [SerializeField] private LayerMask groundLayer; [SerializeField] private LayerMask tunnelLayer; private int _selectedTurretIndex = 0; private bool _isBuildMode = false; private GameObject _ghostInstance; private Vector3Int _currentGridPos; private PlayerInputActions _inputActions; // Public property to check if currently in build mode public bool IsBuildMode => _isBuildMode; private Dictionary _tunnelRegistry = new Dictionary(); private HashSet _occupiedNodes = new HashSet(); void Awake() { if (Instance == null) Instance = this; else Destroy(gameObject); _inputActions = new PlayerInputActions(); } public override void OnNetworkSpawn() { _inputActions.Player.ToggleBuild.performed += ctx => ToggleBuildMode(); _inputActions.Player.Build.performed += ctx => OnBuildRequested(); _inputActions.Player.Cancel.performed += ctx => ExitBuildMode(); _inputActions.Player.Select1.performed += ctx => SelectTurret(0); _inputActions.Player.Select2.performed += ctx => SelectTurret(1); _inputActions.Player.Select3.performed += ctx => SelectTurret(2); _inputActions.Enable(); } void Update() { if (!_isBuildMode || _ghostInstance == null) return; UpdateGhostPosition(); } private void UpdateGhostPosition() { Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, groundLayer | tunnelLayer)) { Vector3 targetPos; // 터널 조준 시: 복잡한 계산 없이 X, Z는 유지, Y만 3 낮춤 if (((1 << hit.collider.gameObject.layer) & tunnelLayer) != 0) { Vector3 parentPos = hit.collider.transform.position; targetPos = new Vector3(parentPos.x, parentPos.y - (tunnelLengthInBlocks * cellSize), parentPos.z); } else { // 지형 조준 시: 격자에 맞춤 _currentGridPos = WorldToGrid3D(hit.point + hit.normal * 0.01f); targetPos = GridToWorld(_currentGridPos); } _currentGridPos = WorldToGrid3D(targetPos); _ghostInstance.transform.position = targetPos; } } // 모든 좌표를 반올림하여 0.5 단위 오차를 제거 public Vector3Int WorldToGrid3D(Vector3 worldPos) { return new Vector3Int( Mathf.RoundToInt((worldPos.x - pivotOffset) / cellSize), Mathf.RoundToInt((worldPos.y - pivotOffset) / cellSize), Mathf.RoundToInt((worldPos.z - pivotOffset) / cellSize) ); } public Vector3 GridToWorld(Vector3Int gridPos) { return new Vector3( (gridPos.x * cellSize) + pivotOffset, (gridPos.y * cellSize) + pivotOffset, (gridPos.z * cellSize) + pivotOffset ); } // [에러 해결] ConstructionSite.cs 참조용 public TurretData GetTurretData(int index) { if (index < 0 || index >= turretLibrary.Count) return default; return turretLibrary[index]; } // BuildManager.cs 내 핵심 수정 부분 // [수정] 서버와 클라이언트 모두에서 터널 위치를 기록하도록 함 // BuildManager.cs 내부 public void RegisterTunnel(Vector3Int topPos, TunnelNode node) { // 터널이 점유하는 Y, Y-1, Y-2 세 칸 모두에 노드를 등록합니다. for (int i = 0; i < 3; i++) { Vector3Int cellPos = topPos + (Vector3Int.down * i); if (!_tunnelRegistry.ContainsKey(cellPos)) { _tunnelRegistry.Add(cellPos, node); } } } // [수정] 특정 좌표의 터널을 찾는 공용 함수 public TunnelNode GetTunnelAt(Vector3Int pos) { return _tunnelRegistry.GetValueOrDefault(pos); } // Helper method to properly check if pointer is over UI with New Input System private bool IsPointerOverUI() { if (EventSystem.current == null) return false; // Use the new input system's pointer position Vector2 pointerPosition = Mouse.current.position.ReadValue(); PointerEventData eventData = new PointerEventData(EventSystem.current) { position = pointerPosition }; List results = new List(); EventSystem.current.RaycastAll(eventData, results); // Filter out non-interactive UI elements (crosshair, HUD, etc.) foreach (var result in results) { GameObject uiObject = result.gameObject; // Ignore non-interactive UI elements by name if (uiObject.name == "Crosshair" || uiObject.name.Contains("HUD") || uiObject.name.Contains("Display")) { continue; } // Check if the UI element is actually interactive (has a Selectable component) UnityEngine.UI.Selectable selectable = uiObject.GetComponent(); if (selectable != null && selectable.interactable) { return true; } // Also check parent objects for Selectable components (in case we hit a child element) selectable = uiObject.GetComponentInParent(); if (selectable != null && selectable.interactable) { return true; } } // No interactive UI elements found return false; } // 1. 건설 요청 시 실제 계산된 worldPos를 넘겨줍니다. private void OnBuildRequested() { if (!_isBuildMode) return; if (IsPointerOverUI()) return; // 고스트가 현재 위치한 '그 좌표'를 그대로 보냅니다. RequestBuildRpc(_selectedTurretIndex, _currentGridPos, _ghostInstance.transform.position); ExitBuildMode(); } // 2. RPC에서 넘겨받은 worldPos를 사용하여 토대를 생성합니다. [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestBuildRpc(int index, Vector3Int gridPos, Vector3 worldPos) { if (constructionSitePrefab == null) { Debug.LogError("[BuildManager] Construction site prefab is null!"); return; } // GridToWorld를 다시 계산하지 않고 전달받은 worldPos를 그대로 사용합니다. GameObject siteObj = Instantiate(constructionSitePrefab, worldPos, Quaternion.identity); NetworkObject netObj = siteObj.GetComponent(); if (netObj == null) { Debug.LogError("[BuildManager] Construction site has no NetworkObject component!"); Destroy(siteObj); return; } netObj.Spawn(); ConstructionSite site = siteObj.GetComponent(); if (site != null) { site.Initialize(index, gridPos); } } public void SelectTurret(int index) { if (index < 0 || index >= turretLibrary.Count) return; _selectedTurretIndex = index; if (_isBuildMode && _ghostInstance != null) { Destroy(_ghostInstance); _ghostInstance = Instantiate(turretLibrary[_selectedTurretIndex].ghostPrefab); } } private void ToggleBuildMode() { if (_isBuildMode) ExitBuildMode(); else EnterBuildMode(); } private void EnterBuildMode() { _isBuildMode = true; if (turretLibrary.Count > 0) _ghostInstance = Instantiate(turretLibrary[_selectedTurretIndex].ghostPrefab); } private void ExitBuildMode() { _isBuildMode = false; if (_ghostInstance) Destroy(_ghostInstance); } public override void OnNetworkDespawn() => _inputActions.Disable(); }