[Unity UI] 3D 아이템 및 인벤토리 구현 리팩토링 -Refactoring-

새로 쓰게 된 이유

전에 쓴 글을 보고 나중에 저 글을 보면서 구현하라고 하면 과연 똑같이 할 수 있을까? 란 생각과 그 방식으로 구현하게되면 추후에 더 많은 기능들을 구현했을때 다른 오브젝트들과 상호작용에 있어서 제대로 돌아갈 수 있을까? 란 생각이 많이 들었다.

프로젝트 코드이기도 했고, 내가 생각했던 인벤토리 형식이 아니였어서 이번엔 완전히 RPG게임의 인벤토리와 같은 기능을 구현해보았고, 나중에도 보고 따라 만들 수 있도록 상세하게 쓰고싶었다.

 

나는 로스트아크의 인벤토리를 참고해서 얼추 만들어보았다.


우선 내가 생각한 인벤토리이다.

다른 사이트에서 참고용으로 가져옴.

Panel이 총 3개(원정대, 개인, 소지품)으로 있고, 개인창고와 소지품은 또다시 2개의 Panel이 들어간다.

첫번째로는 위에 소지품 텍스트가 들어가있는 Panel과 2번째는 아이템슬롯들에 구성된 Panel이다.

왜 그런지는 지금당장 알 필요 없다. 굳이 똑같이 만들려 하지도않았지만 이걸 참고해서 구현하는것이 목표다.

1. UI의 구성

처음 Panel을 만들려 하면 Canvas가 생기고 그 밑에 Panel이 생기게 된다. 이때 Panel의 Inspector에서 image에 Color에서 투명도인 A값을 0으로 해준다.

이래야 화면이 안뿌옇다.

 

그 뒤에 Panel을 하나 더 만들어준다.

나는 해상도를 1920x1080크기로 해서 크기가 다소 크다. 저게 인게임에서의 크기다 보니 저기에 맞춰서 만들면 된다.

인벤토리로 만들 Panel의 크기를 600에 600으로 해주었다. 그리고 저 판넬에는 인벤토리의 배경으로 쓸만한 이미지를 넣어주면된다. 굳이 이미지보단 색상으로만 해도된다. 어차피 자식 오브젝트들이 가려버린다.


2. UpperTooltip

다시 Panel을 하나 더 오브젝트의 자식으로 넣어 만들고, Rect Transform으로 가로길이는 Shift를 누른채로 가로넓이를 부모와 맞춤으로 설정하고 높이만 적당히 80으로 해둔다. 그리고 text를 넣어서 Inventory라는 글을 넣어주었다.

 

우리는 저 인벤토리 창을 마우스로 드래그 할 수 있게 할것이다.

저 UpperTooltip에 스크립트를 추가해서 밑에 코드를 적용시켜주자.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.EventSystems;

public class PanelControl: MonoBehaviour, IPointerDownHandler, IDragHandler
{
    private Transform TargetPanel;

    private Vector2 StartPoint;
    private Vector2 EndPoint;

    void Awake()
    {
        if(TargetPanel == null)
        {
            TargetPanel = transform.parent;
        }
    }

    // 드래그 시작 위치 지정
    void IPointerDownHandler.OnPointerDown(PointerEventData eventData)
    {
        StartPoint = TargetPanel.position;
        EndPoint = eventData.position;
    }

    // 드래그 : 마우스 커서 위치로 이동
    void IDragHandler.OnDrag(PointerEventData eventData)
    {
        TargetPanel.position = StartPoint + (eventData.position - EndPoint);
    }

}

나도 구글링으로 크롤링 해온 코드이다.

IpointDownHandler와 IDragHandler를 이용해서 마우스 클릭과 움직임을 감지하게 해서 부모 판넬자체를 마우스의 위치에 따라 움직이게 하는 코드이다. 부모오브젝트를 움직이면 자식오브젝트도 따라올테니 말이다.

 

이 코드를 적용시켰다면 아마 Inventory자체의 Panel은 UpperTooltip으로 마우스를 클릭해서 움직일때 따라 움직일것이다.


3. InventorySlotsPanel

두번째 Panel을 생성해서 이름을 InventorySlotsPanel이라고 바꾸어준다. 이 곳이 아이템 슬롯들이 들어갈 판넬이다.

