2026-03-26 인벤토리
This commit is contained in:
8
Assets/02_Scripts/Item.meta
Normal file
8
Assets/02_Scripts/Item.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 44e602ec00fd406458986459bd362b5e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
33
Assets/02_Scripts/Item/Item.cs
Normal file
33
Assets/02_Scripts/Item/Item.cs
Normal file
@@ -0,0 +1,33 @@
|
||||
using UnityEngine;
|
||||
|
||||
public enum ItemType
|
||||
{
|
||||
EQUIPMENT = 0,
|
||||
CONSUMABLE = 1
|
||||
}
|
||||
|
||||
[CreateAssetMenu(fileName = "New Item", menuName = "Item")]
|
||||
public class Item : ScriptableObject
|
||||
{
|
||||
public string ItemId;
|
||||
public ItemType ItemType;
|
||||
public int SortId;
|
||||
public string ItemName;
|
||||
public Sprite Icon; //인벤토리용 2D 아이콘
|
||||
[TextArea] public string Description;
|
||||
public bool IsStackable; // 중첩 가능 여부
|
||||
public int MaxStack = 99; //장비는 1개
|
||||
public int Rarity = 1; //기본등급 1
|
||||
|
||||
// 월드용 데이터
|
||||
public GameObject PrefabVisible; // 월드상에서 보일 아이템의 프리팹
|
||||
public GameObject PrefabTuning; // 위치,회전 등이 조정된 모델
|
||||
public GameObject PrefabWild; // 아무런 조정도 하지 않은 아이템의 진짜 원본
|
||||
|
||||
public Vector3 WorldScale = Vector3.one; // 아이템마다 다른 크기 조절이 필요할 때
|
||||
public Vector3 WorldRotation = Vector3.zero;
|
||||
|
||||
[Header("Collider Settings")]
|
||||
public Vector3 ColliderCenter = Vector3.zero;
|
||||
public Vector3 ColliderSize = Vector3.one;
|
||||
}
|
||||
2
Assets/02_Scripts/Item/Item.cs.meta
Normal file
2
Assets/02_Scripts/Item/Item.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5f26678279b3061489f872b3da046a05
|
||||
21
Assets/02_Scripts/Item/ItemInstance.cs
Normal file
21
Assets/02_Scripts/Item/ItemInstance.cs
Normal file
@@ -0,0 +1,21 @@
|
||||
using UnityEngine;
|
||||
|
||||
[System.Serializable]
|
||||
public class ItemInstance
|
||||
{
|
||||
public Item Data; // 원본 ScriptableObject 참조 (이름, 아이콘 등 불변 데이터)
|
||||
|
||||
// 개별적으로 변하는 데이터들
|
||||
public int EnhancementLevel; // 강화 수치
|
||||
public int Durability; // 내구도
|
||||
public int CurrentStack; // 현재 수량 (중첩 아이템일 경우)
|
||||
|
||||
//생성자
|
||||
public ItemInstance(Item sourceData, int stack = 1)
|
||||
{
|
||||
this.Data = sourceData;
|
||||
this.CurrentStack = stack;
|
||||
this.EnhancementLevel = -1;// 기본 강화 수치 (-1은 강화수치가 없는 아이템)
|
||||
this.Durability = -1; // 기본 내구도 (-1은 내구도가 없는 아이템)
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/Item/ItemInstance.cs.meta
Normal file
2
Assets/02_Scripts/Item/ItemInstance.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e19279917301c134f9d0cf2067c09fac
|
||||
105
Assets/02_Scripts/Item/WorldItem.cs
Normal file
105
Assets/02_Scripts/Item/WorldItem.cs
Normal file
@@ -0,0 +1,105 @@
|
||||
using NUnit.Framework.Interfaces;
|
||||
using System;
|
||||
using UnityEngine;
|
||||
|
||||
public class WorldItem : MonoBehaviour
|
||||
{
|
||||
// 실제 아이템 데이터 (강화 수치, 수량 등을 포함)
|
||||
public ItemInstance ItemInstance;
|
||||
private GameObject ModelPrefab; //월드 좌표는 사용하지 않는다 (복잡한 구조를 쉽게 가져오기 위해 Prefab으로 선언)
|
||||
private BoxCollider _boxCollider;
|
||||
|
||||
private float _rotationSpeed; // 회전 속도
|
||||
private float _bounceAmplitude; // 오르내리는 높이
|
||||
private float _bounceFrequency; // 오르내리는 속도
|
||||
private Vector3 _startPos;
|
||||
|
||||
[Header("Only Test")]
|
||||
[SerializeField] private Item _testField_OriginItem;
|
||||
[SerializeField] private int _testField_ItemStack = 1;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_boxCollider = GetComponent<BoxCollider>();
|
||||
if (_boxCollider == null) _boxCollider = gameObject.AddComponent<BoxCollider>();
|
||||
if (_boxCollider != null) _boxCollider.isTrigger = true;
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
//테스트용
|
||||
if (ItemInstance == null || ItemInstance.Data == null)
|
||||
{
|
||||
ItemInstance Item = new ItemInstance(_testField_OriginItem, _testField_ItemStack);
|
||||
SetItem(Item);
|
||||
}
|
||||
|
||||
_rotationSpeed = GameManager.Instance.ItemRotationSpeed;
|
||||
_bounceAmplitude = GameManager.Instance.ItemBounceAmplitude;
|
||||
_bounceFrequency = GameManager.Instance.ItemBounceFrequency;
|
||||
|
||||
_startPos = transform.position;
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
// Y축을 기준으로 매 프레임 회전
|
||||
transform.Rotate(Vector3.up * _rotationSpeed * Time.deltaTime);
|
||||
|
||||
// 위아래로 둥둥 떠있는 듯한 느낌
|
||||
float newY = _startPos.y + Mathf.Sin(Time.time * _bounceFrequency) * _bounceAmplitude;
|
||||
transform.position = new Vector3(_startPos.x, newY, _startPos.z);
|
||||
}
|
||||
|
||||
// 아이템 데이터를 이 오브젝트에 주입하는 메서드
|
||||
public void SetItem(ItemInstance instance)
|
||||
{
|
||||
ItemInstance = instance;
|
||||
|
||||
// 기존 모델 지우기
|
||||
foreach (Transform child in transform) Destroy(child.gameObject);
|
||||
|
||||
if (instance.Data != null)
|
||||
{
|
||||
ModelPrefab = Instantiate(instance.Data.PrefabVisible, transform); //원본데이터에서 복제하여 자식으로 배치
|
||||
ModelPrefab.transform.localPosition = Vector3.zero;
|
||||
ModelPrefab.transform.localRotation = Quaternion.identity;
|
||||
ModelPrefab.transform.localScale = instance.Data.WorldScale;
|
||||
ModelPrefab.transform.localRotation = Quaternion.Euler(instance.Data.WorldRotation);
|
||||
|
||||
// 2. [핵심] 데이터에 저장된 값을 그대로 콜라이더에 주입
|
||||
if (_boxCollider != null)
|
||||
{
|
||||
_boxCollider.center = instance.Data.ColliderCenter;
|
||||
_boxCollider.size = instance.Data.ColliderSize;
|
||||
}
|
||||
}
|
||||
|
||||
gameObject.layer = LayerMask.NameToLayer("Item");
|
||||
}
|
||||
|
||||
// 플레이어가 아이템을 획득할 때 호출
|
||||
public void PickUp()
|
||||
{
|
||||
GameManager.Instance.Inventory.AddItem(this.ItemInstance);
|
||||
|
||||
PlayPickupEffect();
|
||||
|
||||
Destroy(gameObject);
|
||||
}
|
||||
|
||||
private void PlayPickupEffect()
|
||||
{
|
||||
// 이펙트 프리팹을 소환하거나 소리를 재생
|
||||
Debug.Log($"{ItemInstance.Data.ItemName} 획득 이펙트 재생!");
|
||||
}
|
||||
|
||||
private void OnTriggerEnter(Collider other)
|
||||
{
|
||||
if (other.CompareTag("Player"))
|
||||
{
|
||||
PickUp();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
Assets/02_Scripts/Item/WorldItem.cs.meta
Normal file
2
Assets/02_Scripts/Item/WorldItem.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5c8828a71cdaf64ba889ff392c6fb1f
|
||||
@@ -14,6 +14,12 @@ public class GameManager : MonoBehaviour
|
||||
//UI
|
||||
public IntroUIManager IntroUI { get; private set; }
|
||||
public InGameUIManager InGameUI { get; private set; }
|
||||
public InventoryManager Inventory { get; private set; }
|
||||
|
||||
[Header("Item Dynamic Settings")]
|
||||
public float ItemRotationSpeed = 50f; // 회전 속도
|
||||
public float ItemBounceAmplitude = 0.1f; // 오르내리는 높이
|
||||
public float ItemBounceFrequency = 2f; // 오르내리는 속도
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
@@ -37,6 +43,7 @@ private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
|
||||
this.Camera = FindFirstObjectByType<CameraManager>();
|
||||
this.IntroUI = FindFirstObjectByType<IntroUIManager>();
|
||||
this.InGameUI = FindFirstObjectByType<InGameUIManager>();
|
||||
this.Inventory = FindFirstObjectByType<InventoryManager>();
|
||||
|
||||
if (this.Level != null) this.Level.OnSceneLoaded(scene, mode);
|
||||
if (this.Camera != null) this.Camera.OnSceneLoaded(scene, mode);
|
||||
|
||||
390
Assets/02_Scripts/Managers/Local/InventoryManager.cs
Normal file
390
Assets/02_Scripts/Managers/Local/InventoryManager.cs
Normal file
@@ -0,0 +1,390 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using Unity.VisualScripting;
|
||||
using Unity.VisualScripting.Antlr3.Runtime.Misc;
|
||||
using UnityEngine;
|
||||
|
||||
public class InventoryManager : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private GameObject _inventoryRoot;
|
||||
[SerializeField] private GameObject _inventoryContentRoot;
|
||||
[SerializeField] private GameObject _slotPrefab;
|
||||
|
||||
private ItemInstance[] Items;
|
||||
private InventorySlot[] Slots;
|
||||
public int Capacity = 30; // 인벤토리 칸수
|
||||
|
||||
private bool _isUpdating = false;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
// Items가 아예 없으면 새로 만들기
|
||||
if (Items == null)
|
||||
{
|
||||
Items = new ItemInstance[Capacity];
|
||||
}
|
||||
// 기존 데이터가 있는데 Capacity와 다르다면
|
||||
else if (Items.Length != Capacity)
|
||||
{
|
||||
// 기존 요소를 새 배열로 복사
|
||||
System.Array.Resize(ref Items, Capacity);
|
||||
}
|
||||
|
||||
Slots = new InventorySlot[Capacity];
|
||||
|
||||
//기존 슬롯들 지우기
|
||||
foreach (Transform child in _inventoryContentRoot.transform) Destroy(child.gameObject);
|
||||
|
||||
for (int i = 0; i < Capacity; i++) //설정된 인벤토리 칸수만큼 추가
|
||||
{
|
||||
GameObject invenSlotObject = Instantiate(_slotPrefab, _inventoryContentRoot.transform);
|
||||
InventorySlot invenSlot = invenSlotObject.GetComponent<InventorySlot>();
|
||||
if (invenSlot != null)
|
||||
{
|
||||
Slots[i] = invenSlot;
|
||||
invenSlot.SlotIndex = i;
|
||||
invenSlot.SetItem(Items[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
private void Start()
|
||||
{
|
||||
|
||||
}
|
||||
public void InventoryToggle()
|
||||
{
|
||||
if (_inventoryRoot != null)
|
||||
{
|
||||
InventoryOnOff(!_inventoryRoot.activeSelf);
|
||||
}
|
||||
}
|
||||
|
||||
public void InventoryOnOff(bool onOff)
|
||||
{
|
||||
if (_inventoryRoot != null)
|
||||
{
|
||||
_inventoryRoot.SetActive(onOff);
|
||||
}
|
||||
}
|
||||
|
||||
public void UpdateUI()
|
||||
{
|
||||
for (int i = 0; i < Slots.Length; i++)
|
||||
{
|
||||
Slots[i].SetItem(Items[i]);
|
||||
}
|
||||
}
|
||||
|
||||
public void MoveItem(int sourceIndex, int targetIndex)
|
||||
{
|
||||
// 동일한 슬롯이면 계산할 필요 없이 종료
|
||||
if (sourceIndex == targetIndex) return;
|
||||
|
||||
// source가 null인 경우(빈 슬롯 드래그)
|
||||
if (Items[sourceIndex] == null) return;
|
||||
|
||||
// 타겟 슬롯이 비어있으면 그냥 이동
|
||||
if (Items[targetIndex] == null)
|
||||
{
|
||||
Items[targetIndex] = Items[sourceIndex];
|
||||
Items[sourceIndex] = null;
|
||||
}
|
||||
// 타겟 슬롯에 같은 아이템이 있고 중첩 가능하다면 병합 시도
|
||||
else if (Items[sourceIndex].Data.ItemId == Items[targetIndex].Data.ItemId && Items[targetIndex].Data.IsStackable)
|
||||
{
|
||||
int canAdd = Items[targetIndex].Data.MaxStack - Items[targetIndex].CurrentStack;
|
||||
int actualAdd = Mathf.Min(canAdd, Items[sourceIndex].CurrentStack);
|
||||
|
||||
Debug.Log($"actualAdd : {actualAdd}");
|
||||
|
||||
Items[targetIndex].CurrentStack += actualAdd;
|
||||
Items[sourceIndex].CurrentStack -= actualAdd;
|
||||
|
||||
// 원본 슬롯 아이템이 0개가 되면 제거
|
||||
if (Items[sourceIndex].CurrentStack <= 0)
|
||||
{
|
||||
Items[sourceIndex] = null;
|
||||
}
|
||||
|
||||
}
|
||||
// 아이템이 다르거나 중첩 불가라면 스왑
|
||||
else
|
||||
{
|
||||
ItemInstance temp = Items[targetIndex];
|
||||
Items[targetIndex] = Items[sourceIndex];
|
||||
Items[sourceIndex] = temp;
|
||||
}
|
||||
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
public void ExecuteSplit(int sourceIndex, int targetIndex, int splitCount)
|
||||
{
|
||||
|
||||
// 대상 슬롯이 비어있는 경우
|
||||
if (Items[targetIndex] == null)
|
||||
{
|
||||
// 전체를 다 옮기는 경우 (이동과 동일)
|
||||
if (splitCount >= Items[sourceIndex].CurrentStack)
|
||||
{
|
||||
// 대상 슬롯에 원본 그대로 배치
|
||||
Items[targetIndex] = Items[sourceIndex];
|
||||
Slots[targetIndex].SetItem(Items[sourceIndex]);
|
||||
|
||||
// 원본 슬롯은 비우기
|
||||
Items[sourceIndex] = null;
|
||||
Slots[sourceIndex].ClearSlot(); // 기존 아이템 파괴 및 UI 초기화 함수
|
||||
}
|
||||
// 일부만 떼어내는 경우 (분할)
|
||||
else
|
||||
{
|
||||
//원본 아이템 개수 차감
|
||||
Items[sourceIndex].CurrentStack -= splitCount;
|
||||
Slots[sourceIndex].UpdateSlotUI();
|
||||
|
||||
//새로운 아이템 인스턴스 생성 (복제)
|
||||
ItemInstance newItem = new ItemInstance(Items[sourceIndex].Data, splitCount);
|
||||
|
||||
// 대상 슬롯에 새 아이템 배치
|
||||
Items[targetIndex] = newItem;
|
||||
Slots[targetIndex].SetItem(newItem);
|
||||
}
|
||||
}
|
||||
// 대상 슬롯에 같은 아이템이 있는 경우 (합치기)
|
||||
else if (Items[targetIndex].Data == Items[sourceIndex].Data)
|
||||
{
|
||||
int maxStack = Items[targetIndex].Data.MaxStack;
|
||||
int targetRoom = maxStack - Items[targetIndex].CurrentStack; // 대상 슬롯의 남은 공간
|
||||
|
||||
// 옮기려는 양(splitCount)이 남은 공간보다 작거나 같으면 -> 다 들어감
|
||||
if (splitCount <= targetRoom)
|
||||
{
|
||||
Items[targetIndex].CurrentStack += splitCount;
|
||||
Items[sourceIndex].CurrentStack -= splitCount;
|
||||
|
||||
// 원본이 0개가 되면 슬롯 비우기
|
||||
if (Items[sourceIndex].CurrentStack <= 0)
|
||||
{
|
||||
Items[sourceIndex] = null;
|
||||
Slots[sourceIndex].ClearSlot();
|
||||
}
|
||||
else
|
||||
{
|
||||
Slots[sourceIndex].UpdateSlotUI();
|
||||
}
|
||||
Slots[targetIndex].UpdateSlotUI();
|
||||
}
|
||||
// 옮기려는 양이 남은 공간보다 많으면 -> 공간만큼만 채우고 나머지는 원본에 남김
|
||||
else
|
||||
{
|
||||
Items[targetIndex].CurrentStack = maxStack; // 대상은 풀스택
|
||||
Items[sourceIndex].CurrentStack -= targetRoom; // 원본에서는 들어간 만큼만 뺌
|
||||
|
||||
Slots[sourceIndex].UpdateSlotUI();
|
||||
Slots[targetIndex].UpdateSlotUI();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 수동 정렬
|
||||
public void SortInventory()
|
||||
{
|
||||
// null이 아닌 아이템만 모아서 (필요시 이름/ID 등으로 정렬)
|
||||
List<ItemInstance> validItems = Items.Where(i => i != null)
|
||||
.OrderBy(i => i.Data.ItemType) // 1순위: 아이템 타입
|
||||
.ThenBy(i => i.Data.SortId) // 2순위: 고유 정렬 번호
|
||||
.ToList();
|
||||
|
||||
// 배열 초기화 후 앞에서부터 다시 채워넣기
|
||||
Items = new ItemInstance[Capacity];
|
||||
for (int i = 0; i < validItems.Count; i++)
|
||||
{
|
||||
Items[i] = validItems[i];
|
||||
}
|
||||
|
||||
UpdateUI();
|
||||
}
|
||||
|
||||
private int CalculateCanAddAmount(ItemInstance item)
|
||||
{
|
||||
int totalCanAdd = 0;
|
||||
|
||||
// 기존 슬롯의 여유 공간 합산 (중첩 가능한 경우만)
|
||||
if (item.Data.IsStackable)
|
||||
{
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
{
|
||||
if (Items[i] != null && Items[i].Data.ItemId == item.Data.ItemId)
|
||||
{
|
||||
totalCanAdd += (Items[i].Data.MaxStack - Items[i].CurrentStack);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 빈 슬롯이 제공할 수 있는 최대 공간 합산
|
||||
int emptySlotCount = Items.Count(x => x == null);
|
||||
|
||||
if (item.Data.IsStackable)
|
||||
{
|
||||
// 빈 슬롯당 MaxStack만큼 수용 가능
|
||||
totalCanAdd += (emptySlotCount * item.Data.MaxStack);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 중첩 불가는 빈 슬롯 하나당 1개
|
||||
totalCanAdd += emptySlotCount;
|
||||
}
|
||||
|
||||
return totalCanAdd;
|
||||
}
|
||||
|
||||
//서버 통신 AddItem
|
||||
public async Awaitable<bool> AddItemAsync(ItemInstance item)
|
||||
{
|
||||
if (_isUpdating) return false;
|
||||
_isUpdating = true;
|
||||
|
||||
// 현재 가방에 총 몇 개까지 더 들어가는가?
|
||||
int possibleAmount = CalculateCanAddAmount(item);
|
||||
|
||||
if (possibleAmount <= 0)
|
||||
{
|
||||
Debug.LogWarning("인벤토리에 공간이 없습니다!");
|
||||
_isUpdating = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
// 습득량 결정
|
||||
int originStack = item.CurrentStack;
|
||||
int actualAmountToTake = Mathf.Min(item.CurrentStack, possibleAmount);
|
||||
int remainingAmount = item.CurrentStack - actualAmountToTake;
|
||||
|
||||
// 서버 통신 (실제로 가방에 들어올 양만 설정해서 보냄)
|
||||
// 원래 item의 수량을 조절해서 보냄
|
||||
item.CurrentStack = actualAmountToTake;
|
||||
|
||||
// bool serverResult = await DataManager.Instance.SaveItemToServer(item);
|
||||
bool serverResult = true; // 테스트용
|
||||
|
||||
// 서버 저장 성공 시에만 실제 로컬 배열에 데이터 기입
|
||||
if (serverResult)
|
||||
{
|
||||
bool result = AddItem(item); //함수 내에서 item.CurrentStack 에 추가하고 남은양이 기록됨
|
||||
|
||||
// 이미 습득량을 계산해서 AddItem에 넣어서 보통의 경우 남는게 없지만, 만약 AddItem을 하고 남은 분량이 있다면 그 값을 습득량 계산하고 남은값(remainingAmount)에 더해줘야 실제 남은값임
|
||||
item.CurrentStack += remainingAmount;
|
||||
|
||||
_isUpdating = false;
|
||||
return result;
|
||||
}
|
||||
else
|
||||
item.CurrentStack = originStack;//추가된게 없으므로 원래 수량
|
||||
|
||||
Debug.LogError("서버 저장 실패로 아이템 획득이 취소되었습니다.");
|
||||
_isUpdating = false;
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool AddItem(ItemInstance item)
|
||||
{
|
||||
int amountToAdd = item.CurrentStack;
|
||||
|
||||
// 기존에 같은 아이템이 있는 모든 슬롯을 먼저 채우기
|
||||
if (item.Data.IsStackable)
|
||||
{
|
||||
for (int i = 0; i < Items.Length; i++)
|
||||
{
|
||||
// 같은 ID이고, 아직 더 넣을 공간이 있는 슬롯 탐색
|
||||
if (Items[i] != null && Items[i].Data.ItemId == item.Data.ItemId && Items[i].CurrentStack < Items[i].Data.MaxStack)
|
||||
{
|
||||
int canAdd = Items[i].Data.MaxStack - Items[i].CurrentStack; // 슬롯의 여유 공간
|
||||
int actualAdd = Mathf.Min(canAdd, amountToAdd); // 실제로 넣을 수 있는 양
|
||||
|
||||
Items[i].CurrentStack += actualAdd;
|
||||
amountToAdd -= actualAdd;
|
||||
|
||||
// 다 채웠으면 종료
|
||||
if (amountToAdd <= 0)
|
||||
{
|
||||
UpdateUI();
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 기존 슬롯을 다 채우고 남은 양이 있을 경우, 혹은 스택형 아이템이 아닌 경우
|
||||
while (amountToAdd > 0)
|
||||
{
|
||||
int emptyIndex = System.Array.FindIndex(Items, x => x == null);
|
||||
|
||||
if (emptyIndex == -1)
|
||||
{
|
||||
// 더 이상 빈칸이 없는데 아이템이 남았다면 나머지는 버리도록
|
||||
item.CurrentStack = amountToAdd; // 남은 양을 아이템 객체에 업데이트
|
||||
|
||||
// 남은 양만큼의 아이템 데이터를 새로 만들어서 필드에 버립니다.
|
||||
ItemInstance leftOver = new ItemInstance(item.Data, item.CurrentStack);
|
||||
leftOver.EnhancementLevel = item.EnhancementLevel;
|
||||
leftOver.Durability = item.Durability;
|
||||
|
||||
SpawnItemToWorld(leftOver); // 필드에 버림 처리
|
||||
|
||||
item.CurrentStack = 0; // 원본에서는 이제 남은 양이 없도록 처리
|
||||
|
||||
UpdateUI();
|
||||
Debug.LogWarning("인벤토리가 가득 찼습니다.");
|
||||
return false;
|
||||
}
|
||||
|
||||
// 새 슬롯에 들어갈 양 결정 (MaxStack까지만)
|
||||
int addAtOnce = Mathf.Min(amountToAdd, item.Data.MaxStack);
|
||||
|
||||
// 여러 칸을 추가해야 될수 있으므로 복사본으로 추가
|
||||
Items[emptyIndex] = new ItemInstance(item.Data, addAtOnce);
|
||||
|
||||
// 만약 아이템에 강화 정보 등이 있다면
|
||||
Items[emptyIndex].EnhancementLevel = item.EnhancementLevel;
|
||||
|
||||
amountToAdd -= addAtOnce;
|
||||
}
|
||||
|
||||
UpdateUI();
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool AddItem(Item itemData)
|
||||
{
|
||||
ItemInstance Item = new ItemInstance(itemData);
|
||||
return AddItem(Item);
|
||||
}
|
||||
|
||||
// 월드에 아이템을 버리는(생성하는) 함수
|
||||
public void SpawnItemToWorld(ItemInstance dropItem)
|
||||
{
|
||||
if (dropItem == null || dropItem.CurrentStack <= 0) return;
|
||||
|
||||
GameObject player = GameManager.Instance.Level.CurrentCharacter;
|
||||
Vector3 dropPosition = player != null ? player.transform.position + player.transform.forward * 1.5f + Vector3.up * (dropItem.Data.WorldScale.y * 0.5f + 0.25f) : Vector3.zero;
|
||||
|
||||
// 빈 게임오브젝트를 생성하고 WorldItem 컴포넌트를 붙임
|
||||
GameObject dropObj = new GameObject($"{dropItem.Data.ItemName}_Dropped");
|
||||
dropObj.transform.position = dropPosition;
|
||||
|
||||
WorldItem worldItem = dropObj.AddComponent<WorldItem>();
|
||||
worldItem.SetItem(dropItem); // SetItem으로 데이터 주입 및 모델 생성
|
||||
|
||||
Debug.Log($"{dropItem.Data.ItemName} {dropItem.CurrentStack}개를 필드에 버렸습니다.");
|
||||
}
|
||||
|
||||
public void DropItemFromSlot(int slotIndex)
|
||||
{
|
||||
ItemInstance itemToDrop = Items[slotIndex];
|
||||
if (itemToDrop == null) return;
|
||||
|
||||
SpawnItemToWorld(itemToDrop); // 월드에 소환
|
||||
Items[slotIndex] = null; // 인벤토리에서는 제거
|
||||
|
||||
UpdateUI();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b22ff4214afbf4d48891524d590dead9
|
||||
@@ -2,10 +2,21 @@
|
||||
|
||||
public class InGameUIManager : BaseUIManager
|
||||
{
|
||||
public SplitWindowUI SplitWindowUI;
|
||||
public TooltipUI TooltipUI;
|
||||
public Transform DragCanvas;
|
||||
|
||||
[SerializeField] private GameObject _crosshairRoot;
|
||||
|
||||
|
||||
|
||||
public void VisibleCrossHair(bool isOn)
|
||||
{
|
||||
_crosshairRoot.SetActive(isOn);
|
||||
}
|
||||
|
||||
public SplitWindowUI GetSplitWindowUI()
|
||||
{
|
||||
return SplitWindowUI;
|
||||
}
|
||||
}
|
||||
|
||||
8
Assets/02_Scripts/UI/Inventory.meta
Normal file
8
Assets/02_Scripts/UI/Inventory.meta
Normal file
@@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 92a7e920733b3e840935254a21a578d8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
84
Assets/02_Scripts/UI/Inventory/InventoryItemControl.cs
Normal file
84
Assets/02_Scripts/UI/Inventory/InventoryItemControl.cs
Normal file
@@ -0,0 +1,84 @@
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
|
||||
public class InventoryItemControl : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
|
||||
{
|
||||
[HideInInspector] public InventorySlot ParentSlot;
|
||||
|
||||
private Transform _dragTransform;
|
||||
private CanvasGroup _canvasGroup;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
_canvasGroup = GetComponent<CanvasGroup>();
|
||||
}
|
||||
|
||||
private void Start()
|
||||
{
|
||||
_dragTransform = GameManager.Instance.InGameUI.DragCanvas;
|
||||
}
|
||||
|
||||
public void OnBeginDrag(PointerEventData eventData)
|
||||
{
|
||||
// 시작할 때 부모 슬롯을 아예 스크립트 통째로 저장!
|
||||
ParentSlot = GetComponentInParent<InventorySlot>();
|
||||
|
||||
if (ParentSlot == null || ParentSlot.currentItem == null)
|
||||
{
|
||||
// eventData.pointerDrag를 null로 만들면 이후 OnDrag, OnEndDrag도 호출안됨
|
||||
eventData.pointerDrag = null;
|
||||
return;
|
||||
}
|
||||
|
||||
transform.SetParent(_dragTransform);
|
||||
_canvasGroup.blocksRaycasts = false;
|
||||
}
|
||||
|
||||
public void OnDrag(PointerEventData eventData)
|
||||
{
|
||||
transform.position = eventData.position; // 마우스 위치 추적
|
||||
}
|
||||
|
||||
public void OnEndDrag(PointerEventData eventData)
|
||||
{
|
||||
_canvasGroup.blocksRaycasts = true;
|
||||
|
||||
// 마우스 아래에 슬롯이 없어서 제자리로 복귀해야 하는 상황인지 판별
|
||||
if (transform.parent == _dragTransform)
|
||||
{
|
||||
// eventData.pointerEnter는 드래그가 끝난 지점에 마우스가 가리키고 있는 오브젝트 (이전까지 _canvasGroup.blocksRaycasts = false; 였으므로 자신 제외)
|
||||
// 가르키는 오브젝트가 없거나, UI 레이어가 아니라면 버린것으로 간주
|
||||
bool isDroppedOutsideUI = eventData.pointerEnter == null || eventData.pointerEnter.layer != LayerMask.NameToLayer("UI");
|
||||
|
||||
if (isDroppedOutsideUI)
|
||||
{
|
||||
// 먼저 원래 부모 슬롯으로 복귀시킴 (드래그 전용 캔버스에 남는것을 방지)
|
||||
transform.SetParent(ParentSlot.transform);
|
||||
transform.localPosition = Vector3.zero;
|
||||
|
||||
GameManager.Instance.Inventory.DropItemFromSlot(ParentSlot.SlotIndex);
|
||||
}
|
||||
else
|
||||
{
|
||||
// UI 위이긴 한데 (예: 인벤토리 빈 여백, 다른 창) 슬롯이 아니라서 드롭 처리가 안 된 경우 -> 제자리 복귀
|
||||
transform.SetParent(ParentSlot.transform);
|
||||
transform.localPosition = Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
//드래그를 취소했을때 툴팁이 다시 나와야됨
|
||||
GameObject overObj = eventData.pointerCurrentRaycast.gameObject;
|
||||
if (overObj != null)
|
||||
{
|
||||
// 만약 마우스 아래에 슬롯이 있다면 그 슬롯의 아이템정보로 툴팁 띄우기
|
||||
InventorySlot slot = overObj.GetComponentInParent<InventorySlot>();
|
||||
if (slot != null)
|
||||
{
|
||||
if (slot.currentItem != null && slot.currentItem.Data != null)
|
||||
{
|
||||
GameManager.Instance.InGameUI.TooltipUI.ShowTooltip(slot.currentItem, slot.GetComponent<RectTransform>());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9f41f1b66f585124db9f2823ebb15524
|
||||
179
Assets/02_Scripts/UI/Inventory/InventorySlot.cs
Normal file
179
Assets/02_Scripts/UI/Inventory/InventorySlot.cs
Normal file
@@ -0,0 +1,179 @@
|
||||
using TMPro;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class InventorySlot : MonoBehaviour, IDropHandler, IPointerEnterHandler, IPointerExitHandler, IPointerMoveHandler
|
||||
{
|
||||
public int SlotIndex; // 슬롯의 고유 번호
|
||||
private RectTransform _rectTransform; //슬롯박스 사각형
|
||||
|
||||
[Header("Slot References")]
|
||||
[SerializeField] private Image _iconImage;
|
||||
[SerializeField] private TextMeshProUGUI _stackText;
|
||||
[SerializeField] private Image _rarityImage; // 등급 배경 등
|
||||
[SerializeField] private GameObject _highlightBox;
|
||||
|
||||
[Header("Slot Settings")]
|
||||
[SerializeField] private Sprite _rarity_None_Sprite;
|
||||
[SerializeField] private Sprite _rarity_1_Sprite;
|
||||
[SerializeField] private Sprite _rarity_2_Sprite;
|
||||
[SerializeField] private Sprite _rarity_3_Sprite;
|
||||
[SerializeField] private Sprite _rarity_4_Sprite;
|
||||
|
||||
[Header("Current ItemInstance")]
|
||||
public ItemInstance currentItem; // 이 슬롯에 담긴 아이템 정보
|
||||
|
||||
//private Image SlotBg; //기본 슬롯 배경
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
//SlotBg = GetComponent<Image>();
|
||||
_rectTransform = GetComponent<RectTransform>();
|
||||
ClearSlot();
|
||||
}
|
||||
|
||||
// 아이템 데이터 설정 및 UI 갱신
|
||||
public void SetItem(ItemInstance newItem)
|
||||
{
|
||||
currentItem = newItem;
|
||||
|
||||
UpdateSlotUI();
|
||||
}
|
||||
|
||||
public void OnDrop(PointerEventData eventData)
|
||||
{
|
||||
GameObject dropped = eventData.pointerDrag;
|
||||
if (dropped != null)
|
||||
{
|
||||
InventoryItemControl dragItem = dropped.GetComponent<InventoryItemControl>();
|
||||
if (dragItem != null)
|
||||
{
|
||||
InventorySlot originalSlot = dragItem.ParentSlot;
|
||||
|
||||
if (originalSlot != null && originalSlot != this)
|
||||
{
|
||||
// 스택 가능한 아이템이고 개수가 1개보다 많을 때만 분할 창 띄우기
|
||||
if (originalSlot.currentItem.Data.IsStackable && originalSlot.currentItem.CurrentStack > 1)
|
||||
{
|
||||
// 팝업 오픈 (Action으로 분할 로직 전달)
|
||||
GameManager.Instance.InGameUI.GetSplitWindowUI().Open(originalSlot.currentItem.CurrentStack, (splitCount) =>
|
||||
{
|
||||
GameManager.Instance.Inventory.ExecuteSplit(originalSlot.SlotIndex, this.SlotIndex, splitCount);
|
||||
});
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.Log($"{originalSlot.SlotIndex}번 슬롯에서 {this.SlotIndex}번 슬롯으로 이동 시도");
|
||||
GameManager.Instance.Inventory.MoveItem(originalSlot.SlotIndex, this.SlotIndex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
public void OnPointerEnter(PointerEventData eventData)
|
||||
{
|
||||
if (currentItem != null && currentItem.Data != null)
|
||||
{
|
||||
// 드래그 중에는 툴팁 안띄움
|
||||
if (eventData.dragging)
|
||||
{
|
||||
_highlightBox.gameObject.SetActive(false);
|
||||
GameManager.Instance.InGameUI.TooltipUI.HideTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
_highlightBox.gameObject.SetActive(true);
|
||||
GameManager.Instance.InGameUI.TooltipUI.ShowTooltip(currentItem, GetComponent<RectTransform>());
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public void OnPointerMove(PointerEventData eventData)
|
||||
{
|
||||
if (currentItem != null && currentItem.Data != null)
|
||||
{
|
||||
// 드래그 중이거나, 현재 마우스 위치를 로컬 좌표로 변환해서 사각형 안에 있지 않으면
|
||||
if (eventData.dragging || !RectTransformUtility.RectangleContainsScreenPoint(_rectTransform, eventData.position, eventData.pressEventCamera))
|
||||
{
|
||||
_highlightBox.gameObject.SetActive(false);
|
||||
GameManager.Instance.InGameUI.TooltipUI.HideTooltip();
|
||||
return;
|
||||
}
|
||||
|
||||
_highlightBox.gameObject.SetActive(true);
|
||||
GameManager.Instance.InGameUI.TooltipUI.ShowTooltip(currentItem, GetComponent<RectTransform>());
|
||||
}
|
||||
}
|
||||
|
||||
public void OnPointerExit(PointerEventData eventData)
|
||||
{
|
||||
_highlightBox.gameObject.SetActive(false);
|
||||
GameManager.Instance.InGameUI.TooltipUI.HideTooltip();
|
||||
}
|
||||
|
||||
public void ClearSlot()
|
||||
{
|
||||
_iconImage.sprite = null;
|
||||
_iconImage.enabled = false;
|
||||
|
||||
_stackText.text = "";
|
||||
_stackText.enabled = false;
|
||||
|
||||
_rarityImage.sprite = _rarity_None_Sprite;
|
||||
//SlotBg.sprite = RarityImage.sprite;
|
||||
}
|
||||
|
||||
public void UpdateSlotUI()
|
||||
{
|
||||
if (currentItem == null)
|
||||
{
|
||||
ClearSlot();
|
||||
return;
|
||||
}
|
||||
|
||||
if (currentItem.Data.Icon != null)
|
||||
{
|
||||
_iconImage.sprite = currentItem.Data.Icon;
|
||||
_iconImage.enabled = true;
|
||||
}
|
||||
|
||||
if (currentItem.Data.IsStackable)
|
||||
_stackText.text = currentItem.CurrentStack.ToString();
|
||||
else
|
||||
_stackText.text = "";
|
||||
|
||||
_stackText.enabled = true;
|
||||
|
||||
switch (currentItem.Data.Rarity)
|
||||
{
|
||||
case 1:
|
||||
{
|
||||
_rarityImage.sprite = _rarity_1_Sprite;
|
||||
}
|
||||
break;
|
||||
case 2:
|
||||
{
|
||||
_rarityImage.sprite = _rarity_2_Sprite;
|
||||
}
|
||||
break;
|
||||
case 3:
|
||||
{
|
||||
_rarityImage.sprite = _rarity_3_Sprite;
|
||||
}
|
||||
break;
|
||||
case 4:
|
||||
{
|
||||
_rarityImage.sprite = _rarity_4_Sprite;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
{
|
||||
_rarityImage.sprite = _rarity_None_Sprite;
|
||||
}
|
||||
break;
|
||||
|
||||
}
|
||||
//SlotBg.sprite = _rarityImage.sprite;
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/UI/Inventory/InventorySlot.cs.meta
Normal file
2
Assets/02_Scripts/UI/Inventory/InventorySlot.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6168315e6b8be1443b23324f00913e8a
|
||||
74
Assets/02_Scripts/UI/Inventory/TooltipUI.cs
Normal file
74
Assets/02_Scripts/UI/Inventory/TooltipUI.cs
Normal file
@@ -0,0 +1,74 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using UnityEngine.UI;
|
||||
|
||||
public class TooltipUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private GameObject _tooltipWindow;
|
||||
[SerializeField] private TextMeshProUGUI _titleText;
|
||||
[SerializeField] private TextMeshProUGUI _descriptionText;
|
||||
[SerializeField] private TextMeshProUGUI _infoText; // 강화 수치나 타입 등
|
||||
[SerializeField] private Image _icon;
|
||||
|
||||
private RectTransform rectTransform;
|
||||
|
||||
private void Awake()
|
||||
{
|
||||
rectTransform = _tooltipWindow.GetComponent<RectTransform>();
|
||||
HideTooltip();
|
||||
}
|
||||
|
||||
private void Update()
|
||||
{
|
||||
|
||||
}
|
||||
|
||||
public void ShowTooltip(ItemInstance item, RectTransform slotRect)
|
||||
{
|
||||
_icon.sprite = item.Data.Icon;
|
||||
_titleText.text = item.Data.ItemName;
|
||||
_descriptionText.text = item.Data.Description;
|
||||
|
||||
//무기일 경우
|
||||
if (item.Data.ItemType == ItemType.EQUIPMENT)
|
||||
{
|
||||
_infoText.text =
|
||||
$"공격력 : 00\n방어력 : 00";
|
||||
}
|
||||
else if (item.Data.ItemType == ItemType.CONSUMABLE)
|
||||
{
|
||||
_infoText.text =
|
||||
$"회복량 : 00";
|
||||
}
|
||||
|
||||
|
||||
_tooltipWindow.SetActive(true);
|
||||
//UpdateToolTip(item); // 텍스트 갱신
|
||||
|
||||
// 레이아웃 강제 갱신 (텍스트 길이에 따라 변한 툴팁 크기를 즉시 반영)
|
||||
LayoutRebuilder.ForceRebuildLayoutImmediate(rectTransform);
|
||||
|
||||
// 기본 위치 설정 (슬롯의 오른쪽)
|
||||
Vector3[] slotCorners = new Vector3[4];
|
||||
slotRect.GetWorldCorners(slotCorners);
|
||||
|
||||
Vector3 targetPos = slotCorners[2];
|
||||
rectTransform.pivot = new Vector2(0f, 1f); // 툴팁 피벗을 좌상단으로
|
||||
|
||||
// 화면 잘림 검사
|
||||
float tooltipWidth = rectTransform.rect.width;
|
||||
// 툴팁의 우측 끝 좌표가 화면 너비보다 크면 왼쪽으로 뒤집기
|
||||
if (targetPos.x + tooltipWidth > Screen.width)
|
||||
{
|
||||
targetPos = slotCorners[1]; // 슬롯의 좌측 상단 모서리
|
||||
rectTransform.pivot = new Vector2(1f, 1f); // 툴팁 피벗을 우상단으로 변경
|
||||
}
|
||||
|
||||
rectTransform.position = targetPos;
|
||||
}
|
||||
|
||||
public void HideTooltip()
|
||||
{
|
||||
_tooltipWindow.SetActive(false);
|
||||
}
|
||||
}
|
||||
2
Assets/02_Scripts/UI/Inventory/TooltipUI.cs.meta
Normal file
2
Assets/02_Scripts/UI/Inventory/TooltipUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6ce182b987390a44192add2739f07f27
|
||||
37
Assets/02_Scripts/UI/_Shared/SplitWindowUI.cs
Normal file
37
Assets/02_Scripts/UI/_Shared/SplitWindowUI.cs
Normal file
@@ -0,0 +1,37 @@
|
||||
using UnityEngine;
|
||||
using TMPro;
|
||||
using System;
|
||||
|
||||
public class SplitWindowUI : MonoBehaviour
|
||||
{
|
||||
[SerializeField] private TMP_InputField _inputField;
|
||||
private int _maxAmount;
|
||||
private Action<int> _onConfirm;
|
||||
|
||||
private void Start()
|
||||
{
|
||||
gameObject.SetActive(false);
|
||||
}
|
||||
|
||||
public void Open(int max, Action<int> onConfirm)
|
||||
{
|
||||
gameObject.SetActive(true);
|
||||
_maxAmount = max;
|
||||
_onConfirm = onConfirm;
|
||||
_inputField.text = "1"; // 기본값
|
||||
_inputField.ActivateInputField();
|
||||
}
|
||||
|
||||
public void OnClickConfirm()
|
||||
{
|
||||
if (int.TryParse(_inputField.text, out int amount))
|
||||
{
|
||||
// 1보다 작으면 1로, 최대치보다 크면 최대치로 보정
|
||||
amount = Mathf.Clamp(amount, 1, _maxAmount);
|
||||
_onConfirm?.Invoke(amount);
|
||||
}
|
||||
Close();
|
||||
}
|
||||
|
||||
public void Close() => gameObject.SetActive(false);
|
||||
}
|
||||
2
Assets/02_Scripts/UI/_Shared/SplitWindowUI.cs.meta
Normal file
2
Assets/02_Scripts/UI/_Shared/SplitWindowUI.cs.meta
Normal file
@@ -0,0 +1,2 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88c61d650f9274141b4c26634a0d55cf
|
||||
Reference in New Issue
Block a user