게임에서 상자 시스템은 보상을 통해 유저들에게 재미와 긴장감을 제공하며, 게임 내 아이템을 무작위로 획득하는 기회를 제공합니다. 또한 유저는 상자에 아이템을 보관하는 기능을 이용할 수 있습니다. 상자 시스템을 구현하고 이를 정리하였습니다.

 

📺 미리보기

 

💬 목차

  • 총 두 개의 목차로 구성되어 있습니다.
  • 글 별로 아직 언급되지 않은 클래스의 호출이 있을 수 있습니다. 이 사항은 모든 글을 참조하면 충분히 이해할 수 있습니다.

[📌현재 글] 1. [유니티] 상자 시스템(1) - 상자 데이터

2. [유니티] 상자 시스템(2) - 상자 다이얼로그

 

📖 구현 내용

  • 월드에 배치되어있는 상자를 바라보고 키를 누르면 상자가 열립니다.
  • 플레이어에게 아이템을 지급하기위한 목적으로 상자를 처음 열을경우 설정에 따라 아이템이 무작위로 생성됩니다.
  • 아이템이 생성될 때 아이템이 생성되는 위치 또한 무작위로 설정됩니다.
  • 처음으로 상자를 여는것이 아닌경우 저장된 아이템이 유지되며 해당 아이템을 꺼낼 수 있습니다.
  • 플레이어의 아이템을 상자에 집어넣어 보관할 수 있습니다.
  • 상자의 데이터가 저장되어 게임을 세이브 및 로드를 할 때 해당 사항이 저장됩니다.
  • 상자를 열 때 UI의 영역의 크기를 다르게 설정할 수 있습니다. 예) 2x3 크기, 5x5크기 등...

 

✅ 구현

  • 이번 글에서는 월드에 배치할 상자에 대해 아이템을 보관하고, 상자 내부의 아이템을 초기화하기위한 기능을 구현합니다.
  • 세이브 및 로드 기능도 포함되어있지만, 이 글에서는 다루지 않겠습니다.

 

· ChestController.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;

namespace ChestSystem
{
    [System.Serializable]
    public struct ChestSlotItem
    {
        [Header("아이템 코드")]
        [SerializeField] public EntityCode itemCode;

        [HideInInspector] public int itemCount;
        [HideInInspector] public int itemPositionIndex; // 아이템의 슬롯 위치
    }

    [System.Serializable]
    public struct ChestItemSpawnConfig
    {
        [Header("\n초기화 시 아이템 스폰 설정")]

        [Header("스폰을 시도할 아이템의 코드")]
        [SerializeField] public EntityCode itemCode;

        [Header("아이템의 스폰율 (0~1)")]
        [Range(0.0f, 1.0f)][SerializeField] public float spawnRate;

        [Header("아이템의 최소 스폰 개수")]
        [SerializeField] public int minItemCount;

        [Header("아이템의 최대 스폰 개수")]
        [SerializeField] public int maxItemCount;
    }

    [System.Serializable]
    public class ChestInfo
    {
        [Header("상자의 고유 ID")]
        [SerializeField] public int chestUniqueId;

        [Header("상자 인벤토리의 가로 셀 개수")]
        [Range(1, 10)][SerializeField] public int row;

        [Header("상자 인벤토리의 세로 셀 개수")]
        [Range(1, 10)][SerializeField] public int col;

        [Header("초기화 시 상자를 초기화 할 설정값")]
        [SerializeField] public ChestItemSpawnConfig[] chestItemSpawnConfig;

        // 상자에 있는 아이템의 정보
        [HideInInspector] public List<ChestSlotItem> chestSlotItems;
    }

    public class ChestController : MonoBehaviour
    {
        [Header("초기화 시 상자의 정보")]
        [SerializeField] public ChestInfo ChestInfo;

        [field: Header("상자의 타입")]
        [field: SerializeField] public ChestType ChestType { private set; get; } = ChestType.HEAVY_WOOD;

        private Animator mChestAnimator; // 상자의 애니메이션 컨트롤러

        private bool mIsInitReady = true; // 현재 초기화 대기상태인가?
        public bool IsInitReady
        {
            get
            {
                return mIsInitReady;
            }
        }

        public int ChestUniqueId
        {
            get
            {
                return ChestInfo.chestUniqueId;
            }
        }

        private void Awake() 
        {
            mChestAnimator = GetComponentInChildren<Animator>();    
        }

        public void TryOpenDialog()
        {
            // 만약 초기화 대기 상태라면?
            if (mIsInitReady == true)
                Init();

            // 상자 다이얼로그 열기
            ChestDialogManager.Instance.TryOpenDialog(this);
        }

