Inventory System (2)

JJW·2024년 12월 11일
0

Unity

목록 보기
15/34

어제 설명했던 Inventory System (1)에 이어지는 내용입니다.
이번 글에서 구현할 건 인벤토리에 대한 내용입니다.


Inventory System


인벤토리 구현에 필요한 정보

  • 구현에 대해 필요한 정보를 먼저 설명하겠습니다.
  • MVP 패턴의 숙달을 위해 MVP 패턴으로 인벤토리를 구현합니다.
  • Model, View, Presenter의 역할을 구분하여 구현합니다.
  • Model은 데이터 및 인벤토리의 아이템을 추가하거나 제거하는 역할을 담당합니다.
  • View는 UI(슬롯 및 인벤토리)의 데이터를 관리합니다.
  • Presenter는 View (사용자 상호작용) -> Model -> Presenter -> View의 흐름을 가지도록 구현합니다.
  • Slot은 Slot의 데이터를 관리 및 UI 갱신 사용자 클릭 이벤트를 처리합니다.
  • Slot은 스크롤을 구현하지 않고 슬롯의 개수 추가또한 존재하지 않습니다.
  • 정렬의 기능을 구현합니다. (이름 순, 개수 순, 타입 순)
  • 아이템 상세정보 창을 구현할 때 버튼 기능은 없으며 정보만 표시합니다.
  • 아이템을 장착할 때에는 Drag Drop으로 장착,해제가 가능하도록 구현합니다.

구현

1. InventoryModel

public class InventoryItemData
{
    public ItemData Item { get; set; }
    public int Quantity { get; set; }

    public InventoryItemData(ItemData item, int quantity)
    {
        Item = item;
        Quantity = quantity;
    }

    public InventoryItemData Clear()
    {
        Item = null;
        Quantity = 0;

        return this;
    }
}

public class InventoryModel
{
    public List<InventoryItemData> Items { get; private set; } = new List<InventoryItemData>();
    public int MaxSlots { get; private set; } = 20;

    public InventoryModel(int maxSlots)
    {
        MaxSlots = maxSlots;

        // 빈 슬롯 초기화
        for (int i = 0; i < maxSlots; i++)
        {
            // 초기 상태는 아이템이 없는 빈 슬롯 상태
            Items.Add(new InventoryItemData(null, 0));
        }
    }

    // 인벤토리 아이템 추가 함수
    public bool AddItem(ItemData item, int quantity = 1)
    {
        // 중첩 가능한 아이템 처리
        if (item.IsStackable)
        {
            // 이미 존재하는 아이템 찾기
            var existingItem = Items.Find(entry => entry.Item == item);
            if (existingItem != null && existingItem.Item != null)
            {
                int newQuantity = existingItem.Quantity + quantity;

                if (newQuantity > item.MaxStackSize)
                {
                    // 초과분 계산
                    int remainingQuantity = newQuantity - item.MaxStackSize;
                    existingItem.Quantity = item.MaxStackSize;

                    // 빈 슬롯에 초과분 추가
                    var emptySlot = Items.Find(entry => entry.Item == null);
                    if (emptySlot != null)
                    {
                        emptySlot.Item = item;
                        emptySlot.Quantity = remainingQuantity;
                        return true;
                    }
                    return false; // 빈 슬롯이 없음
                }

                // 중첩 가능한 경우 수량만 업데이트
                existingItem.Quantity = newQuantity;
                return true;
            }
        }

        // 빈 슬롯 찾기
        var slot = Items.Find(entry => entry.Item == null);
        if (slot != null)
        {
            // 빈 슬롯에 데이터 설정
            slot.Item = item;
            slot.Quantity = quantity;
            return true;
        }

        return false; // 빈 슬롯이 없음
    }