위치는 중앙에서 -40밑으로 위치하게 하고 세로길이를 400으로 해준다. 여기에 이미지를 넣거나 색상으로 그럴싸하게 만들어주면 된다.


3. Slot(슬롯) 오브젝트

여기가 핵심이다. 여기엔 아이템이 들어갈 슬롯이다.

내가 미리 만들어본 Slot오브젝트이다. 하나씩 설명해 주겠다.

1. Slot오브젝트 : Image

2. ItemImg : Image이고, 아이템의 이미지가 들어갈 곳이다.

3. MouseEffect : Image이고, 마우스를 올릴때 그 슬롯에 일어날 이펙트가 들어갈 이미지를 넣는 오브젝트다.

4. CountText : Text이고, 아이템의 갯수를 표시해준다.

 

우선 Slot스크립트를 생성하고 밑에 코드를 넣어주자.

Slot 스크립트

using System.Collections;
using System.Collections.Generic;
using System.Text;
using TMPro;
using UnityEditorInternal.Profiling.Memory.Experimental;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;

public class Slot : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler,
    IBeginDragHandler ,IDragHandler, IEndDragHandler, IDropHandler
{
    public Item item; //슬롯에 들어와있는 아이템
    [SerializeField]private Image slot_Item_Image; //아이템 이미지
    [SerializeField] private Image Event_Image; //마우스 이펙트
    [SerializeField] private TMP_Text slot_Item_Count_TXT; //아이템 갯수
    private bool isIn = false; //아이템이 들어있는지 여부

   
    void Awake()
    {
        canvas = FindObjectOfType<Canvas>(); // Assuming there's only one canvas in the scene
        draggingPlane = canvas.transform as RectTransform;
        slot_Item_Count_TXT.text = "";
    }

    public void SetSlot(Item setitem, bool is_New_Item) //인벤토리에 들어오면 
    {
        if (setitem != null)
        {
            get_Item(setitem);
            if (is_New_Item) // 새 아이템일 경우
            {
                setitem.add_Item_Count(1); // 아이템 개수를 1로 설정
            }
            Slot_Update(setitem);
        }
        Update_Count_text();
    }

    private void get_Item(Item get_item) //아이템 습득시 아이템 스크립트에 적용될 코드
    {
        get_item.set_isGet(true);
    }
    private void Slot_Update(Item update_Item)
    {
        SetColor(1f, slot_Item_Image);
        this.slot_Item_Image.sprite = update_Item.getSprite(); ;
        this.isIn = true;
        this.item = update_Item;
    }
    private void Clear_Slot() //슬롯 초기화
    {
        SetColor(0f, slot_Item_Image);
        this.item = null;
        this.slot_Item_Image.sprite = null;
        this.isIn = false;
        Update_Count_text();
    }

    public void Update_Count_text() //아이템 갯수 업데이트
    {
        if(item != null && item.get_Item_Count() > 0){
            slot_Item_Count_TXT.text = item.get_Item_Count().ToString();
            Debug.Log(item.get_Item_Count());
        }
        else
        {
            slot_Item_Count_TXT.text = "";
        }
    }

    public void addCount(int count)
    {
        if (item != null)
        {
            item.add_Item_Count(count);
            Update_Count_text();
        }
    }

    private void SetColor(float alpha, Image setImage)
    {
        Color color = setImage.color;
        color.a = alpha;
        setImage.color = color;
    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        SetColor(1f, Event_Image);
    }
    

    public void OnPointerExit(PointerEventData eventData)
    {
        SetColor(0f, Event_Image);
    }


    

    private Canvas canvas; //캔버스
    private GameObject draggingIcon; //아이템을 드래그할때 생길 오브젝트
    private RectTransform draggingPlane; //드래그 위치

    public void OnBeginDrag(PointerEventData eventData)
    {
        if (item != null && slot_Item_Image.sprite != null)
        {
            // Create a temporary icon to follow the cursor
            draggingIcon = new GameObject("dragging icon");
            draggingIcon.transform.SetParent(canvas.transform, false);
            draggingIcon.transform.SetAsLastSibling();
            var image = draggingIcon.AddComponent<Image>();
            //var rectTransform = draggingIcon.GetComponent<RectTransform>();
            // Set the temporary icon's sprite to the current item's sprite
            image.sprite = slot_Item_Image.sprite;
            image.SetNativeSize();
            // Prevent the dragging icon from blocking raycasts
            image.raycastTarget = false;

            // 슬롯의 RectTransform 크기를 가져와서 드래그 중인 이미지의 크기로 설정
            RectTransform slotRectTransform = GetComponent<RectTransform>();
            RectTransform imageRectTransform = image.GetComponent<RectTransform>();
            imageRectTransform.sizeDelta = slotRectTransform.sizeDelta; // 슬롯의 크기와 비슷하게 설정

            slot_Item_Image.color = new Color(slot_Item_Image.color.r, slot_Item_Image.color.g, slot_Item_Image.color.b, 0.5f); // Make the original image semi-transparent

            SetDraggedPosition(eventData);
        }
    }

    public void OnDrag(PointerEventData eventData) //드래그 이벤트 바로 밑에 함수 실행
    {
        if (draggingIcon != null)
        {
            SetDraggedPosition(eventData);
        }
    }

    private void SetDraggedPosition(PointerEventData eventData) //아이템이 드래그위치를 따라오게
    {
        if (eventData.pointerEnter != null && draggingPlane != null)
        {
            Vector3 globalMousePos;
            if (RectTransformUtility.ScreenPointToWorldPointInRectangle(draggingPlane, eventData.position, eventData.pressEventCamera, out globalMousePos))
            {
                draggingIcon.transform.position = globalMousePos;
            }
        }
    }

    public void OnEndDrag(PointerEventData eventData) //드래그가 끝나고 놓앗을때(아이템 위치 변경시)
    {
        if (draggingIcon != null)
        {
            Destroy(draggingIcon); // 드래그 아이콘 제거
        }

        // 슬롯에 아이템이 남아있으면 이미지의 투명도를 원래대로 복원
        if (item != null)
        {
            slot_Item_Image.color = new Color(slot_Item_Image.color.r, slot_Item_Image.color.g, slot_Item_Image.color.b, 1f);
        }
    }

    public void OnDrop(PointerEventData eventData) //아이템을 슬롯위에 놓았을때
    {
        if (eventData.pointerDrag != null) //드래그된 오브젝트가 널이 아니면 실행
        {
  
            //드래그된 슬롯
            Slot draggedSlot = eventData.pointerDrag.GetComponent<Slot>();
            // 드래그된 슬롯과 드롭받는 슬롯이 같다면, 아무것도 하지 않고 리턴
            if (draggedSlot == this)
            {
                return;
            }
            if (draggedSlot != null && draggedSlot.item != null) //슬롯안에 아이템이 있으면
            {
                // 드롭받는 슬롯이 비어있는 경우, 아이템을 여기로 이동
                if (this.item == null)
                {
                    this.Slot_Update(draggedSlot.item); // 새 슬롯에 아이템 설정
                    draggedSlot.Clear_Slot(); // 원래 슬롯은 클리어
                }
                else if (this.item != null && this.item.isSameItem(draggedSlot.item)) //같은 아이템을 합칠때
                {
                    this.item.add_Item_Count(draggedSlot.item.get_Item_Count()); // 수량 합치기
                    Destroy(draggedSlot.item.gameObject); // 드래그된 아이템 파괴
                    draggedSlot.Clear_Slot(); // 원래 슬롯의 아이템 제거
                }
                else
                {
                    // 두 슬롯 모두 비어있지 않은 경우, 아이템 교환
                    Item tempItem = this.item;
                    int tempCount = this.item.get_Item_Count(); // 현재 슬롯의 아이템 수량을 임시 저장

                    this.Slot_Update(draggedSlot.item); // 교환되는 아이템 설정
                    this.item.set_Item_Count(draggedSlot.item.get_Item_Count()); // 교환되는 아이템의 수량 설정

                    draggedSlot.Slot_Update(tempItem); // 원래 슬롯의 아이템을 교환 슬롯에 설정
                    draggedSlot.item.set_Item_Count(tempCount); // 원래 슬롯의 아이템 수량을 교환 슬롯에 설정

                    // 각 슬롯의 텍스트 업데이트
                    this.Update_Count_text();
                    draggedSlot.Update_Count_text();
                }
                Update_Count_text(); // 갯수 업데이트
            }
        }
    }

    public void SetSlotItem(Item setitem) { item = setitem; }
    public void setSlotImage(Image SlotImage) { slot_Item_Image = SlotImage; }
    public void Set_isIn(bool IsItem) { isIn = IsItem; }
    public void set_Count_txt(int count) { slot_Item_Count_TXT.text = count.ToString(); }
    public bool isin() { return isIn; }
    public Item getItem() { return item; }
}

