[인벤토리 시스템] 아이템

Jongmin Kim·2025년 8월 13일

Lapi

목록 보기
9/14

시작

인벤토리 시스템은 게임에서 가장 중요하다고 말할 수 있는 부분이다. 인벤토리를 통해서 사용자는 자신의 획득한 아이템을 가지고 특별한 연산들을 진행할 수 있다.

또한, 인벤토리와 다른 UI들과의 상호작용을 생각하면 절대적으로 차지하는 비중이 많다. 그러므로 인벤토리 시스템은 추후의 확장을 생각해서 더 구조적인 설계가 필요하다. 그럼 시작해보자.



설계

우선 인벤토리를 구현하기 전에 전체 흐름을 살펴보자.

인벤토리에서는 주로 돈과 아이템들 관리하게 된다. 돈은 유저 서비스에 묶일 수도 있다고 생각은 하지만 나는 인벤토리에서 돈과 아이템을 관리하는 것이 책임 분리에서 더 낫다고 판단했다.

InventoryPresenterIInventoryServiceIInventoryView 사이에서 돈 갱신 이벤트를 연결해주는 역할과 아이템 슬롯을 관리하는 역할을 한다.

ItemSlotPresenter는 포인터, 드래그, 드랍 등의 기능들을 수행하며 각 컨텍스트에 맞추어 서비스의 인덱스를 참조하게 된다.

아이템 슬롯은 인벤토리, 장비, 스킬, 퀵슬롯, 상점, 제작소 등 여러 맥락에서 사용될 수 있기 때문에 매우 범용적으로 구현해야 한다.

하지만 범용적으로 구현하는 만큼 SRP를 위배하게 되고 이를 그나마 보완하기 위해 컨텍스트와 핸들러를 두어 책임을 위임한다.



아이템 정의

아이템은 Scriptable Object(이하 SO)를 사용하여 정의한다. 런타임에서 값이 유지되며 전역적으로 참조할 수 있기 때문에 매우 편리하다.

그 다음은 아이템에 포함될 데이터를 생각해야 한다. 개발자마다 포함할 데이터를 판단하는 작업은 다를테지만 나는 다음과 같이 판단을 했다.

  1. 아이템 코드
  2. 아이템 명
  3. 아이템 타입
  4. 중첩 가능의 여부
  5. 아이템 쿨타임
  6. 아이템 스프라이트

공통적으로 사용되는 아이템은 이 정도를 포함한다고 생각했다. 만약 더 필요한 데이터가 있다면 그 부분은 그 아이템에 맞게 아이템을 상속받아서 구현하면 되기 때문이다.


아이템 코드 정의

우선, 아이템 코드를 enum을 사용해서 정의한다.

public enum ItemCode
{
    NONE = 0,

    // 소비 아이템(1 ~ 200)
    SMALL_HP_POTION = 1, SMALL_MP_POTION = 2, SMALL_POTION = 3,
    MIDDLE_HP_POTION = 4, MIDDLE_MP_POTION = 5, MIDDLE_POTION = 6,

    // 퀘스트 아이템(201 ~ 400)

    // 기타 아이템(401 ~ 600)
    BRANCH = 401, STONE = 402, IRON = 403, SILVER = 404, GOLD = 405, RUBY = 406,

    // 스킬 (601 ~ 700)
    DASH = 601, FIRE_BALL = 602, THUNDER = 603,

    // 장비(1001 ~ 1100)
    OLD_SWORD = 1001, OLD_BOW = 1002, OLD_SHIELD = 1003, OLD_HELMET = 1004, OLD_ARMOR = 1005,
    NINA_SWORD = 1006, NINA_BOW = 1007, NINA_SHILD = 1008, NINA_HELMET = 1009, NINA_ARMOR = 1010,
    SENIOR_SWORD = 1011, SENIOR_BOW = 1012, SENIOR_SHIELD = 1013, SENIOR_HELMET = 1014, SENIOR_ARMOR = 1015,
}

아이템 타입 정의

아이템 타입은 SystemFlags 어트리뷰트를 사용하여 여러 개의 비트를 사용할 수 있도록 한다.예를 들면 퀘스트 아이템이면서 소비 아이템인 경우도 존재할테니 말이다.

지금이랑은 관계가 없는 이야기지만 미리 이야기하면 ItemType을 통해서 비트 마스크 연산을 하여 슬롯에 들어갈 수 있는 아이템인지 판단하는 작업을 할 수 있다.

[System.Flags]
public enum ItemType
{
    NONE = 0,

    Consumable = 1 << 0,
    Quest = 1 << 1,
    ETC = 1 << 2,

    Skill = 1 << 3,

    Equipment_Helmet = 1 << 4,
    Equipment_Armor = 1 << 5,
    Equipment_Weapon = 1 << 6,
    Equipment_Shield = 1 << 7,
}