    // 인벤토리 아이템 삭제 함수
    public void RemoveItem(ItemData item, int quantity = 1)
    {
        var existingItem = Items.Find(entry => entry.Item == item);

        if (existingItem != null && existingItem.Item != null)
        {
            if (existingItem.Quantity > quantity)
            {
                existingItem.Quantity -= quantity;
            }
            else
            {
                // 슬롯 초기화
                existingItem.Item = null;
                existingItem.Quantity = 0;
            }
        }
    }

    // Drag Drop으로 인한 아이템 위치 변경 함수
    public void SwapItems(int dragItemIndex, int dropItemIndex)
    {
        // 0 이하가 아니고 슬롯의 최대 개수를 넘지 않았다면
        if (dragItemIndex >= 0 && dragItemIndex < Items.Count && dropItemIndex >= 0 && dropItemIndex < Items.Count)
        {
            var temp = Items[dropItemIndex];
            Items[dropItemIndex] = Items[dragItemIndex];
            Items[dragItemIndex] = temp;
        }
    }

    // Items 리스트 반환
    public List<InventoryItemData> GetItems()
    {
        return Items;
    }
}
  • 데이터와 아이템의 Add, Remove, Swap 을 구현하였습니다.
  • 생성자 함수로 MaxSlots의 개수를 설정할 수 있게끔 하였습니다.

2. InventoryView

public class InventoryView : MonoBehaviour
{
    [SerializeField] private int slotSize;                  // 슬롯의 개수

    private InventoryPresenter _presenter;
    [SerializeField] private List<SlotData> _slots = new List<SlotData>();   // 슬롯 리스트

    // 테스트 변수
    [SerializeField] private ItemData _item;
    [SerializeField] private int _quantity;

    // 테스트 함수
    public void OnClickAddItem()
    {
        _presenter.AddItem(_item, _quantity);
    }

    public void OnClickRemoveItem()
    {
        _presenter.RemoveItem(_item, _quantity);
    }

    private void Start()
    {
        // Model과 Presenter 초기화
        var model = new InventoryModel(slotSize);
        _presenter = new InventoryPresenter(model, this);

        InitSlots();
    }

    private void InitSlots()
    {
        for (int i = 0; i < slotSize; i++)
        {
            _slots[i].Initialize(i, _presenter);
        }
    }

    public void RefreshInventory(List<InventoryItemData> items)
    {
        for (int i = 0; i < _slots.Count; i++)
        {
            if (i < items.Count)
            {
                _slots[i].SetItem(items[i]);
            }
            else
            {
                _slots[i].SetItem(items[i].Clear());
            }
        }
    }

    public void ShowErrorMessage(string message) => Debug.LogError(message);
}
  • UI를 관리하는 클래스입니다.
  • 별 다른 정보 없이 간단하게 Model 과 Presenter를 가지고 있으며 Slot과 UI의 갱신을 담당합니다.
  • 테스트용 코드로 Add,Remove의 테스트를 진행합니다. Swap의 경우에는 UnityEvent.Drag를 사용하기 때문에 Slot의 있습니다.

3. InventoryPresenter

public class InventoryPresenter
{
    private InventoryModel _model;
    private InventoryView _view;

    public InventoryPresenter(InventoryModel model, InventoryView view)
    {
        _model = model;
        _view = view;

        _view.RefreshInventory(_model.GetItems());
    }

    public void AddItem(ItemData item, int quantity = 1)
    {
        if (_model.AddItem(item, quantity))
        {
            _view.RefreshInventory(_model.GetItems());
        }
        else
        {
            _view.ShowErrorMessage("인벤토리 슬롯이 부족합니다.");
        }
    }

    public void RemoveItem(ItemData item, int quantity = 1)
    {
        _model.RemoveItem(item, quantity);
        _view.RefreshInventory(_model.GetItems());
    }

    public void SwapItems(int dragItemIndex, int dropItemIndex)
    {
        _model.SwapItems(dragItemIndex, dropItemIndex);
        _view.RefreshInventory(_model.GetItems());
    }
}
  • Model과 View를 중개해주는 클래스입니다.
  • 실제 아이템 추가 및 삭제 데이터 갱신 및 UI 갱신 요청은 Presenter를 통해 이루어집니다.