정말 길다.

나도 만들면서 당황했다. 인벤토리에 슬롯하나에 이렇게 긴 코드를 필요로 하게 될 줄...

이곳에 기능들을 설명해주자면

Slot에 아이템이 들어오면 어떤 아이템이 들어왔는지 확인하고, 정보가 바뀔때마다 업데이트를 해주며, 해당 아이템을 다른 슬롯으로 옮기거나 아이템의 위치를 맞교환 하는 기능들을 넣어준것이다.

만들면서 별에 별 버그가 있었지만 지금은 완전한 기능을 수행하고있다.

 

이렇게 코드를 Slot에 넣어주면 저 [SerializeField]로 새준 곳에 아까 Slot의 자식오브젝트들을 넣어준다.

아이템은 원래 private인데 확인차 public으로 해둔것이다. 신경쓰지말자

Slot_Item_Image에 ItemImg인 오브젝트를, Event_Image에는 MouseEffect 오브젝트를, Slot_Item_Count_TXT에는 CoutText오브젝트를 각각 넣어준다.

그 밑에 Image에는 이미지를 넣거나 색상으로 꾸며도 된다.

 

다음으로는 자식오브젝트들에 대한 부가설명이다.


4. Slot의 자식오브젝트 설정

1. ItemImg설정

ItemImg

