Notice
Recent Posts
Recent Comments
Link
«   2026/05   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
Tags
more
Archives
Today
Total
관리 메뉴

개발일기

유니티#19 인벤토리, 퀵슬롯, 아이템 시스템 제작 본문

Unity

유니티#19 인벤토리, 퀵슬롯, 아이템 시스템 제작

kimjw7815 2025. 4. 30. 18:25

진짜 개 어렵다

우선 파일 구조부터 정리

Assets/
├──Class/
│  └──Item.cs
├──Prefabs/
│  ├──Masterball(Prefab Asset)
│  ├──Pokeball(Prefab Asset)
│  └──Slot(Prefab Asset)
├──Resources/
│  └──Items/
│     ├──Masterball(Item)
│     └──Pokeball(Item)
├──Scripts/
│  ├──PickableObject.cs
│  └──PlayerSystemManager/
│     ├──InventoryManager.cs
│     ├──InventorySlot.cs
│     ├──ItemUsingManager.cs
│     ├──PlayerStatusManager.cs
│     └──QuickSlotManager.cs
└──Sprites/
   ├──masterball.png
   └──pokeball.png

놀랍게도, 이번에 쓸 것들만 정리한 거다.

일단 처음으로 만든게 퀵슬롯 관련 코드였고, 그 다음에 아이템 관련 코드였고, 그 다음에 인벤토리 관련 코드였다.

기본적인 작동 원리는 이렇다.

 

// Assets/Scripts/PlayerSystemManager/InventoryManager.cs
public class InventoryManager : MonoBehaviour
{
    public int inventorySize = 20;
    public InventorySlot[] slots;


    private Dictionary<int, Item> itemDatabase;

    void Start()
    {
        slots = new InventorySlot[inventorySize];
        for (int i = 0; i < slots.Length; i++)
            slots[i] = new InventorySlot();

        itemDatabase = LoadItemDatabase();

        // 예: 0번 슬롯에 ID 1번 아이템 넣기
        // slots[0].itemId = 1;
        // slots[0].quantity = 1;
    }
    // ...
}

// Assets/Scripts/PlayerSystemManager/InventorySlot.cs
[System.Serializable]
public class InventorySlot
{
    public int itemId;
    public int quantity;

    public InventorySlot()
    {
        itemId = 0;
        quantity = 0;
    }
}

// Assets/Class/Item.cs
using UnityEngine;

public enum ItemType
{
    Consumable,
    Equipment,
    Quest,
    Material,
    Throwable
}

[CreateAssetMenu(fileName = "New Item", menuName = "Inventory/Item")]
public class Item : ScriptableObject
{
    public string itemName;
    public string description;
    public Sprite icon;
    public int id;
    public ItemType itemType;
    public int value;
    public GameObject itemPrefab;
}

 

인벤토리 매니저는 인벤토리 20칸을 생성을 해준다.

이 때, slots가 인벤토리의 전신이 되는 경우인데, InventorySlot 객체를 받는 리스트다. 그러면 이 InventorySlot은  어떻게 정의가 되느냐 하면, 다음과 같다. itemId는 각자 이름이 다 다르지만, 모두 하나를 지칭한다. 바로 Item 객체의 int id를 가리켜주는 것이다.

플레이어가 오브젝트를 주우면, slots의 정보를 바꾼다. 이 때, 슬롯 내부의 아이템과 줍는 아이템의 id 정보를 비교해서, 이미 슬롯에 존재하는 아이템이라면 slots[i].quantity만을 증가시키고, 슬롯 내부에 존재하지 않는 아이템이라면 slots[i].itemId를 수정해 준 다음 slots[i].quantity를 증가시킨다.

// Assets/Scripts/PlayerSystemManager/InventoryManager.cs
public class InventoryManager : MonoBehaviour
{
	// ...
	public bool AddItem(Item item)
    {
        for (int i = 0; i < slots.Length; i++)
        {
            if (slots[i].itemId == item.id)
            {
                slots[i].quantity++;
                return true;
            }
            else if (slots[i].itemId == 0)
            {
                slots[i].itemId = item.id;
                slots[i].quantity++;
                return true;
            }
        }
        return false; // 인벤토리 꽉 찼음
    }

