using UnityEngine; using Unity.Netcode; using UnityEngine.InputSystem; using UnityEngine.EventSystems; using System.Collections.Generic; public class BuildManager : NetworkBehaviour { public static BuildManager Instance; [System.Serializable] public struct TurretData { public string turretName; public GameObject finalPrefab; // NetworkObject 필수 public GameObject ghostPrefab; // 로컬 프리뷰용 public float buildTime; } [Header("Grid Settings")] public float cellSize = 1f; public float tunnelHeight = 3f; [SerializeField] private float yOffset = 0.5f; // 터널 중심점 오프셋 [Header("Prefabs & Layers")] [SerializeField] private GameObject constructionSitePrefab; [SerializeField] private List turretLibrary = new List(); [SerializeField] private LayerMask groundLayer; [SerializeField] private LayerMask playerLayer; private int _selectedTurretIndex = 0; private bool _isBuildMode = false; private GameObject _ghostInstance; private Vector3Int _currentGridPos; private PlayerInputActions _inputActions; // 데이터 레지스트리 (다른 코드에서 참조) 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() { // 최신 Action-based 이벤트 바인딩 _inputActions.Player.ToggleBuild.performed += ctx => ToggleBuildMode(); _inputActions.Player.Build.performed += ctx => OnBuildRequested(); _inputActions.Player.Cancel.performed += ctx => ExitBuildMode(); // 숫자키 슬롯 선택 (Input Action 에셋 설정에 따라 Select1, Select2... 등 사용) _inputActions.Player.Select1.performed += ctx => SelectTurret(0); _inputActions.Player.Select2.performed += ctx => SelectTurret(1); _inputActions.Player.Select3.performed += ctx => SelectTurret(2); _inputActions.Enable(); // 시작 시 씬에 배치된 터널 자동 등록 RegisterAllExistingTunnels(); } void Update() { if (!_isBuildMode || _ghostInstance == null) return; UpdateGhostPosition(); } #region Selection & Build Logic public void SelectTurret(int index) { if (index < 0 || index >= turretLibrary.Count) return; _selectedTurretIndex = index; Debug.Log($"타워 선택: {turretLibrary[_selectedTurretIndex].turretName}"); if (_isBuildMode) { if (_ghostInstance != null) Destroy(_ghostInstance); _ghostInstance = Instantiate(turretLibrary[_selectedTurretIndex].ghostPrefab); _ghostInstance.transform.position = GridToWorld(_currentGridPos); } } private void UpdateGhostPosition() { Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); int tunnelMask = LayerMask.GetMask("Tunnel"); if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, groundLayer | tunnelMask)) { Vector3Int gridPos = WorldToGrid3D(hit.point); // 터널 조준 시 해당 터널의 한 칸 아래로 스냅 if (((1 << hit.collider.gameObject.layer) & tunnelMask) != 0) { gridPos = WorldToGrid3D(hit.collider.transform.position) + Vector3Int.down; } _currentGridPos = gridPos; _ghostInstance.transform.position = GridToWorld(gridPos); } } private void OnBuildRequested() { if (!_isBuildMode || EventSystem.current.IsPointerOverGameObject()) return; // 서버에 건설 가능 여부 확인 및 생성 요청 RequestBuildRpc(_selectedTurretIndex, _currentGridPos); ExitBuildMode(); } [Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)] private void RequestBuildRpc(int index, Vector3Int gridPos) { // 서버 측 중복 점유 확인 if (_occupiedNodes.Contains(gridPos)) return; Vector3 spawnPos = GridToWorld(gridPos); // 1. 토대 생성 및 네트워크 스폰 GameObject siteObj = Instantiate(constructionSitePrefab, spawnPos, Quaternion.identity); siteObj.GetComponent().Spawn(); // 2. 토대 데이터 초기화 ConstructionSite site = siteObj.GetComponent(); if (site != null) { site.Initialize(index, gridPos); } _occupiedNodes.Add(gridPos); } #endregion #region Utility Functions (External References) // ConstructionSite에서 프리팹 정보를 가져갈 때 사용 public TurretData GetTurretData(int index) => turretLibrary[index]; // TunnelNode가 스폰될 때 자신을 등록하기 위해 사용 public void RegisterTunnel(Vector3Int pos, TunnelNode node) { if (!_tunnelRegistry.ContainsKey(pos)) { _tunnelRegistry.Add(pos, node); _occupiedNodes.Add(pos); // 건설된 구역으로 마킹 } } // TunnelNode가 위아래 노드를 찾기 위해 사용 public TunnelNode GetTunnelAt(Vector3Int pos) => _tunnelRegistry.GetValueOrDefault(pos); // 좌표 변환: 월드 -> 격자 인덱스 (0.5 오프셋 반영) public Vector3Int WorldToGrid3D(Vector3 worldPos) { return new Vector3Int( Mathf.FloorToInt(worldPos.x / cellSize), Mathf.RoundToInt((worldPos.y - yOffset) / tunnelHeight), Mathf.FloorToInt(worldPos.z / cellSize) ); } // 좌표 변환: 격자 인덱스 -> 월드 (0.5 오프셋 반영) public Vector3 GridToWorld(Vector3Int gridPos) { return new Vector3( gridPos.x * cellSize, (gridPos.y * tunnelHeight) + yOffset, gridPos.z * cellSize ); } // 씬에 미리 배치된 터널들을 한꺼번에 등록 public void RegisterAllExistingTunnels() { TunnelNode[] nodes = FindObjectsByType(FindObjectsSortMode.None); foreach (var node in nodes) { Vector3Int pos = WorldToGrid3D(node.transform.position); RegisterTunnel(pos, node); } } #endregion #region Mode Switching public void ToggleBuildMode() { if (_isBuildMode) ExitBuildMode(); else EnterBuildMode(); } private void EnterBuildMode() { if (turretLibrary.Count == 0) return; _isBuildMode = true; _ghostInstance = Instantiate(turretLibrary[_selectedTurretIndex].ghostPrefab); } private void ExitBuildMode() { _isBuildMode = false; if (_ghostInstance) Destroy(_ghostInstance); } #endregion public override void OnNetworkDespawn() => _inputActions.Disable(); }