인벤토리/퀵슬롯 시스템 개발

This commit is contained in:
2026-01-18 15:54:42 +09:00
parent 9b13f439e3
commit bbd156ac03
21 changed files with 905 additions and 152 deletions

View File

@@ -0,0 +1,41 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
public class InventorySlotUI : MonoBehaviour
{
[Header("UI Elements")]
[SerializeField] private Image iconImage;
[SerializeField] private TextMeshProUGUI amountText;
[SerializeField] private GameObject selectionHighlight;
// 슬롯의 내용을 갱신하는 함수
public void UpdateView(int itemID, int amount)
{
// 빈 슬롯 처리 (ID가 -1이거나 개수가 0인 경우)
if (itemID == -1 || amount <= 0)
{
iconImage.enabled = false;
amountText.text = "";
}
else
{
// 싱글톤 데이터베이스에서 아이템 정보 가져오기
var itemData = ItemDatabase.Instance.GetItemByID(itemID);
if (itemData != null)
{
iconImage.sprite = itemData.icon;
iconImage.enabled = true;
amountText.text = amount.ToString();
}
}
}
// 선택 하이라이트 표시/숨김 (나중에 1~5번 키 입력 시 사용)
public void SetSelection(bool isSelected)
{
if (selectionHighlight != null)
selectionHighlight.SetActive(isSelected);
}
}

View File

@@ -0,0 +1,2 @@
fileFormatVersion: 2
guid: 4c873d40887ad9a439c14bf4f7fc31de

View File

@@ -1,24 +0,0 @@
using Unity.Netcode;
using UnityEngine;
public class ItemPickup : NetworkBehaviour
{
public ItemData itemData;
public void SetItem(ItemData data) => itemData = data;
private void OnTriggerEnter(Collider medical)
{
if (!IsServer) return;
// 플레이어인지 확인
if (medical.CompareTag("Player"))
{
if (medical.TryGetComponent<PlayerInventory>(out var inventory))
{
inventory.AddItem(itemData);
GetComponent<NetworkObject>().Despawn(); // 먹었으므로 제거
}
}
}
}

View File

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

View File

@@ -1,23 +1,89 @@
using Unity.Netcode;
using UnityEngine;
using System.Collections.Generic;
using System;
public class PlayerInventory : NetworkBehaviour
{
// 아이템 이름과 개수를 저장
private Dictionary<string, int> items = new Dictionary<string, int>();
[Header("Inventory Settings")]
[Range(1, 10)] // 인스펙터에서 슬라이더로 조절 가능
public int slotCount = 5;
public float maxWeight = 50f;
public void AddItem(ItemData data)
public struct InventorySlot : INetworkSerializable, IEquatable<InventorySlot>
{
if (!IsServer) return;
public int ItemID; // string에서 int로 변경
public int Amount;
if (items.ContainsKey(data.itemName))
items[data.itemName]++;
else
items.Add(data.itemName, 1);
public void NetworkSerialize<T>(BufferSerializer<T> serializer) where T : IReaderWriter
{
serializer.SerializeValue(ref ItemID);
serializer.SerializeValue(ref Amount);
}
Debug.Log($"[Inventory] {data.itemName} 획득! (현재: {items[data.itemName]}개)");
public bool Equals(InventorySlot other) => ItemID == other.ItemID && Amount == other.Amount;
}
// 여기서 클라이언트 UI를 갱신하는 ClientRpc를 호출할 수 있습니다.
[Header("References")]
[SerializeField] private ItemDatabase database;
public NetworkList<InventorySlot> Slots;
// 무게 정보는 서버에서만 쓰고 나머지는 읽기만 가능 (네트워크 보안)
private NetworkVariable<float> _currentWeight = new NetworkVariable<float>(0f, NetworkVariableReadPermission.Everyone, NetworkVariableWritePermission.Server);
public float CurrentWeight => _currentWeight.Value;
void Awake()
{
Slots = new NetworkList<InventorySlot>();
}
public override void OnNetworkSpawn()
{
// 서버에서만 슬롯 초기화
if (IsServer && Slots.Count == 0)
{
// 설정된 slotCount만큼 데이터 슬롯 생성
for (int i = 0; i < slotCount; i++)
Slots.Add(new InventorySlot { ItemID = -1, Amount = 0 });
}
// 로컬 플레이어라면 UI 연결
if (IsOwner)
{
QuickslotUI ui = FindFirstObjectByType<QuickslotUI>();
if (ui != null) ui.BindInventory(this);
}
}
// PlayerInventory.cs 내의 AddItemRpc 예시
[Rpc(SendTo.Server, InvokePermission = RpcInvokePermission.Everyone)]
public void AddItemRpc(int itemID, int amount)
{
// 이제 필드 변수 없이 바로 접근 가능!
ItemData data = ItemDatabase.Instance.GetItemByID(itemID);
if (data == null) return;
float addedWeight = data.weight * amount;
bool added = false;
for (int i = 0; i < Slots.Count; i++)
{
// 기존에 같은 아이템이 있거나(-1은 빈 슬롯을 의미) 빈 슬롯인 경우
if (Slots[i].ItemID == itemID || Slots[i].ItemID == -1)
{
Slots[i] = new InventorySlot
{
ItemID = itemID,
Amount = Slots[i].Amount + amount
};
added = true;
break;
}
}
if (added)
{
_currentWeight.Value += addedWeight;
}
}
}

