using System.Collections.Generic; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.EventSystems; using Northbound.Data; namespace Northbound { public class BuildingPlacement : NetworkBehaviour { [Header("Settings")] public LayerMask groundLayer; public float maxPlacementDistance = 100f; [Header("Preview Materials")] public Material validMaterial; public Material invalidMaterial; [Header("Current Selection")] public int selectedBuildingIndex = 0; [Header("Debug Visualization")] public bool showGridBounds = true; [Header("Drag Building")] [Tooltip("드래그 건설 활성화")] public bool enableDragBuilding = true; [Tooltip("드래그 건설 시 최대 건물 수")] public int maxDragBuildingCount = 50; private bool isBuildModeActive = false; private GameObject previewObject; private int currentRotation = 0; // 0-3 private Material[] originalMaterials; private Renderer[] previewRenderers; private PlayerInputActions _inputActions; // 드래그 건설 관련 private bool isDragging = false; private Vector3 dragStartPosition; private List dragPreviewObjects = new List(); private List dragBuildingPositions = new List(); public override void OnNetworkSpawn() { if (!IsOwner) return; _inputActions = new PlayerInputActions(); _inputActions.Player.ToggleBuildMode.performed += OnToggleBuildMode; _inputActions.Player.Rotate.performed += OnRotate; _inputActions.Player.Build.performed += OnBuildPressed; _inputActions.Player.Build.canceled += OnBuildReleased; _inputActions.Player.Cancel.performed += OnCancel; _inputActions.Enable(); // Create default materials if not assigned if (validMaterial == null) { validMaterial = CreateGhostMaterial(new Color(0, 1, 0, 0.5f)); } if (invalidMaterial == null) { invalidMaterial = CreateGhostMaterial(new Color(1, 0, 0, 0.5f)); } } private Material CreateGhostMaterial(Color color) { // Try to find appropriate shader for current render pipeline Shader shader = Shader.Find("Universal Render Pipeline/Lit"); if (shader == null) shader = Shader.Find("Standard"); if (shader == null) shader = Shader.Find("Diffuse"); Material mat = new Material(shader); // Set base color if (mat.HasProperty("_BaseColor")) mat.SetColor("_BaseColor", color); // URP else if (mat.HasProperty("_Color")) mat.SetColor("_Color", color); // Standard // Enable transparency if (mat.HasProperty("_Surface")) { // URP Transparency mat.SetFloat("_Surface", 1); // Transparent mat.SetFloat("_Blend", 0); // Alpha mat.SetFloat("_AlphaClip", 0); mat.SetFloat("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetFloat("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetFloat("_ZWrite", 0); mat.renderQueue = 3000; mat.EnableKeyword("_SURFACE_TYPE_TRANSPARENT"); mat.EnableKeyword("_ALPHAPREMULTIPLY_ON"); } else { // Standard RP Transparency mat.SetFloat("_Mode", 3); // Transparent mode mat.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); mat.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); mat.SetInt("_ZWrite", 0); mat.DisableKeyword("_ALPHATEST_ON"); mat.EnableKeyword("_ALPHABLEND_ON"); mat.DisableKeyword("_ALPHAPREMULTIPLY_ON"); mat.renderQueue = 3000; } return mat; } public override void OnNetworkDespawn() { if (IsOwner && _inputActions != null) { _inputActions.Player.ToggleBuildMode.performed -= OnToggleBuildMode; _inputActions.Player.Rotate.performed -= OnRotate; _inputActions.Player.Build.performed -= OnBuildPressed; _inputActions.Player.Build.canceled -= OnBuildReleased; _inputActions.Player.Cancel.performed -= OnCancel; _inputActions.Disable(); _inputActions.Dispose(); } ClearDragPreviews(); } private void Update() { if (!IsOwner) return; if (isBuildModeActive) { if (isDragging && enableDragBuilding) { UpdateDragPreview(); } else { UpdatePreviewPosition(); } } } private void OnCancel(InputAction.CallbackContext context) { // 드래그 중일 때 드래그 취소 if (isDragging && isBuildModeActive) { CancelDrag(); Debug.Log("[BuildingPlacement] 드래그 건설 취소됨"); } // 건설 모드 활성화 상태면 건설 모드 종료 else if (isBuildModeActive) { isBuildModeActive = false; if (BuildingQuickslotUI.Instance != null) { BuildingQuickslotUI.Instance.HideQuickslot(); } DestroyPreview(); ClearDragPreviews(); Debug.Log("[BuildingPlacement] 건설 모드 취소됨"); } } private void OnToggleBuildMode(InputAction.CallbackContext context) { isBuildModeActive = !isBuildModeActive; if (isBuildModeActive) { // UI 표시 if (BuildingQuickslotUI.Instance != null) { BuildingQuickslotUI.Instance.ShowQuickslot(this); } CreatePreview(); } else { // UI 숨김 if (BuildingQuickslotUI.Instance != null) { BuildingQuickslotUI.Instance.HideQuickslot(); } // 드래그 중이었다면 취소 if (isDragging) { CancelDrag(); Debug.Log("[BuildingPlacement] 건설 모드 종료로 드래그 취소됨"); } DestroyPreview(); ClearDragPreviews(); } } /// /// UI에서 건물 선택 시 호출 /// public void SetSelectedBuilding(int index) { if (index < 0 || BuildingManager.Instance == null || index >= BuildingManager.Instance.GetBuildableBuildings().Count) { Debug.LogWarning($"[BuildingPlacement] 유효하지 않은 건물 인덱스: {index}"); return; } selectedBuildingIndex = index; currentRotation = 0; // 회전 초기화 // 드래그 중이었다면 취소 if (isDragging) { CancelDrag(); Debug.Log("[BuildingPlacement] 건물 변경으로 드래그 취소됨"); } // 프리뷰 다시 생성 if (isBuildModeActive) { DestroyPreview(); CreatePreview(); } } private void CreatePreview() { // 기존 프리뷰가 있다면 먼저 정리 (누적 방지) DestroyPreview(); if (BuildingManager.Instance == null) { Debug.LogWarning("[BuildingPlacement] BuildingManager가 없습니다."); return; } if (selectedBuildingIndex < 0 || selectedBuildingIndex >= BuildingManager.Instance.GetBuildableBuildings().Count) { Debug.LogWarning($"[BuildingPlacement] 유효하지 않은 건물 인덱스: {selectedBuildingIndex}"); return; } TowerData data = BuildingManager.Instance.GetBuildableBuildings()[selectedBuildingIndex]; if (data == null) { Debug.LogError($"[BuildingPlacement] TowerData is NULL at index {selectedBuildingIndex}"); return; } if (data.prefab == null) { Debug.LogError($"[BuildingPlacement] TowerData.prefab is NULL at index {selectedBuildingIndex}. Run 'Northbound > Diagnose Tower System'"); return; } // 완성 건물 프리팹으로 프리뷰 생성 (사용자가 완성 모습을 볼 수 있도록) previewObject = Instantiate(data.prefab); // (0,0,0)에 고스트가 보이지 않도록 처음에는 비활성화 previewObject.SetActive(false); // Remove NetworkObject component from preview NetworkObject netObj = previewObject.GetComponent(); if (netObj != null) { Destroy(netObj); } // Remove Building component from preview Building building = previewObject.GetComponent(); if (building != null) { Destroy(building); } // Apply ghost materials previewRenderers = previewObject.GetComponentsInChildren(); foreach (var renderer in previewRenderers) { Material[] mats = new Material[renderer.materials.Length]; for (int i = 0; i < mats.Length; i++) { mats[i] = validMaterial; } renderer.materials = mats; } // Disable colliders in preview foreach (var collider in previewObject.GetComponentsInChildren()) { collider.enabled = false; } // Disable NavMeshObstacles in preview (they act as obstacles for NavMeshAgent) foreach (var obstacle in previewObject.GetComponentsInChildren()) { obstacle.enabled = false; } } private void DestroyPreview() { if (previewObject != null) { Destroy(previewObject); previewObject = null; } } private void UpdatePreviewPosition() { if (previewObject == null || BuildingManager.Instance == null) return; if (selectedBuildingIndex < 0 || selectedBuildingIndex >= BuildingManager.Instance.GetBuildableBuildings().Count) return; TowerData data = BuildingManager.Instance.GetBuildableBuildings()[selectedBuildingIndex]; if (data == null || data.prefab == null) return; Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); if (Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer)) { // Raycast 성공 시 프리뷰 표시 previewObject.SetActive(true); // Check if placement is valid bool isValid = BuildingManager.Instance.IsValidPlacement(data, hit.point, currentRotation, out Vector3 snappedPosition); // Check affordability var coreResourceManager = CoreResourceManager.Instance; bool canAfford = coreResourceManager != null && coreResourceManager.CanAfford(data.mana); // Update preview position (placementOffset 적용) previewObject.transform.position = snappedPosition + data.placementOffset; previewObject.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0); // Update material based on validity and affordability Material targetMat = (isValid && canAfford) ? validMaterial : invalidMaterial; foreach (var renderer in previewRenderers) { Material[] mats = new Material[renderer.materials.Length]; for (int i = 0; i < mats.Length; i++) { mats[i] = targetMat; } renderer.materials = mats; } } else { // Raycast 실패 시 (0,0,0)에 고스트가 보이지 않도록 프리뷰 숨김 previewObject.SetActive(false); } } private void OnRotate(InputAction.CallbackContext context) { if (!isBuildModeActive) return; // 드래그 중에는 회전 불가 if (isDragging) { Debug.Log("[BuildingPlacement] 드래그 중에는 회전할 수 없습니다"); return; } currentRotation = (currentRotation + 1) % 4; } private void OnBuildPressed(InputAction.CallbackContext context) { if (!isBuildModeActive || IsPointerOverUI()) { return; } if (enableDragBuilding) { // 드래그 시작 Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); if (Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer)) { isDragging = true; dragStartPosition = hit.point; // 메인 프리뷰 숨기기 if (previewObject != null) { previewObject.SetActive(false); } Debug.Log("[BuildingPlacement] 드래그 건설 시작 (ESC로 취소 가능)"); } } } private void OnBuildReleased(InputAction.CallbackContext context) { if (!isBuildModeActive) return; if (enableDragBuilding && isDragging) { // 드래그 종료 - 배치 실행 ExecuteDragBuild(); isDragging = false; // 메인 프리뷰 다시 표시 if (previewObject != null) { previewObject.SetActive(true); } ClearDragPreviews(); } else if (!enableDragBuilding) { // 단일 건물 배치 (드래그 비활성화 시) OnBuild(); } } /// /// 드래그 취소 /// private void CancelDrag() { if (!isDragging) return; isDragging = false; // 메인 프리뷰 다시 표시 if (previewObject != null) { previewObject.SetActive(true); } // 드래그 프리뷰 정리 ClearDragPreviews(); } private void UpdateDragPreview() { if (BuildingManager.Instance == null) return; if (selectedBuildingIndex < 0 || selectedBuildingIndex >= BuildingManager.Instance.GetBuildableBuildings().Count) { Debug.LogWarning($"[BuildingPlacement] Invalid building index: {selectedBuildingIndex}"); return; } TowerData data = BuildingManager.Instance.GetBuildableBuildings()[selectedBuildingIndex]; if (data == null || data.prefab == null) { Debug.LogError($"[BuildingPlacement] TowerData or prefab is null at index {selectedBuildingIndex}. Please run 'Northbound > Populate Towers from Prefabs' and update BuildingManager."); return; } Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); if (!Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer)) { return; } // 드래그 영역 계산 Vector3 dragEndPosition = hit.point; List positions = CalculateDragBuildingPositions(dragStartPosition, dragEndPosition, data); // Check affordability for all buildings var coreResourceManager = CoreResourceManager.Instance; bool canAffordAll = coreResourceManager != null && coreResourceManager.CanAfford(data.mana * positions.Count); // 기존 프리뷰 정리 ClearDragPreviews(); // 새로운 프리뷰 생성 dragBuildingPositions.Clear(); foreach (var pos in positions) { if (dragPreviewObjects.Count >= maxDragBuildingCount) { Debug.LogWarning($"[BuildingPlacement] 드래그 건설 최대 개수 도달: {maxDragBuildingCount}"); break; } bool isValid = BuildingManager.Instance.IsValidPlacement(data, pos, currentRotation, out Vector3 snappedPosition); GameObject preview = Instantiate(data.prefab); // Remove NetworkObject and Building components if (preview.GetComponent() != null) Destroy(preview.GetComponent()); if (preview.GetComponent() != null) Destroy(preview.GetComponent()); // Set position and rotation preview.transform.position = snappedPosition + data.placementOffset; preview.transform.rotation = Quaternion.Euler(0, currentRotation * 90f, 0); // Apply materials Material targetMat = (isValid && canAffordAll) ? validMaterial : invalidMaterial; Renderer[] renderers = preview.GetComponentsInChildren(); foreach (var renderer in renderers) { Material[] mats = new Material[renderer.materials.Length]; for (int i = 0; i < mats.Length; i++) { mats[i] = targetMat; } renderer.materials = mats; } // Disable colliders foreach (var collider in preview.GetComponentsInChildren()) { collider.enabled = false; } // Disable NavMeshObstacles in preview (they act as obstacles for NavMeshAgent) foreach (var obstacle in preview.GetComponentsInChildren()) { obstacle.enabled = false; } dragPreviewObjects.Add(preview); if (isValid) { dragBuildingPositions.Add(snappedPosition); } } } private List CalculateDragBuildingPositions(Vector3 start, Vector3 end, TowerData data) { List positions = new List(); // 그리드에 스냅 Vector3 snappedStart = BuildingManager.Instance.SnapToGrid(start); Vector3 snappedEnd = BuildingManager.Instance.SnapToGrid(end); // 건물 크기 고려 Vector3 size = data.GetSize(currentRotation); float gridSize = BuildingManager.Instance.gridSize; float stepX = Mathf.Max(size.x, gridSize); float stepZ = Mathf.Max(size.z, gridSize); // 드래그 방향 계산 Vector3 direction = snappedEnd - snappedStart; float distanceX = Mathf.Abs(direction.x); float distanceZ = Mathf.Abs(direction.z); int countX = Mathf.Max(1, Mathf.RoundToInt(distanceX / stepX) + 1); int countZ = Mathf.Max(1, Mathf.RoundToInt(distanceZ / stepZ) + 1); float dirX = direction.x >= 0 ? 1 : -1; float dirZ = direction.z >= 0 ? 1 : -1; // 그리드 패턴으로 위치 생성 for (int x = 0; x < countX; x++) { for (int z = 0; z < countZ; z++) { Vector3 pos = snappedStart + new Vector3( x * stepX * dirX, 0, z * stepZ * dirZ ); positions.Add(pos); } } return positions; } private void ExecuteDragBuild() { if (dragBuildingPositions.Count == 0) { Debug.Log("[BuildingPlacement] 배치 가능한 건물이 없습니다."); return; } TowerData selectedData = BuildingManager.Instance.GetBuildableBuildings()[selectedBuildingIndex]; // Check affordability var coreResourceManager = CoreResourceManager.Instance; if (coreResourceManager == null || !coreResourceManager.CanAfford(selectedData.mana * dragBuildingPositions.Count)) { Debug.Log("[BuildingPlacement] 자원이 부족합니다."); return; } int successCount = 0; foreach (var position in dragBuildingPositions) { BuildingManager.Instance.RequestPlaceFoundation(selectedBuildingIndex, position, currentRotation); successCount++; } Debug.Log($"[BuildingPlacement] 드래그 건설 완료: {successCount}개의 {selectedData.buildingName} 배치 시도"); } private void ClearDragPreviews() { foreach (var preview in dragPreviewObjects) { if (preview != null) { Destroy(preview); } } dragPreviewObjects.Clear(); dragBuildingPositions.Clear(); } private void OnBuild() { if (!isBuildModeActive || previewObject == null) return; // UI 위에서 클릭한 경우 무시 if (IsPointerOverUI()) { Debug.Log("[BuildingPlacement] UI 위에서 클릭함 - 건설 취소"); return; } // Get placement position Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue()); if (Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer)) { TowerData selectedData = BuildingManager.Instance.GetBuildableBuildings()[selectedBuildingIndex]; // Check affordability var coreResourceManager = CoreResourceManager.Instance; if (coreResourceManager != null && !coreResourceManager.CanAfford(selectedData.mana)) { Debug.Log("[BuildingPlacement] 자원이 부족합니다."); return; } if (BuildingManager.Instance.IsValidPlacement(selectedData, hit.point, currentRotation, out Vector3 groundPosition)) { // 토대 배치 요청 BuildingManager.Instance.RequestPlaceFoundation(selectedBuildingIndex, groundPosition, currentRotation); Debug.Log($"[BuildingPlacement] 건설 시작: {selectedData.buildingName}"); } else { Debug.Log("[BuildingPlacement] 배치할 수 없는 위치입니다."); } } } /// /// 마우스 포인터가 UI 위에 있는지 확인 /// private bool IsPointerOverUI() { // EventSystem이 없으면 UI 체크 불가능 if (EventSystem.current == null) return false; // New Input System을 사용하는 경우 if (Mouse.current != null) { Vector2 mousePosition = Mouse.current.position.ReadValue(); return EventSystem.current.IsPointerOverGameObject(); } // Legacy Input System (폴백) return EventSystem.current.IsPointerOverGameObject(); } /// /// 건설 모드 활성화 상태 확인 /// public bool IsBuildModeActive() { return isBuildModeActive; } } }