개발 낙서장

[Unity2D] 인벤토리 시스템 본문

Unity/Devlog

[Unity2D] 인벤토리 시스템

권승준 2023. 3. 24. 12:19

구현 동기

딱히 동기랄 것도 없다. 아이템이 없는 게임은 거의 없다!

기본 구조

스크립트는 크게 3개 정도로 구성했다.

  1. Item의 정보를 담는 클래스
  2. UI에 보여지는 아이템 슬롯 클래스
  3. 인벤토리 전체를 관리하는 클래스

가장 중요한 클래스는 인벤토리 전체를 관리하는 클래스이다.
여기서 인벤토리 관련 모든 동작(Save & Load, 정렬, 이동 등)이 이루어지기 때문에 짜임새 있게 작성해야 한다.

ItemSO

어차피 아이템의 고유 ID 혹은 이름으로 정보를 저장하고 불러올 것이기 때문에 정말 기본적인 정보들만 기입했다.

이 ItemSO는 나중에 파이어베이스의 파이어스토어에서 정보를 가져와 캐싱하는 스크립트가 될 것이다.

ItemSlot

public class ItemSlot : MonoBehaviour, IPointerClickHandler, IPointerEnterHandler, IPointerExitHandler
{
    [SerializeField] private ItemSO _item;
    public ItemSO Item
    {
        get { return _item; }
        set
        {
            _item = value;
            if (value != null)
                _slotImg.sprite = value._icon;
            else
                _slotImg.sprite = _emptyImg;
        }
    }

    public Image _slotImg;
    public Sprite _emptyImg;
    public Outline _outline;

    public bool IsEmpty
    {
        get { return Item == null; }
    }

    private void Start()
    {
        _outline.enabled = false;
    }

    public void OnPointerClick(PointerEventData eventData)
    {
        if (eventData.clickCount >= 2)
        {
            if (!IsEmpty)
                Debug.Log($"{Item._name} : {Item._description}");
        }

        if (InventoryManager.Instance.CurIndex == -1)
        {
            if (IsEmpty)
                return;
            InventoryManager.Instance.SelectItem(this);
        }
        else
        {
            InventoryManager.Instance.MoveItem(this);
        }

    }

    public void OnPointerEnter(PointerEventData eventData)
    {
        _outline.enabled = true;
    }

    public void OnPointerExit(PointerEventData eventData)
    {
        if (InventoryManager.Instance.CurIndex != -1 && InventoryManager.Instance._slots[InventoryManager.Instance.CurIndex] != this)
            _outline.enabled = false;
        else if (InventoryManager.Instance.CurIndex == -1)
            _outline.enabled = false;
    }
}

인벤토리의 슬롯 하나하나를 담당하는 클래스이다.
뒤에 나올 InventoryManager에서 슬롯의 유무나 아이템의 정보를 받기 위해 Item과 IsEmpty라는 프로퍼티를 선언해 코드 가독성을 좋게 했다.

여기도 역시 별다른 건 없고 그나마 있는 기능이라면 클릭에 따른 아이템 선택, 이동, 동작 기능이 있다.
아마 나중에 아이템도 타입에 따라 나눌 경우 여러가지 부분이 추가될 것이다.(소비 아이템과 장비 아이템의 동작이 다르듯이)

InventoryManager

public class InventoryManager : MonoBehaviour
{
    public static InventoryManager Instance;

    public ItemSlot _itemPrefab;

    public GameObject _slotPanel;
    public List<ItemSlot> _slots = new List<ItemSlot>();

    [SerializeField] int _curIndex;
    public int CurIndex
    {
        get { return _curIndex; }
        set { _curIndex = value; }
    }

    [SerializeField] List<ItemSO> cacheTest = new List<ItemSO>();
    Dictionary<string, ItemSO> _itemCache = new Dictionary<string, ItemSO>();

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

        DontDestroyOnLoad(this);