View File

@@ -20,7 +20,6 @@ public class PlayerNetworkController : NetworkBehaviour
[SerializeField] private LayerMask interactableLayer;
[SerializeField] private LayerMask constructionLayer;
[SerializeField] private float buildSpeedMultiplier = 2f;
[SerializeField] private float pickupRadius = 1.5f; // 줍기 인식 범위
[SerializeField] private LayerMask itemLayer; // DroppedItem 레이어 설정
[Header("Mining Settings")]

View File

@@ -0,0 +1,76 @@
using UnityEngine;
using UnityEngine.UI;
using TMPro;
using Unity.Netcode;
using System.Collections.Generic;
public class QuickslotUI : MonoBehaviour
{
[Header("Dynamic Setup")]
[SerializeField] private GameObject slotPrefab; // 생성할 슬롯 프리팹
[SerializeField] private Transform slotContainer; // 슬롯들이 들어갈 부모 (HotbarPanel)
[Header("Weight UI")]
[SerializeField] private Slider weightSlider;
[SerializeField] private TextMeshProUGUI weightText;
private List<InventorySlotUI> _uiSlots = new List<InventorySlotUI>();
private PlayerInventory _linkedInventory;
public void BindInventory(PlayerInventory inventory)
{
_linkedInventory = inventory;
// 1. 기존에 있던 슬롯 UI들 모두 삭제 (초기화)
foreach (Transform child in slotContainer) Destroy(child.gameObject);
_uiSlots.Clear();
// 2. 인벤토리 데이터 크기(slotCount)만큼 UI 프리팹 생성
for (int i = 0; i < _linkedInventory.slotCount; i++)
{
GameObject newSlot = Instantiate(slotPrefab, slotContainer);
InventorySlotUI slotUI = newSlot.GetComponent<InventorySlotUI>();
_uiSlots.Add(slotUI);
}
// 3. 이벤트 등록 및 초기 갱신
_linkedInventory.Slots.OnListChanged += (e) => RefreshAll();
RefreshAll();
}
private void RefreshAll()
{
if (_linkedInventory == null) return;
for (int i = 0; i < _uiSlots.Count; i++)
{
var data = _linkedInventory.Slots[i];
_uiSlots[i].UpdateView(data.ItemID, data.Amount);
}
UpdateWeightDisplay();
}
// 데이터 변경 이벤트 핸들러
private void OnInventoryDataChanged(NetworkListEvent<PlayerInventory.InventorySlot> changeEvent)
{
RefreshAll();
}
private void UpdateWeightDisplay()
{
if (_linkedInventory == null) return;
float current = _linkedInventory.CurrentWeight;
float max = _linkedInventory.maxWeight;
if (weightSlider != null) weightSlider.value = current / max;
if (weightText != null) weightText.text = $"{current:F1} / {max:F1} kg";
}
private void OnDestroy()
{
// 이벤트 구독 해제 (메모리 누수 방지)
if (_linkedInventory != null)
_linkedInventory.Slots.OnListChanged -= OnInventoryDataChanged;
}
}

View File

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