4. SlotData

public class SlotData : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler
{
    [SerializeField] private Image itemIcon;
    [SerializeField] private Text itemName;
    [SerializeField] private Text itemQuantity;
    [SerializeField] private GameObject slotInfo;

    private InventoryItemData _itemData;
    private int _index;
    private InventoryPresenter _presenter;

    private Vector3 initialPosition;              
    private RectTransform rectTransform;          
    private CanvasGroup canvasGroup;              

    private void Start()
    {
        rectTransform = slotInfo.GetComponent<RectTransform>();
        canvasGroup = slotInfo.GetComponent<CanvasGroup>();
    }

    public void Initialize(int index, InventoryPresenter presenter)
    {
        _index = index;
        _presenter = presenter;
    }

    public void SetItem(InventoryItemData item)
    {
        _itemData = item;
        if (item.Item != null)
        {
            slotInfo.SetActive(true);
            itemIcon.sprite = item.Item.Icon;
            itemName.text = item.Item.ItemName;
            itemQuantity.text = item.Quantity > 1 ? item.Quantity.ToString() : "";
        }
        else
        {
            slotInfo.SetActive(false);
            itemIcon.sprite = null;
            itemName.text = "";
            itemQuantity.text = "";
        }
    }

    public void OnBeginDrag(PointerEventData eventData)
    {
        if (_itemData?.Item != null)                        // 아이템이 있는 경우에만 드래그 가능
        {
            initialPosition = rectTransform.localPosition; // 현재 위치 저장
            canvasGroup.blocksRaycasts = false;            // 드래그 중 충돌 비활성화
            canvasGroup.alpha = 0.6f;                      // 투명도 조정
        }
    }

    public void OnDrag(PointerEventData eventData)
    {
        if (_itemData?.Item != null)
        {
            // 마우스 위치에 따라 SlotInfo 이동
            rectTransform.anchoredPosition += eventData.delta;
        }
    }

    public void OnEndDrag(PointerEventData eventData)
    {
        if (_itemData?.Item != null)
        {
            canvasGroup.blocksRaycasts = true;     // 충돌 복구
            canvasGroup.alpha = 1.0f;              // 투명도 복구

            // 드롭 위치 확인
            if (eventData.pointerEnter != null)
            {
                var targetSlot = eventData.pointerEnter.GetComponent<SlotData>();
                if (targetSlot != null && targetSlot != this)
                {
                    // Swap
                    _presenter.SwapItems(_index, targetSlot._index);
                }
            }

            // 드래그가 끝난 후 원래 위치로 복귀
            rectTransform.localPosition = initialPosition;
        }
    }
}
  • 슬롯의 데이터 및 UI를 관리하는 클래스입니다.
  • 슬롯의 Drag Drop을 구현하기 위해 IBeginDragHandler, IDragHandler, IEndDragHandler 을 상속받아서 사용합니다.
  • Begin에서 슬롯을 클릭하면 현재 위치와 연출을 위한 투명도를 조절합니다.

테스트 영상

  • 슬롯간의 이동은 잘 되지만 문제가 보이는게 이미지가 Slot에 가려지는 문제가 발생합니다.
  • 해당 문제는 Slot의 렌더링 순서와 관련이 있습니다.
    문제를 해결하기 위해 밑에 방법으로 수정하였습니다.

