3D RPG 만들기! (8) 인벤토리 만들기

    2달가량 글이 없었는데, 여러 해보고싶은게 생겨서 하느라 늦었다.

    글을 임시저장해놓고 약간만 수정해서 올리면되는데 이렇게 늦어질줄 몰랐다

    완성까지 1달걸렸는데 예비군에 자격증시험이 겹칠때라 꺽여버렸다.

     

     

     

    미리보는 완성본

     

    z키를 누르면 근처의 아이템을 주워서 인벤토리에 넣는다.

    인벤토리 안에서 드래그로 위치변경, 위치교환, 분해, 버리기, 사용하기 등 구현했다.

     

    인벤토리 구성

    만들때 순서를 인벤토리를 먼저 만들고 아이템을 만들었다. 물론 포스팅은 판대로했지만 ㅇㅅㅇ

    반대로 만드는게 더 깔끔하지만, 계획해둔 생각이 있어서 다행이 별 지장없이 만들어졌다.

    우선 인벤토리는 이렇게 만들었다.

     

     

    inventory

    • 내부적으로 인벤토리가 연산되는 부분
    • item과 최대용량을 변수로 가지고있고, 아이템을 더하거나 빼는 작업을 한다.

     

    inventoryUI

    • 외부적으로 보이는 UI를 다루는 부분
    • 유저가 합하거나 이동하려는 행동을 inventory에게 전달

     

    SlotUI

    • 인벤토리의 각 칸인 Slot에 해당
    • 어떤 item을 가졌는지 이미지와 개수, 아이템이 있는지 여부를 포함
    • 아이템 자체를 slot이 가지고있지 않음

     

    TooltipUI

    • 인벤토리의 아이템에 마우스를 올리면 아이템 정보를 확인 가능

     

    PopUpUI

    • 아이템을 버리거나 나눌때, 수량을 입력

     

    MovableInventory

    • 인벤토리의 Header를 클릭해서 이동가능하게 해줌

     

    스크립트

    Inventory

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.EventSystems;
    
    public class Inventory : MonoBehaviour
    {
        public Item[] items;
        public int maxCapacity;
        [SerializeField] private InventoryUI inventoryUI;
    
    
        
        public void InitItems(int _maxCapacity)
        {
            // 최대 개수만큼 아이템배열 생성
        }
    
        
        public int TryAddItem(Item _item)
        {
            // 아이템을 더함 (획득)
        	// TryAddItemToIndex 를 호출함
        }
    
        
        public int TryAddItemToIndex(int _index, Item _item, int _amount = 0)
        {
            // 아이템 배열에 비어있는 공간에 아이템을 더함
        	// (비어있는 경우와, 비어있지 않은경우를 다르게 계산함)
        }
    
        
        public void RemoveItem(Item _item)
        {
            // 해당 아이템을 인벤토리에서 제거
        	// (아이템에 개수를 포함하고 있음)
        }
    
        
        public Item RemoveItem(int _index, int _amount)
        {
            // 인덱스에서 개수만큼 아이템 제거
        }
    
    
        public void SwapItems(int firIndex, int secIndex)
        {
        	// 위치 변경
        }
    
    
        public int FindItemCount(Item _item)
        {
            // 입력받은 아이템과 동일한 아이템의 개수 리턴
        }
    
        
        public bool TryUseItem(int _index)
        {
            // 아이템 사용
        }
    
        
        public void UpdateUI()
        {
            // items배열에 있는 item을 UI로 업데이트함
        }
    
        
        public void UpdateUIToIndex(int index)
        {
            // 각 인덱스의 아이템을 UI로 업데이트
        }
    
        
        public void QuickPortion()
        {
           // 가장 첫번째에 있는 포션 사용
           // (버튼에서 사용됨)
        }
    
    }

     

    inventoryUI

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    using UnityEngine.UI;
    using UnityEngine.EventSystems;
    
    public class InventoryUI : MonoBehaviour
    {
    
        [Header("슬롯 동적 생성")]
        [SerializeField] private int hSlotCount = 5;
        [SerializeField] private int vSlotCount = 3;
        [SerializeField] private int maxCapacity => hSlotCount * vSlotCount;
        [SerializeField] private float hSlotSize;
        [SerializeField] private float vSlotSize;
        [SerializeField] private float slotSize;
        [SerializeField] private float hMargin;
        [SerializeField] private float vMargin;
        [SerializeField] private float padding;
    
        [SerializeField] private RectTransform contentArea;
        [SerializeField] private GameObject slotUiPrefab;
    
        [Space(10f)]
        [Header("마우스 이벤트")]
        private List<SlotUI> slotUIs = new List<SlotUI>();
        private GraphicRaycaster gr;
        private PointerEventData ped;
        private List<RaycastResult> rrList;
        [SerializeField] private Image dragImage;   // 드래그중에 
        private SlotUI dragBeginSlotUI;
        [SerializeField] private SlotUI curSlotUI;
        private SlotUI dragEndSlotUI;
        private Vector3 curMousePos;
    
        [Space(10f)]
        [Header("기타")]
        [SerializeField] private PopUpUI popUpUI;
        [SerializeField] private TooltipUI tooltipUI;
        private bool isPopUpOn;
        [SerializeField] private Inventory inventory;
    
    
        private void Awake()
        {
            inventory.gameObject.SetActive(true);
    
            initSize();
            CreateSlot();
    
            gr = GetComponent<GraphicRaycaster>();
            ped = new PointerEventData(EventSystem.current);
            rrList = new List<RaycastResult>();
            inventory.InitItems(maxCapacity);
    
            inventory.gameObject.SetActive(false);
        }
    
        private void Update()
        {
            curMousePos = Input.mousePosition;
            ped.position = curMousePos;
    
            SetTooltip();
            TryUseItem();
            OnMouseDown();
            OnMouse();
            OnMouseUp();
        }
    
        public void SetItemUI(int index, Sprite sprite, int amount)
        {
            slotUIs[index].SetItem(sprite, amount);
        }
    
        #region 슬롯 동적 생성
    
        
        private void initSize()
        {
            // 사이즈 설정
        	// 양측 padding 계산 후 남은 공간에서 size와 margin설정
        }
    
        
        private void CreateSlot()
        {
            // 슬롯 동적 생성
        }
    
        
        private RectTransform CloneSlot()
        {
        	// 슬롯 생성
        }
    
        #endregion
    
        #region 마우스 이벤트 처리 (drag and drop)
        
        
        private GameObject FindUI()
        {
            // 클릭했을때 UI가 있는지 확인
        }
    
        
        private void SetTooltip()
        {
            // 툴팁 표시
        }
    
       
        public void TryUseItem()
        {
            // 우클릭으로 아이템 사용
        }
    
        
        private void OnMouseDown()
        {
            // 클릭시 해당 슬롯의 이미지를 가져와서 보여줌
        	// 놓으면 해당 위치의 슬롯과 연산(같으면 더하고, 다르면 교환. 없으면 이동)
        }
    
        
        private void OnMouse()
        {
            // 드래그하는 동안 아이템 이동
        }
    
        
        private void OnMouseUp()
        {
            // 마우스를 때면 아이템을 버리거나 합치거나 연산함
        }
    
        
        private void SwapItems(SlotUI begin, SlotUI end)
        {
            // 아이템 위치 교환
        	// (null과 교환하더라도 상관 X)
        }
    
        
        private void SumItems(SlotUI begin, SlotUI end, int amount = 0)
        {
    		// 합치기
        	// 분해하거나 이동도 이 함수로 구현됨
        }
    
        
        private Item RemoveItem(int _index, int _amount)
        {
            // 아이템 제거
        }
    
       
        private void DropItem(int _index, int _amount)
        {
            // 아이템 떨어트리기
        }
    
        
        public int TryPickUpItem(Item item)
        {
            //아이템 줍기
        }
    
        #endregion
        
    }

     

    전체 스크립트는 git에 올렸다.

     

     

     

    아이템

    아이템은 Itemdata와 Item으로 분리했다.

    ItemData의 경우는 ScriptableOjbect로 만들어서 각 아이템의 고유 정보를 가지고있고,

    Item은 그 고유정보인 ItemData와 개수를 가지고있다.

     

    Portion의 경우는 사용할 수 있어야해서 Useable이라는 interface를 추가로 상속받는다.

    Useable을 상속받기때문에 인벤토리에서 사용할때 is로 확인 가능하다.

    if (items[_index] is UseableItem useableItem) { ... }

     

     

    ItemData의 경우 이렇게 만들었다.

    각 고유번호로 구분을 하고, 이름과 툴팁, 그리고 sprite를 가지고있다.

     

     

     

    아이템 드랍

     

    pickUp이 가능한 아이템을 따로 만들어두고 프리팹화 시켰다.

    그리고 아이템이 생성될때 위로 튀어오르도록 만들었다.

    이미지는 itemData에서 sprite를 그대로 가져와서 넣어주는 방식으로 구현했다.

    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;
    
    public class PickUpItem : MonoBehaviour
    {
        public Item item;
        public int amount;
        private Rigidbody rigid;
        private SpriteRenderer spriteRenderer;
    
        private void Awake()
        {
            rigid = GetComponent<Rigidbody>();
            spriteRenderer = GetComponent<SpriteRenderer>();
        }
    
        private void Start()
        {
            rigid.AddForce(new Vector3(Random.Range(-100f, 100f), 200, Random.Range(-100f, 100f)));
        }
    
        void Update()
        {
            amount = item.amount;
            transform.Rotate(100f * Time.deltaTime * Vector3.up);
        }
    
        public void SetItem(ItemData _data, int _amount = 1)
        {
            item = _data.CreateItem(_amount);
            spriteRenderer.sprite = item.data.sprite;
        }
    }

     

     

     

     

    만들면서 힘들었던거

    1.

    하다가 막힌 부분이 몇 군데 있긴 했다만, 다 검색하고 머리를 짜내니 고쳐졌다.

    그래도 그 중 가장 어려웠던 문제가 있었는데,

    Item이 Monobehaviour를 상속받을 필요가 없다고 생각해서 일반 클래스로 만들면서 생긴거다.

    Monobehaviour를 상속받으면 new키워드로 생성이 불가능하고,

    없어야 new 키워드로 생성이 가능하다.

    그런데 DropItem을 분리하지 않고 만드는 과정에서 Monobehavior가 필요했고

    그과정에서 Monobehaviour를 쓰냐 마냐로 자그마치 이틀간 20시간 이상을 갈아넣었다...

    지금생각하면 당연하지만 이거 만들땐 머리터지는줄 알았다. 

     

    아래는 머리가 너무 복잡해서 써둔 메모

    -인벤토리 안되서 써둔 메모-
    Monobehaviour를 상속받으면 new키워드로 생성 불가
    Monobehaviour를 빼면 transform.Rotate를 사용불가
    Item과 Monobehaviour를 둘다 상속받은 오브젝트를 만들려니 이중상속 불가
    
    drop아이템을 만들어서 Item과 Monobehaviour가 있는 스크립트를 넣으려니 Item이 Monobehaviour가 없어서 컴포넌트로 넣을수없음
    Item이 Monobehaviour가 없도록 수정하려면 너무 많이 수정해야함...
    
    Inventory에 Item이 없으면 자신의 Clone을 만들어서 넣는 방식으로 만든다면? 생성이 안되니 불가능, 프리팹을 Instantiate로 하는것도 낭비
    
    dropItem에 Item을 변수로 만들고 Monobehaviour를 상속받으려고 했으나, Item에 ItemData를 넣지못하게 됨
    
    
    
    Item에 Monobehaviour를 빼는게 맞다고 보고 뺌
    회전이 필요하다면 rotatable을 만들어서 컴포넌트로 추가하는 방식으로 생각중
    ItemData에서 자신을 가지고있는 Item을 만들어주는 메소드를 생성함
    public Item CreateItem()
        {
            return new Item(this);
        }
    근데 또 이러면 Item에 ItemData를 못 넣고 있는 상태.
    
    1. Item에 Monobehaviour를 넣는다.
    	-> new를 사용 불가능하고, Items배열에 Item을 담아두기가 애매해짐.
    2. Item에 Monobehaviour를 넣지 않는다.
    	-> Item에 ItemData를 추가하기가 번거로워지고, ojbect에 Item을 넣을수가 없음.
    
    Monobehaviour를 넣는게 맞다면?
    Items배열은 사용은 가능하니 일단 OK
    참조하는 copy가 아니라, deepcopy를 해야하지만, 이 경우 deepcopy가 힘듬
    
    Monobehaviour를 빼는게 맞다면?
    일단 Item이 Monobehaviour를 가지고 있는것도 이상하긴 함.

    물론 해결은 쉽게 됐다. 

    DropItem을 구분하고 DropItem에 Item을 넣어두면서 해결됐다.

    원하던 방향은 DropItem을 만들더라도 Item을 넣지않고도 정보를 가지고있도록 하고싶었으나, 아무리생각해도 가능한 방법이 아니라서 포기를 했기때문에 해결된 케이스

     

     

    2.

    popupUI를 완벽하게 구현한 것 같으나 확인버튼을 누르면 에러가 나는 상황이 있었다.

    왜 그러는지 이해가안되서 엄청 해맸는데,

    InventoryUI에서 OnMouseDown과 OnMouseUp에서 dragBeginSlotUI와 dragEndSlotUI의 정보를 받아와서 아이템을 나누도록 만들었다.

    그 과정에서 PopUpUI가 켜져있어도 인식이 되버리고, 팝업을 누르면 slot이 아니라서 현재 drag중인 아이템이 null처리가 되버리면서 null값을 참조하도록 되버렸다.

    그래서 nullreferenceexception가 발생했다.

     

    이 경우는 InventoryUI에서 OnMouseDown과 OnMouseUp에서 PopupUI가 켜져있는지 확인하면서 고쳤다.

     

     

    이외에도 엄청 많은 에러가 많았지만, 쉽게 해결되거나 굳이 다룰 내용은 아니니 패스

     

     

    참고한 사이트

    "유니티 인벤토리"라고 검색해서 뜨는 

     

    https://geojun.tistory.com/62

     

    유니티 (Unity) - 처음 만들어 보는 인벤토리 이해하기 (Inventory)

    인벤토리를 만들기 위한 기본 구조를 살펴보겠습니다. 인벤토리가 어떻게 작동하는지 알아보기 위한 아주 간단한 프로그램입니다. 사용된 아이콘 및 슬롯 이미지 https://assetstore.unity.com/packages/2d

    geojun.tistory.com

    심플하게 봄

     

     

    https://rito15.github.io/posts/unity-study-rpg-inventory/

     

    유니티 - RPG Inventory System(RPG 게임 인벤토리 만들기)

    개요

    rito15.github.io

    가장 도움이 됨. 정리가 최고로 잘되있고, 해당블로그 다른 글들도 거의 읽어봤는데 도움되는 정보가 많았다.

     

     

    https://ansohxxn.github.io/unity%20lesson%203/ch5-2/

     

    Chapter 5-2. 인벤토리 : 인벤토리 구현, Grid Layout Group

    인프런에 있는 케이디님의 [유니티 3D] 실전! 생존게임 만들기 - Advanced 강의를 듣고 정리한 필기입니다. 😀 🌜 강의 들으러 가기 Click

    ansohxxn.github.io

    인프런 케이디의 강의를 듣고 필기해둔다는 블로그

    나도 인프런의 케이디 강의를 들었지만 나랑 맞진 않아서 하나밖에 듣지않았다.

    오히려 유튜브에 활동하시는 유니티 선생님들이 더 듣기 편했다. 케바케인듯

    물론 위 블로그의 필기는 최상급

     

    예전에 스펠렁키에서 무기 만들때도 많이 배웠다.

     

     

     

    댓글