아이템 정의

아이템을 정의하기에 필요한 데이터 타입인 ItemCodeItemType을 모두 정의했으니 이제는 Item을 구현할 수 있다.

using UnityEngine;

[CreateAssetMenu(fileName = "New Item", menuName = "SO/Create Generic Item")]
public class Item : ScriptableObject
{
    [Header("아이템 기본 정보")]
    [Header("아이템 코드")]
    [SerializeField] private ItemCode m_code;
    public ItemCode Code => m_code;

    [Header("아이템 타입")]
    [SerializeField] private ItemType m_type;
    public ItemType Type => m_type;

    [Header("아이템 명")]
    [SerializeField] private string m_name;
    public string Name => m_name;

    [Header("슬롯 중첩 여부")]
    [SerializeField] private bool m_stackable;
    public bool Stackable => m_stackable;

    [Header("아이템 쿨타임")]
    [SerializeField] private float m_cool = -1f;
    public float Cool => m_cool;

    [Header("아이템 이미지")]
    [SerializeField] private Sprite m_sprite;
    public Sprite Sprite => m_sprite;
}



아이템 생성

다음과 같이 [Create] → [SO] → [Create Generic Item]을 통해서 아이템 SO를 개발자 취향껏 마음대로 아이템을 생성할 수 있다.

다음은 내가 임의로 체력 포션을 만든 예시다.



아이템 매니저 정의

이렇게 위의 작업을 반복하여 여러 아이템을 정의하였다면 이 아이템을 관리하고 접근할 수 있는 객체가 필요하다. 이를 아이템 매니저라고 한다.

아이템 매니저는 씬을 넘나들면서 참조할 필요성이 있기 때문에 전역적으로 존재해야 한다.

전역적으로 객체를 존재하게 만들기 위해서는 DontDestroyOnLoad, Singleton, Static 등 다양한 방법을 사용할 수 있다.

하지만 위의 방법을 사용하게 되면 직관적인 편집이 어렵다. 어떤 아이템을 관리해야 하는지 일일히 하드 코딩을 통해야 하며 씬에 종속적이다.


따라서 아이템 매니저를 SO로 생성한다. SO는 어떤 아이템을 관리해야 하는지의 여부를 비개발 직군도 충분히 결정할 수 있을만큼 직관적이며, 씬에 종속적이지 않기 때문에 장점이 뚜렷하다.

SO로 생성하는 만큼 매니저?라는 이름의 의미가 직관적이진 못한 것 같아서 나는 아이템 데이터베이스라고 정했다.

역시나 SO로 아이템 매니저를 생성하기야 하지만 이는 충분히 개발하면서 변경될 여지가 다분하다. 따라서 다른 코드에서의 관심사를 분리하기 위해 아이템 매니저 인터페이스를 구현한다.

public interface IItemDataBase
{
    Item GetItem(ItemCode code);
}

그리고 우리는 계획대로 SO를 이용하여 ItemDataBase를 생성한다.

using System.Collections.Generic;
using UnityEngine;

[CreateAssetMenu(fileName = "Item DataBase", menuName = "SO/DB/Create Item DataBase")]
public class ItemDataBase : ScriptableObject, IItemDataBase
{
    [Header("아이템 목록")]
    [SerializeField] private Item[] m_item_list;

    private Dictionary<ItemCode, Item> m_item_dict;

#if UNITY_EDITOR
    private void OnEnable()
    {
		Initialize();
    }
#endif

	private void Initialize()
    {
        m_item_dict = new();

		// SO의 OnEnable()은 유니티 에디터에서 런타임이 아닌 환경에서도 작동하기 때문에
        // 반드시 리스트의 조건이 널이 아닌 경우에 작동하도록 설정한다.
        if (m_item_list == null)
        {
            return;
        }

		// 인스펙터를 통해 로드한 아이템 리스트를
        // 아이템 코드를 키로 하여 딕셔너리에 저장한다.
        foreach (var item in m_item_list)
        {
            m_item_dict.TryAdd(item.Code, item);
        }    
    }

    public Item GetItem(ItemCode code)
    {
    	if(m_item_dict == null)
        {
        	Initialize();
        }
        
        return m_item_dict.TryGetValue(code, out var item) ? item : null;
    }
}



아이템 매니저 생성

다음과 같이 [Create] → [SO] → [DB] → [Create Item DataBase]로 아이템 매니저를 생성한다.

그리고 생성한 아이템 매니저에 관리할 아이템의 목록을 인스펙터에서 채워 넣는다.



마무리

아직 인벤토리 시스템의 끝이 멀게만 느껴진다. 이제 아이템을 정의했으니 이를 토대로 다음 글에서는 인벤토리 서비스에 대해서 알아보도록 하겠다.

profile
Game Client Programmer

0개의 댓글