문제 해결

    public void OnBeginDrag(PointerEventData eventData)
    {
        if (_itemData?.Item != null)                        // 아이템이 있는 경우에만 드래그 가능
        {
            initialPosition = rectTransform.localPosition; // 현재 위치 저장
            canvasGroup.blocksRaycasts = false;            // 드래그 중 충돌 비활성화
            canvasGroup.alpha = 0.6f;                      // 투명도 조정
            canvas = slotInfo.AddComponent<Canvas>();
            canvas.overrideSorting = true;
            canvas.sortingOrder = 1;
        }
    }
    
    public void OnEndDrag(PointerEventData eventData)
    {
        if (_itemData?.Item != null)
        {
            canvasGroup.blocksRaycasts = true;     // 충돌 복구
            canvasGroup.alpha = 1.0f;              // 투명도 복구

            // 드롭 위치 확인
            if (eventData.pointerEnter != null)
            {
                var targetSlot = eventData.pointerEnter.GetComponent<SlotData>();
                if (targetSlot != null && targetSlot != this)
                {
                    // Swap
                    _presenter.SwapItems(_index, targetSlot._index);
                }
            }

            // 드래그가 끝난 후 원래 위치로 복귀
            rectTransform.localPosition = initialPosition;
            Destroy(canvas);
        }
    }

수정 후 테스트

  • Canvas를 동적으로 생성하여 overrideSorting 과 sortingOrder 의 값을 조절해주어서 해결하였습니다.
  • Slot의 구조를 잘못 구성했기 때문에 이렇게 DrawCalls을 증가시킬 수 밖에 없는 해결법이라 너무 찝찝하고 아쉽습니다...
  • 다른 방법이 생각이 나더라도 다시 짜야하기 때문에.. 다른 방식으로 구현하거나 해결하시기를 바랍니다.

아이템 삭제 및 기존 코드 수정

  • Remove가 눌렀을 때 삭제되는 것 보다는 기존의 인벤토리 레퍼런스를 위해 참고한 펠월드 처럼 Remove 버튼에 아이템을 Drag Drop으로 삭제시키는게 더 좋을 것 같습니다.
  • 삭제를 하고 나서 인벤토리의 UI 업데이트를 보면 앞에서부터 자기와 같은 아이템을 지워버려서 이상하게 UI가 업데이트 됩니다.
    이를 위해 Unique 값을 생성하여 해당 Unique값의 아이템을 지우도록 수정해야 합니다.
  • 해당 문제 수정 및 기능 추가를 위해 아래 방식을 따라주세요
public class InventoryItemData
{
    public ItemData Item { get; set; }
    public int Quantity { get; set; }

    public Guid UniqueID { get; set; } 

    public InventoryItemData(ItemData item, int quantity)
    {
        Item = item;
        Quantity = quantity;
        UniqueID = item == null ? Guid.Empty : Guid.NewGuid();
    }

    public InventoryItemData Clear()
    {
        Item = null;
        Quantity = 0;
        UniqueID = Guid.Empty;

        return this;
    }
}

public class InventoryModel
{
    public List<InventoryItemData> Items { get; private set; } = new List<InventoryItemData>();
    public int MaxSlots { get; private set; } = 20;

    public InventoryModel(int maxSlots)
    {
        MaxSlots = maxSlots;

        // 빈 슬롯 초기화
        for (int i = 0; i < maxSlots; i++)
        {
            // 초기 상태는 아이템이 없는 빈 슬롯 상태
            Items.Add(new InventoryItemData(null, 0));
        }
    }

    // 인벤토리 아이템 추가 함수
    public bool AddItem(InventoryItemData item)
    {
        if (item.Item.IsStackable)
        {
            // 중첩 가능한 아이템 찾기
            var existingItem = Items.Find(entry => entry.Item == item.Item && entry.Quantity < entry.Item.MaxStackSize);
            if (existingItem != null && existingItem.Item != null)
            {
                int newQuantity = existingItem.Quantity + item.Quantity;

                if (newQuantity > item.Item.MaxStackSize)
                {
                    int remainingQuantity = newQuantity - item.Item.MaxStackSize;
                    existingItem.Quantity = item.Item.MaxStackSize;

                    // 빈 슬롯에 초과분 추가
                    var emptySlot = Items.Find(entry => entry.Item == null);
                    if (emptySlot != null)
                    {
                        emptySlot.Item = item.Item;
                        emptySlot.Quantity = remainingQuantity;
                        emptySlot.UniqueID = Guid.NewGuid();
                        return true;
                    }
                    return false;
                }

                existingItem.Quantity = newQuantity;
                return true;
            }
            else
            {
                // 중첩 가능한 아이템이지만 인벤토리에 없는 경우
                var emptySlot = Items.Find(entry => entry.Item == null);
                if (emptySlot != null)
                {
                    emptySlot.Item = item.Item;
                    emptySlot.Quantity = item.Quantity;
                    emptySlot.UniqueID = Guid.NewGuid();
                    return true;
                }
            }
        }

        // 빈 슬롯 찾기
        var slot = Items.Find(entry => entry.Item == null);
        if (slot != null)
        {
            slot.Item = item.Item;
            slot.Quantity = item.Quantity;
            slot.UniqueID = Guid.NewGuid();
            return true;
        }

        return false;
    }