ItemImg오브젝트는 초기에 투명도를 0으로 해준다.

이유는 자식오브젝트가 부모 오브젝트보다 시각적으로 봤을때 앞으로 나와있어서 자식오브젝트가 부모오브젝트를 뒤덮는 크기였을때 색상이나 이미지를 넣게되면, 부모오브젝트에 어떤 이미지나 색상이 들어와도 그 부모는 보이지 않는다.

그래서 투명도를 0으로 하여 부모 오브젝트의 이미지를 보이게 해준다.

2. MouseEffect설정

MouseEffect

MouseEffect도 위와 같은 이유로 투명도를 0으로 해준다.

여기에는 Source Image에 이펙트를 넣어주는걸 추천한다. 색상만으로 이펙트를 하기엔 너무 볼품없기때문에 이펙트에 맞는 이미지를 따로 구해서 넣어주자.

3. CountText 설정

Text는 슬롯의 오른쪽위에 위치하게 하여 그럴싸하게만 해주면 된다. 별거 없다.

 

이렇게 해주었으면 Slot은 끝이다.

다 만든 Slot을 Project에 끌어 넣어주면 알아서 prefab으로 된다.

 


5. InventorySlotsPanel 설정

InventorySlotsPanel

오브젝트 이름이 다르지만 똑같은거다.

슬롯의 부모오브젝트인 InventorySlotsPanel에 Add Component로 Grid Layout Group을 추가해준다.

이는 자식오브젝트들이 일정한 크기와 간격으로 오와열을 잘 맞춰주도록 해주는 기능이다.

 

1. Padding은 간격으로 부모오브젝트의 위,아래,양옆으로부터 얼마나 간격을 둘것인지 설정한다.

2. CellSize는 자식 오브젝트들이 X(가로), Y(세로)의 크기를 통일시키기 위함으로써 자식오브젝트들의 크기를 정해준다.

3. Spacing은 자식오브젝트간에 간격이다. padding과 헷갈리지말자.

Start Corner로 왼쪽 위부터 시작하도록 지정해주고 Start Axis는 가로로 추가되도록 해준다. 이렇게만 해주면 된다.

 

그 뒤에 아까 만든 Slot Prefab(프리팹)을 InventorySlotsPanel자식에 충분히 추가해주자.

Slot오브젝트를 Ctrl+C , V 해주면 이렇게 이름옆에 (1) (2) 하면서 추가되는데 GridLayout덕에 일정하게 Panel안에 Slot들이 추가되는걸 볼 수 있다.

이렇게 슬롯이 일정하게 추가될것이다.