        CurIndex = -1;
    }

    public void AddItem(ItemSO item) {
        for(int i = 0; i < _slots.Count; i++)
        {
            if (_slots[i].IsEmpty)
            {
                _slots[i].Item = item;
                return;
            }
        }
    }

    public void RemoveItem(ItemSlot slot)
    {
        int index = _slots.IndexOf(slot);
        _slots[index].Item = null;
    }

    public void MoveItem(ItemSlot slot)
    {
        _slots[CurIndex]._outline.enabled = false;
        slot._outline.enabled = false;

        if (_slots[CurIndex] == slot)
        {
            CurIndex = -1;
            return;
        }

        if(slot.IsEmpty)
        {
            slot.Item = _slots[CurIndex].Item;
            _slots[CurIndex].Item = null;
        }
        else
        {
            ItemSO item = slot.Item;
            slot.Item = _slots[CurIndex].Item;
            _slots[CurIndex].Item = item;
        }

        CurIndex = -1;
    }

    public void SelectItem(ItemSlot slot)
    {
        CurIndex = _slots.IndexOf(slot);
        _slots[CurIndex]._outline.enabled = true;
    }

    public void SaveSlots()
    {
        Inventory inventory = new Inventory();

        foreach(ItemSlot slot in _slots)
        {
            if(slot.Item != null)
                inventory._itemArr.Add(slot.Item._name);
            else
                inventory._itemArr.Add("null");
        }

        string json = NewtonsoftJson.Instance.ObjectToJson(inventory);
        NewtonsoftJson.Instance.SaveJsonFile("Assets/Resources/Json/", "Inventory", json);
    }

    public void LoadSlots()
    {
        Inventory inventory = NewtonsoftJson.Instance.LoadJsonFile<Inventory>("Assets/Resources/Json/", "Inventory");

        for (int i = 0; i < _slots.Count; i++)
        {
            if (inventory._itemArr[i] == "null")
                _slots[i].Item = null;
            else
            {
                if (_itemCache.TryGetValue(inventory._itemArr[i], out var value))
                    _slots[i].Item = value;
                else
                {
                    ItemSO itemSO = Resources.Load("ScriptableObject/ItemData/" + inventory._itemArr[i]) as ItemSO;
                    _itemCache[inventory._itemArr[i]] = itemSO;
                    cacheTest.Add(itemSO);
                    _slots[i].Item = itemSO;
                }
            }
        }
    }

    public void DeleteAll()
    {
        foreach (var item in _slots)
            item.Item = null;
    }

    public void SortSlots()
    {

    }
}

기능으로는 아이템 추가, 삭제, 이동, 선택, 인벤토리 저장과 로드가 있다.

인벤토리 슬롯의 개수가 고정돼있다고 가정해서 그리드 레이아웃 패널에 아이템 슬롯들을 미리 배치해 두고 각 슬롯의 정보들을 저장하고 불러오고 옮기는 방식으로 설계했다.

아이템을 선택하고 옮기는 부분에서 살~짝 고민했었는데 처음에는 슬롯 자체를 옮길까 했지만 비효율적이고 null exception도 많이 발생했다.
그래서 아이템을 선택하면 해당 슬롯의 인덱스를 받아 각 슬롯의 아이템 정보만 가져오거나 옮기거나 하는 방식으로 하니 에러도 없고 훨씬 쉬웠다.

Save & Load는 NewtonsoftJson을 이용했다. JsonUtility를 사용해도 될 것 같다.

public class Inventory
{
    public List<string> _itemArr = null;

    public Inventory()
    {
        _itemArr = new List<string>();
    }

    public Inventory(List<string> itemArr)
    {
        _itemArr = itemArr;
    }
}

인벤토리 정보를 파싱하기 위한 클래스를 간단하게 만들었다. 아이템의 이름(혹은 고유 ID)만 Json에 저장해서 불러올 때는 이름 정보를 가지고 게임 데이터에서 불러오는 방식으로 구현했다.
불러올 때는 같은 아이템 정보를 계속해서 불러와 효율이 떨어지는 것을 방지하기 위해 Dictionary로 캐싱했다.

Json에는 이런 형식으로 저장된다.
슬롯에 아이템이 존재하면 해당 아이템의 이름이, 아니라면 null이 string 값으로 저장되어 Save & Load를 편하게 할 수 있다.

구현 후기

간단하게 추가랑 기능 버튼들을 만들어서 테스트해 봤다. 모든 기능이 정상적으로 작동하는 모습이다.

예전엔 인벤토리 시스템 같은 걸 대체 어떻게 만드나 싶었어서 에셋을 써볼까 하다가 어차피 공부하는 입장에서 에셋을 쓰는 게 무슨 의미일까 싶어 그냥 포기했었다.
그 이후로 아이템 관련 구현은 하지 않았었는데 막상 해보니 주먹구구식으로 해서 그런가 수월하게 진행됐다.

기본 틀은 나름 체계적으로 짜놨으니 Save & Load를 파이어베이스와 연계하고 아이템에 대한 세부 정보를 구분지으면 될 것 같다.

'Unity > Devlog' 카테고리의 다른 글

[Unity2D] 1:1 대전 게임  (0) 2023.10.04
[Unity2D] 포톤 서버로 멀티 구현  (0) 2023.02.20
[Unity 2D] 유닛 확장  (0) 2023.01.23
[Unity 2D] 캐릭터 움직임  (0) 2023.01.23
[Unity 3D] NPC 퀘스트 팝업  (0) 2023.01.16
Comments