    // 인벤토리 아이템 삭제 함수
    public void RemoveItem(InventoryItemData item)
    {
        var existingItem = Items.Find(entry => entry.UniqueID == item.UniqueID);

        if (existingItem != null && existingItem.Item != null)
        {
            if (existingItem.Quantity > item.Quantity)
            {
                existingItem.Quantity -= item.Quantity;
            }
            else
            {
                existingItem.Clear();
            }
        }
    }

    // Drag Drop으로 인한 아이템 위치 변경 함수
    public void SwapItems(int dragItemIndex, int dropItemIndex)
    {
        if (dragItemIndex >= 0 && dragItemIndex < Items.Count &&
            dropItemIndex >= 0 && dropItemIndex < Items.Count)
        {
            var temp = Items[dropItemIndex];
            Items[dropItemIndex] = Items[dragItemIndex];
            Items[dragItemIndex] = temp;
        }
    }

    // Items 리스트 반환
    public List<InventoryItemData> GetItems()
    {
        return Items;
    }
}
  • 제일 변동사항이 많은 클래스입니다.
  • Unique 변수가 추가되었으며 Remove에 Unique를 사용하여 판단합니다.
  • Add시에는 Unique 값이 존재하지 않으면 추가해주고 중첩 가능 여부 부분이 수정되었습니다.
  • 나머지 클래스들은 변동사항에 맞게 수정해주시면 됩니다.

테스트 영상

  • 기존에 삭제시키면 Slot이 당겨지는 현상이 수정되었습니다.
  • 기존 코드에서는 중첩이 가능한 경우 한번만 체크하여 이미 20개가 채워지면 다음 부터는 1개씩 밖에 추가가 안되는 오류가 수정되었습니다.

정렬 구현

  • 타입 별 정렬을 구현합니다.
  • Unity List에서 제공하는 Sort를 사용하여 구현합니다.

public enum SortType
{
    ByName,        // 이름 순
    ByQuantity,    // 수량 순
    ByItemType,    // 아이템 타입 순
}
  • SortType Enum을 선언해줍니다.

    public void SortItems(SortType type)
    {
        Items.Sort((a, b) =>
        {
            // 빈 슬롯 처리: 빈 슬롯은 항상 뒤로
            if (a.Item == null && b.Item == null) return 0;
            if (a.Item == null) return 1;
            if (b.Item == null) return -1;

            switch (type)
            {
                case SortType.ByName:
                    // 이름 순 정렬
                    return string.Compare(a.Item.ItemName, b.Item.ItemName, StringComparison.Ordinal);

                case SortType.ByQuantity:
                    // 수량 순 정렬
                    return b.Quantity.CompareTo(a.Quantity);

                case SortType.ByItemType:
                    // 타입 순 정렬
                    return a.Item.ItemType.CompareTo(b.Item.ItemType);

                default:
                    return 0;
            }
        });
    }
  • InventoryModel 에 해당 코드를 추가해줍니다.
  • 수량 순 같은 경우에는 내림차 순 정렬이며 나머지는 오름차 순 정렬입니다.

    public void SortItems(SortType type)
    {
        _model.SortItems(type);
        _view.RefreshInventory(_model.GetItems());
    }
  • InventoryPrecenter 에 해당 코드를 추가해줍니다.

    public void OnClickSortType(int type)
    {
        _presenter.SortItems((SortType)type);
    }
  • InventoryView 에 해당 함수 추가한 이후 버튼의 이벤트에 연결해주시면 됩니다.

