건설 퀵슬롯 생성

This commit is contained in:
2026-01-28 00:37:56 +09:00
parent 8799c4f8f4
commit 68a2e4e340
18 changed files with 2507 additions and 167 deletions

View File

@@ -8,6 +8,8 @@ namespace Northbound
[Header("Building Info")]
public string buildingName;
public GameObject prefab;
[Tooltip("UI에 표시될 건물 아이콘")]
public Sprite icon;
[Header("Grid Size")]
[Tooltip("Width in grid units")]

View File

@@ -133,13 +133,6 @@ namespace Northbound
return false;
}
// 쿨다운 체크
if (Time.time - _lastInteractionTime < buildingData.interactionCooldown)
{
Debug.Log($"[BuildingFoundation] Cooldown active: {Time.time - _lastInteractionTime}/{buildingData.interactionCooldown}");
return false;
}
// 이미 완성됨
if (_currentProgress.Value >= buildingData.requiredWorkAmount)
{
@@ -155,7 +148,6 @@ namespace Northbound
return false;
}
Debug.Log($"<color=green>[BuildingFoundation] CanInteract = true</color>");
return true;
}

View File

@@ -120,11 +120,6 @@ namespace Northbound
// 충돌한 Collider가 있으면 배치 불가
if (colliders.Length > 0)
{
Debug.Log($"<color=yellow>[BuildingManager] 물리적 충돌 감지: {colliders.Length}개의 오브젝트와 겹침</color>");
foreach (var col in colliders)
{
Debug.Log($" - {col.gameObject.name} (Layer: {LayerMask.LayerToName(col.gameObject.layer)})");
}
return false;
}

View File

@@ -1,6 +1,7 @@
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.EventSystems;
namespace Northbound
{
@@ -124,12 +125,51 @@ namespace Northbound
if (isBuildModeActive)
{
// UI 표시
if (BuildingQuickslotUI.Instance != null)
{
BuildingQuickslotUI.Instance.ShowQuickslot(this);
}
CreatePreview();
}
else
{
// UI 숨김
if (BuildingQuickslotUI.Instance != null)
{
BuildingQuickslotUI.Instance.HideQuickslot();
}
DestroyPreview();
}
Debug.Log($"[BuildingPlacement] 건설 모드 {(isBuildModeActive ? "" : "")}");
}
/// <summary>
/// UI에서 건물 선택 시 호출
/// </summary>
public void SetSelectedBuilding(int index)
{
if (index < 0 || BuildingManager.Instance == null ||
index >= BuildingManager.Instance.availableBuildings.Count)
{
Debug.LogWarning($"[BuildingPlacement] 유효하지 않은 건물 인덱스: {index}");
return;
}
selectedBuildingIndex = index;
currentRotation = 0; // 회전 초기화
// 프리뷰 다시 생성
if (isBuildModeActive)
{
DestroyPreview();
CreatePreview();
}
Debug.Log($"[BuildingPlacement] 건물 선택됨: {BuildingManager.Instance.availableBuildings[index].buildingName}");
}
private void CreatePreview()
@@ -187,6 +227,8 @@ namespace Northbound
{
collider.enabled = false;
}
Debug.Log($"[BuildingPlacement] 프리뷰 생성됨: {data.buildingName}");
}
private void DestroyPreview()
@@ -241,6 +283,13 @@ namespace Northbound
{
if (!isBuildModeActive || previewObject == null) return;
// UI 위에서 클릭한 경우 무시
if (IsPointerOverUI())
{
Debug.Log("<color=cyan>[BuildingPlacement] UI 위에서 클릭함 - 건설 취소</color>");
return;
}
// Get placement position
Ray ray = Camera.main.ScreenPointToRay(Mouse.current.position.ReadValue());
if (Physics.Raycast(ray, out RaycastHit hit, maxPlacementDistance, groundLayer))
@@ -249,34 +298,43 @@ namespace Northbound
if (BuildingManager.Instance.IsValidPlacement(selectedData, hit.point, currentRotation, out Vector3 groundPosition))
{
// 토대 배치 요청 (실제로는 토대가 생성됨)
// 토대 배치 요청
BuildingManager.Instance.RequestPlaceFoundation(selectedBuildingIndex, groundPosition, currentRotation);
Debug.Log($"<color=green>[BuildingPlacement] 토대 배치 요청: {selectedData.buildingName}</color>");
Debug.Log($"<color=cyan>[BuildingPlacement] 건설 시작: {selectedData.buildingName}</color>");
}
else
{
Debug.LogWarning("<color=yellow>[BuildingPlacement] 배치할 수 없는 위치입니다.</color>");
Debug.Log("<color=yellow>[BuildingPlacement] 배치할 수 없는 위치입니다.</color>");
}
}
}
private void OnDrawGizmos()
/// <summary>
/// 마우스 포인터가 UI 위에 있는지 확인
/// </summary>
private bool IsPointerOverUI()
{
if (!IsOwner || !isBuildModeActive || !showGridBounds) return;
if (BuildingManager.Instance == null || previewObject == null || !previewObject.activeSelf) return;
// EventSystem이 없으면 UI 체크 불가능
if (EventSystem.current == null)
return false;
BuildingData data = BuildingManager.Instance.availableBuildings[selectedBuildingIndex];
if (data == null) return;
// New Input System을 사용하는 경우
if (Mouse.current != null)
{
Vector2 mousePosition = Mouse.current.position.ReadValue();
return EventSystem.current.IsPointerOverGameObject();
}
// Draw grid bounds being used for collision detection
Bounds bounds = BuildingManager.Instance.GetPlacementBounds(data, previewObject.transform.position, currentRotation);
// Legacy Input System (폴백)
return EventSystem.current.IsPointerOverGameObject();
}
// Check if valid
bool isValid = BuildingManager.Instance.IsValidPlacement(data, previewObject.transform.position, currentRotation, out _);
Gizmos.color = isValid ? new Color(0, 1, 0, 0.5f) : new Color(1, 0, 0, 0.5f);
Gizmos.DrawWireCube(bounds.center, bounds.size);
/// <summary>
/// 건설 모드 활성화 상태 확인
/// </summary>
public bool IsBuildModeActive()
{
return isBuildModeActive;
}
}
}