        // 최초로 해당 상자를 여는경우 무작위 초기화 설정
        private void Init()
        {
            // 최초 1회 상자를 열었음을 설정
            mIsInitReady = false;

            // 무작위 unique 값을 리턴하기위한 리스트
            List<int> randPosGen = new List<int>();
            for (int i = 0; i < ChestInfo.row * ChestInfo.col; ++i)
                randPosGen.Add(i);

            // 상자 아이템 리스트 생성
            ChestInfo.chestSlotItems = new List<ChestSlotItem>();

            for (int i = 0; i < ChestInfo.chestItemSpawnConfig.Length; ++i)
            {
                // 확률을 계산하여 확률에 미치지 못하는경우 리턴
                if (Random.value > ChestInfo.chestItemSpawnConfig[i].spawnRate)
                    continue;

                // 아이템 한개 생성
                ChestSlotItem slotItem = new ChestSlotItem();
                {
                    // 해당 아이템의 개수를 범위 내 랜덤으로 지정
                    slotItem.itemCount = Random.Range(ChestInfo.chestItemSpawnConfig[i].minItemCount, ChestInfo.chestItemSpawnConfig[i].maxItemCount + 1);

                    // 무작위 위치를 지정
                    int randIndex = Random.Range(0, randPosGen.Count);
                    
                    // 고유한 무작위 값 가져오기
                    int randPos = randPosGen[randIndex];
                    
                    // 해당 위치의 값 요소 제거 (고유성 보장)
                    randPosGen.RemoveAt(randIndex);

                    // 값 지정
                    slotItem.itemPositionIndex = randPos;

                    // 아이템코드 가져오기
                    slotItem.itemCode = ChestInfo.chestItemSpawnConfig[i].itemCode;

                    // 아이템을 리스트에 삽입
                    ChestInfo.chestSlotItems.Add(slotItem);
                }
            }
        }

        public void PlayChestAnim(bool isOpen)
        {
            switch (this.ChestType)
            {
                case ChestType.HEAVY_WOOD:
                    SoundManager.Instance.PlaySound2D($"{(isOpen ? "Open" : "Shut")}_HeavyWood_" + SoundManager.Range(1, 3));
                    break;
                case ChestType.LIGHT_WOOD:
                    SoundManager.Instance.PlaySound2D($"{(isOpen ? "Open" : "Shut")}_LightWood_" + SoundManager.Range(1, 3));
                    break;
                case ChestType.METAL:
                    SoundManager.Instance.PlaySound2D($"{(isOpen ? "Open" : "Shut")}_Metal_" + SoundManager.Range(1, 3));
                    break;
            }

            if(isOpen)
                mChestAnimator.SetTrigger("openLid");
            else
                mChestAnimator.SetTrigger("closeLid");
        }

        #region Save & Load
        public void LoadFromData(GameSave.SceneChestInfo chestInfo)
        {
            // 로드 된 경우 상자를 열었음을 설정
            mIsInitReady = false;

            // 기존 상자의 슬롯 정보는 제거
            ChestInfo.chestItemSpawnConfig = null;

            // 새로운 슬롯들을 사용하기위해 리스트 대치
            ChestInfo.chestSlotItems = chestInfo.chestSlotItems.ToList();
        }

        public GameSave.SceneChestInfo GetData()
        {
            GameSave.SceneChestInfo sceneChestInfo = new GameSave.SceneChestInfo();

            sceneChestInfo.chestUniqueId = ChestInfo.chestUniqueId;
            sceneChestInfo.chestSlotItems = ChestInfo.chestSlotItems.ToArray();

            return sceneChestInfo;
        }
        #endregion
    }
}

 

[Header("초기화 시 상자의 정보")]
[SerializeField] public ChestInfo ChestInfo;
  • 상자를 처음 열을경우 해당 상자에 아이템을 초기화하기위한 정보를 정의합니다.

 

[field: Header("상자의 타입")]
[field: SerializeField] public ChestType ChestType { private set; get; } = ChestType.HEAVY_WOOD;
  • 상자의 타입입니다.
  • 큰 기능이 아닌 소리를 재생하기위한 타입을 정의합니다.
  • 예를들어, HEAVY_WOOD인경우에는 무거운 나무소리, METAL인경우에는 철제 상자 소리가 들리게합니다.

 

private bool mIsInitReady = true; // 현재 초기화 대기상태인가?
public bool IsInitReady
{
    get
    {
        return mIsInitReady;
    }
}
  • 해당 상자가 한번도 열리지 않아 초기화 대상인지 확인하기위한 변수입니다.

 

public int ChestUniqueId
{
    get
    {
        return ChestInfo.chestUniqueId;
    }
}
  • 세이브 및 로드 기능을 이용할 때 상자를 식별하기위한 ID입니다.

 

public void TryOpenDialog()
{
    // 만약 초기화 대기 상태라면?
    if (mIsInitReady == true)
        Init();

    // 상자 다이얼로그 열기
    ChestDialogManager.Instance.TryOpenDialog(this);
}
  • 해당 상자에 접근하여 상자 열기를 시도합니다.
  • ChestDialogManager에 상자의 정보를 전달하여 상자 내부의 아이템을 다이얼로그 창을 통해 보여줍니다.
  • ChestDialogManager는 다음 글에서 다루겠습니다.

 