테스트 영상

  • 정렬은 잘 이루어지나 동일한 조건의 아이템의 경우에는 누를 때 마다 순서가 바뀝니다.
  • 해당 문제 수정을 위해 동일한 조건의 경우 이름 오름차 순 정렬을 적용하였습니다.

문제 해결

    public void SortItems(SortType type)
    {
        int quantityComparison = 0;

        Items.Sort((a, b) =>
        {
            // 빈 슬롯 처리: 빈 슬롯은 항상 뒤로
            if (a.Item == null && b.Item == null) return 0;
            if (a.Item == null) return 1;
            if (b.Item == null) return -1;

            switch (type)
            {
                case SortType.ByName:
                    // 이름 순 정렬
                    return string.Compare(a.Item.ItemName, b.Item.ItemName, StringComparison.Ordinal);

                case SortType.ByQuantity:
                    // 수량 순 정렬
                    quantityComparison = b.Quantity.CompareTo(a.Quantity);
                    if (quantityComparison == 0)
                    {
                        return string.Compare(a.Item.ItemName, b.Item.ItemName, StringComparison.Ordinal);
                    }
                    return quantityComparison;

                case SortType.ByItemType:
                    // 타입 순 정렬
                    quantityComparison = a.Item.ItemType.CompareTo(b.Item.ItemType);
                    if (quantityComparison == 0)
                    {
                        return string.Compare(a.Item.ItemName, b.Item.ItemName, StringComparison.Ordinal);
                    }
                    return quantityComparison;

                default:
                    return 0;
            }
        });
    }

수정 후 테스트

  • 누를 때 마다 순서가 바뀌는 오류가 수정된 모습을 볼 수 있습니다.

아이템 상세정보 창 구현

  • 아이템 클릭 이벤트와 상세정보 창을 구현합니다.

1. ItemDetailView

public class ItemDetailView : MonoBehaviour
{
  public static ItemDetailView Instance { get; private set; }

  [SerializeField] private GameObject detailPanel;        // 상세정보 창 Panel
  [SerializeField] private Image itemIcon;                // 아이템 아이콘
  [SerializeField] private Text itemType;                 // 아이템 이름
  [SerializeField] private Text itemName;                 // 아이템 이름
  [SerializeField] private Text itemDescription;          // 아이템 설명
  [SerializeField] private Text itemQuantity;             // 아이템 수량
  [SerializeField] private Text itemStat;                 // 아이템 수량

  [SerializeField] private RectTransform detailPanelRect; // 상세정보 창 RectTransform

  private void Awake()
  {
      if (Instance == null)
      {
          Instance = this;
      }
      else
      {
          Destroy(gameObject);
      }

      HideDetails();
  }

  public void ShowDetails(InventoryItemData itemData, RectTransform slotRect)
  {
      if (itemData == null || itemData.Item == null)
      {
          HideDetails();
          return;
      }

      // 상세정보 창 업데이트
      itemIcon.sprite = itemData.Item.Icon;
      itemName.text = itemData.Item.ItemName;
      itemType.text = GetItemType(itemData.Item.ItemType);
      itemDescription.text = itemData.Item.Description;
      itemQuantity.text = itemData.Quantity.ToString();
      itemStat.text = GetStat(itemData.Item.StatModifiers);

      InitPos(slotRect);

      detailPanel.SetActive(true);
  }

  public void HideDetails()
  {
      detailPanel.SetActive(false);
  }