    public bool AddItemById(int id)
    {
        for (int i = 0; i < slots.Length; i++)
        {
            if (slots[i].itemId == id)
            {
                slots[i].quantity++;
                return true;
            }
            else if (slots[i].itemId == 0)
            {
                slots[i].itemId = id;
                slots[i].quantity++;
                return true;
            }
        }
        return false; // 인벤토리 꽉 찼음
    }
}

// Assets/Scripts/PickableObject.cs
public class PickableObject : MonoBehaviour
{
    void Update()
    {
        // 플레이어가 반경 내에 있는지 감지
        Collider2D[] hits = Physics2D.OverlapCircleAll(transform.position, radius, playerLayer);
        foreach (var hit in hits)
        {
            if (hit.CompareTag("Player"))
            {
                player = hit.transform;

                if (Input.GetKeyDown(pickUpKeyCode))
                {
                    PickUp();
                }
            }
        }
    }

    void PickUp()
    {
        // 인벤토리에 추가
        InventoryManager inventory = player.GetComponent<InventoryManager>();
        if (inventory != null && itemId != 0)
        {
            bool success = inventory.AddItemById(itemId);
            if (success)
            {
                Destroy(gameObject); // 인벤토리에 들어갔으므로 씬에서 제거
            }
            else
            {
                Debug.Log("인벤토리가 가득 찼습니다.");
            }
        }
        else
        {
            Debug.LogWarning("InventoryManager 또는 itemData가 없음.");
        }
    }
}

 

이제 그 다음 아이템의 사용에 관한 걸 처리하는 걸 보자.

우선 사용할 아이템을 설정하기 위해, 퀵슬롯을 도입해준다. 이 퀵슬롯 같은 경우는 1~8개가 존재하는데, 각각 인벤토리의 1~8칸을 그대로 가져온다고 보면 되겠다. 단, 퀵슬롯에 저장되는 건 오직 Item.id 뿐이다. 어차피 구체적인 정보는 모두 InventoryManager.cs에 저장되어 있으니, 여기서 Item.id 정보만 잘 저장해놔서 실제 사용 스레드(ItemUsingController.cs)로 전달해주면 되는 일이기 때문이다. QuickSlotManager.cs에서 자세히 볼 만한 건, 사용할 아이템 선택과 디자인 정도겠다.

// Assets/Scripts/PlayerSystemManager/QuickSlotManager.cs
public class QuickSlotManager : MonoBehaviour
{
	// ...
	void CreateSlots()
    {
        for (int i = 0; i < 8; i++)
        {
            GameObject slot = Instantiate(slotPrefab, slotParent.transform);
            RectTransform rt = slot.GetComponent<RectTransform>();

            float angle = i * Mathf.PI * 2f / 8f;
            Vector2 pos = new Vector2(Mathf.Cos(angle), Mathf.Sin(angle)) * radius;
            rt.anchoredPosition = pos;

            rt.localRotation = Quaternion.Euler(0f, 0f, -90+angle * Mathf.Rad2Deg);

            // Icon 자식 회전 보정
            Transform icon = rt.Find("Icon");
            if (icon != null)
            {
                icon.localRotation = Quaternion.Euler(0f, 0f, 90 - angle * Mathf.Rad2Deg); // 반대로 회전시켜서 보정
            }
            Transform itemQuantity = rt.Find("ItemQuantity");
            itemQuantity.localRotation = Quaternion.Euler(0f, 0f, 90 - angle * Mathf.Rad2Deg); // 반대로 회전시켜서 보정

            slots[i] = rt;
        }
    }
    