View File

@@ -0,0 +1,302 @@
using System.Collections.Generic;
using Unity.Netcode;
using UnityEngine;
using UnityEngine.InputSystem;
using TMPro;
namespace Northbound
{
/// <summary>
/// 건설 모드 시 건축 가능한 건물들을 표시하는 퀵슬롯 UI
/// </summary>
public class BuildingQuickslotUI : MonoBehaviour
{
public static BuildingQuickslotUI Instance { get; private set; }
[Header("UI References")]
[SerializeField] private GameObject quickslotPanel;
[SerializeField] private Transform slotContainer;
[SerializeField] private GameObject slotButtonPrefab;
[Header("Settings")]
[SerializeField] private int maxSlots = 8;
private List<BuildingSlotButton> slotButtons = new List<BuildingSlotButton>();
private BuildingPlacement buildingPlacement;
private int currentSelectedIndex = -1;
private PlayerInputActions _inputActions;
private InputAction[] _quickslotActions;
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
// Input Actions 초기화
_inputActions = new PlayerInputActions();
InitializeQuickslotActions();
}
private void OnEnable()
{
EnableQuickslotActions(false); // 초기에는 비활성화
}
private void OnDisable()
{
DisableQuickslotActions();
}
private void OnDestroy()
{
UnsubscribeQuickslotActions();
_inputActions?.Dispose();
}
private void Start()
{
if (quickslotPanel != null)
{
quickslotPanel.SetActive(false);
}
InitializeSlots();
}
/// <summary>
/// 퀵슬롯 Input Actions 초기화 및 구독 (직접 참조)
/// </summary>
private void InitializeQuickslotActions()
{
_quickslotActions = new InputAction[maxSlots];
// 수동으로 각 액션 할당
// Input Actions 에셋에 QuickSlot1~8 액션을 추가했는지 확인하세요
_quickslotActions[0] = _inputActions.Player.QuickSlot1;
_quickslotActions[1] = _inputActions.Player.QuickSlot2;
_quickslotActions[2] = _inputActions.Player.QuickSlot3;
_quickslotActions[3] = _inputActions.Player.QuickSlot4;
_quickslotActions[4] = _inputActions.Player.QuickSlot5;
_quickslotActions[5] = _inputActions.Player.QuickSlot6;
_quickslotActions[6] = _inputActions.Player.QuickSlot7;
_quickslotActions[7] = _inputActions.Player.QuickSlot8;
// 이벤트 구독
for (int i = 0; i < _quickslotActions.Length; i++)
{
if (_quickslotActions[i] != null)
{
int slotIndex = i; // 클로저 캡처를 위한 로컬 변수
_quickslotActions[i].performed += ctx => OnQuickslotPressed(slotIndex);
Debug.Log($"[BuildingQuickslotUI] QuickSlot{i + 1} 액션 바인딩 성공");
}
else
{
Debug.LogWarning($"[BuildingQuickslotUI] QuickSlot{i + 1} 액션이 null입니다. Input Actions 에셋을 확인하세요.");
}
}
}
/// <summary>
/// 퀵슬롯 액션 구독 해제
/// </summary>
private void UnsubscribeQuickslotActions()
{
if (_quickslotActions == null) return;
for (int i = 0; i < _quickslotActions.Length; i++)
{
if (_quickslotActions[i] != null)
{
int slotIndex = i;
_quickslotActions[i].performed -= ctx => OnQuickslotPressed(slotIndex);
}
}
}
/// <summary>
/// 퀵슬롯 액션 활성화
/// </summary>
private void EnableQuickslotActions(bool enable)
{
if (_quickslotActions == null) return;
foreach (var action in _quickslotActions)
{
if (action != null)
{
if (enable)
action.Enable();
else
action.Disable();
}
}
}
/// <summary>
/// 퀵슬롯 액션 비활성화
/// </summary>
private void DisableQuickslotActions()
{
EnableQuickslotActions(false);
}
/// <summary>
/// 퀵슬롯 키 입력 처리
/// </summary>
private void OnQuickslotPressed(int slotIndex)
{
// 퀵슬롯이 활성화되어 있고, 유효한 인덱스인 경우만 처리
if (quickslotPanel != null && quickslotPanel.activeSelf && slotIndex < slotButtons.Count)
{
SelectBuilding(slotIndex);
}
}
/// <summary>
/// BuildingManager의 건물 목록으로 슬롯 초기화
/// </summary>
private void InitializeSlots()
{
if (BuildingManager.Instance == null || slotContainer == null || slotButtonPrefab == null)
{
Debug.LogWarning("[BuildingQuickslotUI] 필수 컴포넌트가 설정되지 않았습니다.");
return;
}
// 기존 슬롯 제거
foreach (var slot in slotButtons)
{
if (slot != null)
Destroy(slot.gameObject);
}
slotButtons.Clear();
// 건물 목록으로 슬롯 생성 (최대 maxSlots개)
var buildings = BuildingManager.Instance.availableBuildings;
int slotCount = Mathf.Min(buildings.Count, maxSlots);
for (int i = 0; i < slotCount; i++)
{
CreateSlot(buildings[i], i);
}
Debug.Log($"[BuildingQuickslotUI] {slotButtons.Count}개의 건물 슬롯 생성됨");
}
/// <summary>
/// 개별 슬롯 버튼 생성
/// </summary>
private void CreateSlot(BuildingData buildingData, int index)
{
GameObject slotObj = Instantiate(slotButtonPrefab, slotContainer);
BuildingSlotButton slotButton = slotObj.GetComponent<BuildingSlotButton>();
if (slotButton != null)
{
slotButton.Initialize(buildingData, index, this);
slotButtons.Add(slotButton);
// 핫키 표시 (1-8)
slotButton.SetHotkeyText((index + 1).ToString());
}
}
/// <summary>
/// 건설 모드 활성화 (BuildingPlacement에서 호출)
/// </summary>
public void ShowQuickslot(BuildingPlacement placement)
{
buildingPlacement = placement;
if (quickslotPanel != null)
{
quickslotPanel.SetActive(true);
}
// 퀵슬롯 입력 활성화
EnableQuickslotActions(true);
// 기본 선택 (첫 번째 건물)
if (slotButtons.Count > 0)
{
SelectBuilding(0);
}
Debug.Log("[BuildingQuickslotUI] 건설 퀵슬롯 표시됨");
}
/// <summary>
/// 건설 모드 비활성화
/// </summary>
public void HideQuickslot()
{
if (quickslotPanel != null)
{
quickslotPanel.SetActive(false);
}
// 퀵슬롯 입력 비활성화
DisableQuickslotActions();
buildingPlacement = null;
currentSelectedIndex = -1;
// 모든 슬롯 선택 해제
foreach (var slot in slotButtons)
{
if (slot != null)
slot.SetSelected(false);
}
Debug.Log("[BuildingQuickslotUI] 건설 퀵슬롯 숨김");
}
/// <summary>
/// 건물 선택 (슬롯 버튼 클릭 또는 핫키)
/// </summary>
public void SelectBuilding(int index)
{
if (index < 0 || index >= slotButtons.Count)
return;
// 이전 선택 해제
if (currentSelectedIndex >= 0 && currentSelectedIndex < slotButtons.Count)
{
slotButtons[currentSelectedIndex].SetSelected(false);
}
// 새로운 선택
currentSelectedIndex = index;
slotButtons[index].SetSelected(true);
// BuildingPlacement에 알림
if (buildingPlacement != null)
{
buildingPlacement.SetSelectedBuilding(index);
}
Debug.Log($"[BuildingQuickslotUI] 건물 선택됨: {slotButtons[index].GetBuildingName()} (인덱스: {index})");
}
/// <summary>
/// 현재 선택된 건물 인덱스
/// </summary>
public int GetSelectedIndex()
{
return currentSelectedIndex;
}
/// <summary>
/// 슬롯 개수 반환
/// </summary>
public int GetSlotCount()
{
return slotButtons.Count;
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: ea63e719aed95e349b7f8dd7ec1d00b2

View File

@@ -0,0 +1,154 @@
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;
using TMPro;
namespace Northbound
{
/// <summary>
/// 개별 건물 슬롯 버튼
/// </summary>
[RequireComponent(typeof(EventTrigger))]
public class BuildingSlotButton : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
[Header("UI References")]
[SerializeField] private Image iconImage;
[SerializeField] private TextMeshProUGUI nameText;
[SerializeField] private TextMeshProUGUI hotkeyText;
[SerializeField] private Image backgroundImage;
[SerializeField] private Button button;
[Header("Visual Settings")]
[SerializeField] private Color normalColor = new Color(0.2f, 0.2f, 0.2f, 0.8f);
[SerializeField] private Color selectedColor = new Color(0.3f, 0.6f, 0.3f, 1f);
[SerializeField] private Color hoverColor = new Color(0.3f, 0.3f, 0.3f, 1f);
private BuildingData buildingData;
private int slotIndex;
private BuildingQuickslotUI quickslotUI;
private bool isSelected = false;
private void Awake()
{
if (button == null)
button = GetComponent<Button>();
if (button != null)
{
button.onClick.AddListener(OnButtonClicked);
}
}
/// <summary>
/// 슬롯 초기화
/// </summary>
public void Initialize(BuildingData data, int index, BuildingQuickslotUI ui)
{
buildingData = data;
slotIndex = index;
quickslotUI = ui;
UpdateVisuals();
}
/// <summary>
/// UI 업데이트
/// </summary>
private void UpdateVisuals()
{
if (buildingData == null) return;
// 아이콘 설정
if (iconImage != null && buildingData.icon != null)
{
iconImage.sprite = buildingData.icon;
iconImage.enabled = true;
}
else if (iconImage != null)
{
iconImage.enabled = false;
}
// 이름 설정
if (nameText != null)
{
nameText.text = buildingData.buildingName;
}
// 배경 색상
UpdateBackgroundColor();
}
/// <summary>
/// 핫키 텍스트 설정
/// </summary>
public void SetHotkeyText(string text)
{
if (hotkeyText != null)
{
hotkeyText.text = text;
}
}
/// <summary>
/// 버튼 클릭 이벤트
/// </summary>
private void OnButtonClicked()
{
if (quickslotUI != null)
{
quickslotUI.SelectBuilding(slotIndex);
}
}
/// <summary>
/// 선택 상태 설정
/// </summary>
public void SetSelected(bool selected)
{
isSelected = selected;
UpdateBackgroundColor();
}
/// <summary>
/// 배경 색상 업데이트
/// </summary>
private void UpdateBackgroundColor()
{
if (backgroundImage != null)
{
backgroundImage.color = isSelected ? selectedColor : normalColor;
}
}
/// <summary>
/// 마우스 호버 시 (IPointerEnterHandler)
/// </summary>
public void OnPointerEnter(PointerEventData eventData)
{
if (backgroundImage != null && !isSelected)
{
backgroundImage.color = hoverColor;
}
}
/// <summary>
/// 마우스 호버 해제 시 (IPointerExitHandler)
/// </summary>
public void OnPointerExit(PointerEventData eventData)
{
if (backgroundImage != null && !isSelected)
{
backgroundImage.color = normalColor;
}
}
/// <summary>
/// 건물 이름 반환
/// </summary>
public string GetBuildingName()
{
return buildingData != null ? buildingData.buildingName : "Unknown";
}
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: be718fab39448a644b228469875f0689