  private void InitPos(RectTransform slotRect)
  {
      // 슬롯 기준으로 위치 설정
      Vector3[] slotCorners = new Vector3[4];
      slotRect.GetWorldCorners(slotCorners); // 슬롯의 월드 좌표 가져오기
      Vector3 slotTopRight = slotCorners[2]; // 우측 상단 코너

      // 상세정보 창 크기
      Vector2 panelSize = detailPanelRect.sizeDelta * detailPanelRect.lossyScale;

      // 위치 조정 (우측 상단에 배치, 화면 경계 처리)
      Vector2 adjustedPosition = AdjustPositionToScreenBounds(slotTopRight, panelSize);
      detailPanelRect.position = adjustedPosition;
  }

  private Vector2 AdjustPositionToScreenBounds(Vector3 targetPosition, Vector2 panelSize)
  {
      Vector2 screenBounds = new Vector2(Screen.width, Screen.height);

      // 화면 안에 맞추기
      float x = Mathf.Clamp(targetPosition.x + panelSize.x / 2, panelSize.x / 2, screenBounds.x - panelSize.x / 2);
      float y = Mathf.Clamp(targetPosition.y - panelSize.y / 2, panelSize.y / 2, screenBounds.y - panelSize.y / 2);

      return new Vector2(x, y);
  }

  private string GetItemType(ItemType type)
  {
      switch(type)
      {
          case ItemType.Equipment: return "장비";
          case ItemType.Food: return "음식";
          case ItemType.Etc: return "기타";
          case ItemType.Spacial: return "특별한 도구";
          default: return "";
      }
  }

  private string GetStatType(StatType type)
  {
      switch (type)
      {
          case StatType.Attack: return "공격력";
          case StatType.Defence: return "방어력";
          case StatType.WorkSpeed: return "작업 속도";
          case StatType.HP: return "체력";
          case StatType.Stamina: return "기력";
          case StatType.Weight: return "최대 중량";
          default: return "";
      }
  }

  private string GetStat(List<StatModifier> stats)
  {
      string strTemp = "";
      foreach(var pair in stats)
      {
          strTemp += $"{GetStatType(pair.TargetStat.StatType)} : {pair.ModifierValue} \n";
      }
      return strTemp;
  }    
}
  • ItemType과 StatType을 임시로 String으로 반환하게끔 구현하였습니다.
  • ItemData를 받아와 필요한 정보를 표시하고 Slot의 Rect값을 받아와 슬롯의 우측 상단 코너로 나오게끔 위치를 변경해줍니다.
  • 화면에 잘리지 않도록 AdjustPositionToScreenBounds()를 사용하여 구현하였습니다.

2. SlotData

public class SlotData : MonoBehaviour, IBeginDragHandler, IDragHandler, IEndDragHandler, IPointerClickHandler, IPointerExitHandler
{
  [SerializeField] private Image itemIcon;
  [SerializeField] private Text itemName;
  [SerializeField] private Text itemQuantity;
  [SerializeField] private GameObject slotInfo;

  public InventoryItemData _itemData;
  public int _index;
  private InventoryPresenter _presenter;

  private Vector3 initialPosition;              
  private RectTransform rectTransform;          
  private CanvasGroup canvasGroup;

  private Canvas canvas;

  private void Start()
  {
      rectTransform = slotInfo.GetComponent<RectTransform>();
      canvasGroup = slotInfo.GetComponent<CanvasGroup>();
  }

  public void Initialize(int index, InventoryPresenter presenter)
  {
      _index = index;
      _presenter = presenter;
  }

  public void SetItem(InventoryItemData item)
  {
      _itemData = item;
      if (item.Item != null)
      {
          slotInfo.SetActive(true);
          itemIcon.sprite = item.Item.Icon;
          itemName.text = item.Item.ItemName;
          itemQuantity.text = item.Quantity > 1 ? item.Quantity.ToString() : "";
      }
      else
      {
          slotInfo.SetActive(false);
          itemIcon.sprite = null;
          itemName.text = "";
          itemQuantity.text = "";
      }
  }