    void UpdateQuickSlots()
    {
        for (int i = 0; i < 8; i++)
        {
            quickSlot[i] = inventoryManager.slots[i].itemId; // 인벤토리 앞 8칸을 그대로 가져옴

            // 퀵슬롯 UI에 아이템 아이콘 업데이트
            // 슬롯의 이미지 컴포넌트를 찾아 아이콘을 변경
            Image slotImage = slots[i].Find("Icon").GetComponent<Image>();
            if (quickSlot[i] != 0) // 아이템이 있을 경우에만
            {
                Item item = inventoryManager.GetItemById(quickSlot[i]);
                if (item != null)
                {
                    slotImage.sprite = item != null ? item.icon : null;
                    slotImage.enabled = true;
                }
            }
            else
            {
                slotImage.sprite = null;
                slotImage.enabled = false;
            }

            TextMeshProUGUI text = slots[i].Find("ItemQuantity").GetComponent<TextMeshProUGUI>();
            int quantity = inventoryManager.slots[i].quantity;
            text.text = (quantity==0?"":$"{quantity}");
        }
    }
    
    int GetSlotFromDirection(Vector2 direction)
    {
        float angle = Mathf.Atan2(direction.y, direction.x);
        if (angle < 0) angle += 2 * Mathf.PI;

        int index = Mathf.FloorToInt((angle + Mathf.PI / 8f) / (Mathf.PI / 4f)) % 8;
        return index;
    }
    // ...
}

이 때, GetSlotFromDirection이 방향을 보고 어떤 아이템이 선택될지 정하는 코드가 되겠다. 저기서 나오는 index를 기반으로, 해당 슬롯의 Item.id를 확인하고, 사용할 때 몇 번째 슬롯에 접속할지 정하게 된다. 밑의 gif에서 마우스의 움직임에 따라 QuickSlotManager의 CurrentSlotId가 어떻게 변하는지 보자. 참고로 확인한 Item.id는 PlayerStatusController의 public int itemHolding으로 전달되게 된다.

그렇게 Item.id를 전달해주게 되면, ItemUsingController.cs와 InventoryManager.cs에서 각각 일을 하게 된다. ItemUsingController에서 사용 키가 눌린 것을 감지하면, Item.id로부터 Item 객체 전체를 얻어내고, Item.Prefab으로 오브젝트를 생성한다. 그리고 InventoryManager에 접근하여서 Slots[i].quantity를 감소시키거나, slots[i].itemId를 초기화시킨다.

public class ItemUseController : MonoBehaviour
{
    public Transform throwOrigin;
    public float throwForce = 10f;

    private InventoryManager inventory;
    private PlayerStatusController status;
    private QuickSlotManager slot;

    void Start()
    {
        inventory = GetComponent<InventoryManager>();
        status = GetComponent<PlayerStatusController>();
        slot = GetComponent<QuickSlotManager>();
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.F)) // 사용키
        {
            Debug.Log("UseItem Called!");
            UseItem();
        }
    }

    void UseItem()
    {
        int id = status.itemHolding;
        Item item = inventory.GetItemById(id);
        if (item == null) return;

        switch (item.itemType)
        {
            case ItemType.Throwable:
                Throw(item);
                break;
                // 나중에 회복 아이템, 설치형 등 확장 가능
        }
    }

    void Throw(Item item)
    {
        GameObject obj = Instantiate(item.itemPrefab, throwOrigin.position, Quaternion.identity);
        Rigidbody2D rb = obj.GetComponent<Rigidbody2D>();
        int dir = status.direction ? 1 : -1;
        rb.AddForce(new Vector2(dir * throwForce, 0), ForceMode2D.Impulse);

        inventory.RemoveItemBySlotId(slot.currentSlotId);
        status.itemHolding = 0;
    }
}

 

여기까지가 대략적인 설명이다. 코드를 쓰면서 정말 뇌가 깨지는 줄 알았는데 일단 잘 만들어져서 너무 좋은 것 같다

제발 이게 확장성이 좋길, 별달리 더 건드릴 게 없기를.

아 근데 Throwable 말고 다른 아이템도 생각해봐야하긴 해

다음에 필요하면 만들겠지