6. Inventory 판넬과 스크립트

마지막이다.

우선 스크립트를 만들어서 밑에 코드를 InventoryPanel에 추가해주자.

Inventory 스크립트

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using UnityEngine.EventSystems;

public class Inventory : MonoBehaviour
{
    public Slot[] itemSlots;
    public static Inventory instance;

    public void AddItem(Item item)
    {
        if (itemSlots != null)
        {
            // Step 1: Check for an existing item of the same type
            for (int i = 0; i < itemSlots.Length; i++)
            {
                if (itemSlots[i].isin() && itemSlots[i].getItem().getItemName() == item.getItemName())
                {
                    itemSlots[i].item.add_Item_Count(1); // Increase item count
                    itemSlots[i].Update_Count_text(); // Update the count display
                    Debug.Log("Increased count of " + item.getItemName() + " in slot " + i);
                    return; // Item added, exit the method
                }
            }

            // Step 2: If no existing item found, add to the first empty slot
            for (int i = 0; i < itemSlots.Length; i++)
            {
                if (!itemSlots[i].isin())
                {
                    Item newItem = Instantiate(item);

                    // Add the new item instance to the inventory slot
                    itemSlots[i].SetSlot(newItem,true);

                    Debug.Log("New item added to slot " + i);
                    return; // Exit the method after adding the new item
                }
            }

            // If no empty slot found
            Debug.Log("Inventory is full");
        }
    }
}

아이템을 습득했을때 해당 아이템을 슬롯들을 순회하여 찾는다. 해당 아이템이 있으면 그곳에 갯수만 추가해주면 되고, 없으면 빈 슬롯에 추가해주도록 해주었다.

 

이걸 InventoryPanel에 넣어주고 Inspector를 확인한다.

인벤토리 Inspector

스크립트를 추가하면 이런 모습으로 나올것이다. 아마 Element가 처음엔 추가가 안되어 있을텐데 옆에 추가시킨 슬롯의 갯수대로 입력해주거나 밑에 + 버튼을 통해서 Element들을 늘리고 거기에 열심히 복붙했던 Slot들을 순서대로 넣어주면 된다.

이러면 인벤토리 완성이다.


7. 완성

테스트 결과

 

사과에 마우스를 올린 상태라 MouseEffect 이미지가 활성화 되어 있는 것이고, 저 도끼는 소지품 인벤토리에서 드래그로 옮긴 것이다. 위에 처럼 만든 인벤토리를 또 복붙해서 만들어주면 다른 창고의 슬롯에도 아이템을 넣어줄 수 있는 효과를 낼 수 있다.

 

 

아이템은 따로 만들어주어야 할 것이다. 따라 만들어보고 싶다면 밑에 아이템 코드를 복붙해서 만들어보길 바란다.

아이템 스크립트.

using System.Collections;
using System.Collections.Generic;
using Microsoft.Unity.VisualStudio.Editor;
using UnityEngine;
using UnityEngine.UI;
using Image = UnityEngine.UI.Image;

public class Item : MonoBehaviour
{   
    //Child Obejcts same
    protected int ItemNumber;
    protected string ItemName;
    protected Image ItemImage;
    protected Sprite ItemSprite;
    protected int get_Count=0;
    protected bool isget = false;


    public int getItemNumber()
    {
        return this.ItemNumber;
    }
    public string getItemName()
    {
        return this.ItemName;
    }
    public int get_Item_Count()
    {
        return this.get_Count;
    }
    public void set_Item_Count(int get_count)
    {

        this.get_Count = get_count;
    }
    public void add_Item_Count(int add_count)
    {
        this.get_Count += add_count;
    }
    public bool isSameItem(Item otherItem)
    {
        return this.ItemNumber == otherItem.getItemNumber();
    }
    public void set_isGet(bool isget) { this.isget = isget; }
    public bool get_isGet() { return this.isget; }
    public Sprite getSprite() { return this.ItemSprite; }
    public Image getImage() { return this.ItemImage; }

    public void setSprite(Sprite itemSprite) {  this.ItemSprite = itemSprite; }
}

그저 아이템일 뿐이고 상세 구현은 상속시켜서 구현하면된다.

 

 

 

 

이상으로 인벤토리를 구현해내었고 궁금한 점은 댓글을 통해 해주세요.