  public void OnPointerClick(PointerEventData eventData)
  {
      if (_itemData != null && _itemData.Item != null)
      {
          ItemDetailView.Instance.ShowDetails(_itemData, rectTransform);
      }
  }

  public void OnPointerExit(PointerEventData eventData)
  {
      if (ItemDetailView.Instance.gameObject.activeSelf)
      {
          ItemDetailView.Instance.HideDetails(); // 슬롯을 벗어나면 창 닫기
      }
  }

  public void OnBeginDrag(PointerEventData eventData)
  {
      if (_itemData?.Item != null)                        // 아이템이 있는 경우에만 드래그 가능
      {
          initialPosition = rectTransform.localPosition; // 현재 위치 저장
          canvasGroup.blocksRaycasts = false;            // 드래그 중 충돌 비활성화
          canvasGroup.alpha = 0.6f;                      // 투명도 조정
          canvas = slotInfo.AddComponent<Canvas>();
          canvas.overrideSorting = true;
          canvas.sortingOrder = 1;
      }
  }

  public void OnDrag(PointerEventData eventData)
  {
      if (_itemData?.Item != null)
      {
          // 마우스 위치에 따라 SlotInfo 이동
          rectTransform.anchoredPosition += eventData.delta;
      }
  }

  public void OnEndDrag(PointerEventData eventData)
  {
      if (_itemData?.Item != null)
      {
          canvasGroup.blocksRaycasts = true;     // 충돌 복구
          canvasGroup.alpha = 1.0f;              // 투명도 복구

          // 드롭 위치 확인
          if (eventData.pointerEnter != null)
          {
              var targetSlot = eventData.pointerEnter.GetComponent<SlotData>();
              if (targetSlot != null && targetSlot != this)
              {
                  // Swap
                  _presenter.SwapItems(_index, targetSlot._index);
              }
          }

          // 드래그가 끝난 후 원래 위치로 복귀
          rectTransform.localPosition = initialPosition;
          Destroy(canvas);
      }
  }

  public void Refresh()
  {
      canvasGroup.blocksRaycasts = true;     // 충돌 복구
      canvasGroup.alpha = 1.0f;              // 투명도 복구

      // 드래그가 끝난 후 원래 위치로 복귀
      rectTransform.localPosition = initialPosition;
      Destroy(canvas);
  }
}
  • IPointerClickHandler, IPointerExitHandler를 추가로 상속받아 사용했습니다.
  • Click 이벤트가 발생하면 ItemDetailView를 켜주고 슬롯에서 마우스가 벗어남을 감지하면 ItemDetailView를 꺼주도록 구현하였습니다.

테스트 영상

  • 정상적으로 상세정보 창이 노출이 되고 Item의 표시해야 할 정보가 잘 보입니다.

느낀 점

처음 계획했던 기능들에서 구현하다 이것 저것 추가하느라 글이 굉장히 지저분해진 것 같습니다..
그래도 필요한 기능들이나 MVP를 적용하여 Inventory를 구현하였으니 만족스럽긴 한데 잘 사용했는지에 대해서는 모르겠습니다.. 다음 번에는 계획을 좀 더 잘 짜야 이런 일이 없을 것 같다고 느꼈고 그래도 즐거운 시간이였습니다.

  • 제가 작성한 내용이 잘못 되었거나 더 좋은 방법이 있는 경우에는 댓글로 알려주시면 감사합니다 ! (´._.`)
profile
Unity 게임 개발자를 준비하는 취업준비생입니다..

1개의 댓글

comment-user-thumbnail
2025년 8월 11일

This post provides a solid overview of inventory system fundamentals, emphasizing the importance of accuracy, real-time updates, and efficient data handling to streamline warehouse operations. A well-designed inventory system not only reduces errors but also improves stock visibility and supports better decision-making.

For businesses looking to enhance these capabilities, investing in advanced software for warehouse inventory management can bring significant benefits. Such software typically offers features like seamless integration with other enterprise systems, automated tracking, customizable reporting, and scalability to adapt as your operations grow.

답글 달기