// 최초로 해당 상자를 여는경우 무작위 초기화 설정
private void Init()
{
    // 최초 1회 상자를 열었음을 설정
    mIsInitReady = false;

    // 무작위 unique 값을 리턴하기위한 리스트
    List<int> randPosGen = new List<int>();
    for (int i = 0; i < ChestInfo.row * ChestInfo.col; ++i)
        randPosGen.Add(i);

    // 상자 아이템 리스트 생성
    ChestInfo.chestSlotItems = new List<ChestSlotItem>();

    for (int i = 0; i < ChestInfo.chestItemSpawnConfig.Length; ++i)
    {
        // 확률을 계산하여 확률에 미치지 못하는경우 리턴
        if (Random.value > ChestInfo.chestItemSpawnConfig[i].spawnRate)
            continue;

        // 아이템 한개 생성
        ChestSlotItem slotItem = new ChestSlotItem();
        {
            // 해당 아이템의 개수를 범위 내 랜덤으로 지정
            slotItem.itemCount = Random.Range(ChestInfo.chestItemSpawnConfig[i].minItemCount, ChestInfo.chestItemSpawnConfig[i].maxItemCount + 1);

            // 무작위 위치를 지정
            int randIndex = Random.Range(0, randPosGen.Count);

            // 고유한 무작위 값 가져오기
            int randPos = randPosGen[randIndex];

            // 해당 위치의 값 요소 제거 (고유성 보장)
            randPosGen.RemoveAt(randIndex);

            // 값 지정
            slotItem.itemPositionIndex = randPos;

            // 아이템코드 가져오기
            slotItem.itemCode = ChestInfo.chestItemSpawnConfig[i].itemCode;

            // 아이템을 리스트에 삽입
            ChestInfo.chestSlotItems.Add(slotItem);
        }
    }
}
  • 상자를 처음 여는경우 아이템을 지급할 목적으로 초기화합니다.
  • 상자 내부에 존재하는 아이템을 무작위로 설정하여 게임마다 지급하는 아이템을 다르게 설정할 수 있습니다.
  • 인덱스 번호를 이용하여 슬롯 내의 위치를 지정하여 아이템의 위치 또한 변경할 수 있습니다.

 

[System.Serializable]
public struct ChestItemSpawnConfig
{
    [Header("\n초기화 시 아이템 스폰 설정")]

    [Header("스폰을 시도할 아이템의 코드")]
    [SerializeField] public EntityCode itemCode;

    [Header("아이템의 스폰율 (0~1)")]
    [Range(0.0f, 1.0f)][SerializeField] public float spawnRate;

    [Header("아이템의 최소 스폰 개수")]
    [SerializeField] public int minItemCount;

    [Header("아이템의 최대 스폰 개수")]
    [SerializeField] public int maxItemCount;
}
  • 상자를 처음 열어서 초기화를 해야하는경우 스폰할 아이템에 대한 정보를 정의하는 구조체입니다.

 

[System.Serializable]
public struct ChestSlotItem
{
    [Header("아이템 코드")]
    [SerializeField] public EntityCode itemCode;

    [HideInInspector] public int itemCount;
    [HideInInspector] public int itemPositionIndex; // 아이템의 슬롯 위치
}
  • 실제로 상자 내에 보관중인 아이템을 담는 정보입니다.

 

[System.Serializable]
public class ChestInfo
{
    [Header("상자의 고유 ID")]
    [SerializeField] public int chestUniqueId;

    [Header("상자 인벤토리의 가로 셀 개수")]
    [Range(1, 10)][SerializeField] public int row;

    [Header("상자 인벤토리의 세로 셀 개수")]
    [Range(1, 10)][SerializeField] public int col;

    [Header("초기화 시 상자를 초기화 할 설정값")]
    [SerializeField] public ChestItemSpawnConfig[] chestItemSpawnConfig;

    // 상자에 있는 아이템의 정보
    [HideInInspector] public List<ChestSlotItem> chestSlotItems;
}
  • 상자의 정보를 나타내는 구조체입니다.

 

✅ 사용

  • 위 이미지와 같이 상자 오브젝트에서 상자 시스템을 사용합니다.

 

  • 고유 식별 아이디를 설정하여 해당 상자를 식별할 수 있도록 합니다.
  • Row, Col을 설정하여 열리는 상자의 크기를 설정할 수 있습니다.
  • Chest Item Spawn Config에서 처음으로 상자를 열 경우 어떤 아이템들을 스폰할지 설정할 수 있습니다.

 

// 상자 다이얼로그 열기
ChestDialogManager.Instance.TryOpenDialog(this);
  • 상자를 엽니다.
  • 상자 다이얼로그 매니저에게 자신의 정보를 전달하여 호출합니다.
  • 상자 다이얼로그 매니저는 다음 글에서 다루겠습니다.
bonnate