아이템 제작 시스템은 게임에 더 많은 선택과 자유도를 부여하며, 캐릭터의 능력을 강화할 수 있고, 게임의 재미와 플레이 시간을 증가시킬 수 있기 때문에 필요합니다. 아이템 제작 시스템을 구현하고 이를 정리하였습니다.

📺 미리보기

 

💬 목차

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

1. [유니티] 제작 시스템(1) - 레시피

[📌현재 글]  2. [유니티] 제작 시스템(2) - 제작 슬롯

3. [유니티] 제작 시스템(3) - 제작소

4. [유니티] 제작 시스템(4) - 매니저

 

📖 구현 내용

  • 플레이어의 인벤토리에 있는 아이템을 재료로 하여 아이템을 제작할 수 있습니다.
  • 아이템 제작에는 시간이 소요되며 시간을 기다린 후에만 아이템 교환이 이뤄집니다.
  • 아이템 제작 시간 도중 창을 닫는경우 아이템 교환은 이뤄지지 않습니다.
  • 아이템을 제작중인경우 현재 제작중인 아이템을 또 제작하거나, 다른 아이템을 제작할 수 없습니다.

 

✅ 구현

  • 이번 글에서는 제작 레시피를 하나의 슬롯으로 보여주기위한 레시피 슬롯을 구현하고 이를 정리하였습니다.
  • 이 글에서 다루는 기능에서 사용되는 아이템 슬롯(InventorySlot)은 블로그에서 다룬 [인벤토리 시스템]을 기반으로 합니다.

 

· CraftingSlot.cs

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;
using TMPro;

namespace CraftingSystem
{
    public class CraftingSlot : MonoBehaviour
    {
        private static bool mIsCrafting = false;
        public static bool IsCrafting
        {
            get
            {
                return mIsCrafting;
            }
        }

        [Header("제작 결과 아이템의 슬롯")]
        [SerializeField] private InventorySlot mResultItemSlot;

        [Header("제작에 필요한 재료 아이템을 담는 슬라이드 콘텐츠 트랜스폼")]
        [SerializeField] private Transform mRecipeContentTransform;

        [Header("제작 버튼")]
        [SerializeField] private Button mCraftingButton;

        [Header("제작 시간 텍스트 라벨")]
        [SerializeField] private TextMeshProUGUI mCraftingTimeLabel;

        [Header("제작 진행도 이미지")]
        [SerializeField] private Image mCraftingProgressImage;

        [Header("비활성화 상태시 보여줄 이미지 오브젝트")]
        [SerializeField] private GameObject mDisableImageGo;

        /// <summary>
        /// 현재 해당 슬롯이 사용중인 레시피
        /// </summary>
        [HideInInspector] public CraftingRecipe CurrentRecipe;
        private Coroutine? mCoCraftItem; // 제작 연출 및 시간 계산 코루틴

        private void OnDisable()
        {
            if (mCoCraftItem is not null)
                StopCoroutine(mCoCraftItem);

            mIsCrafting = false;
        }

        public void Init(CraftingRecipe recipe)
        {
            CurrentRecipe = recipe;

            // 슬롯 활성화
            gameObject.SetActive(true);

            // 제작 결과 아이템을 슬롯에 등록
            mResultItemSlot.ClearSlot();
            InventoryMain.Instance.AcquireItem(recipe.resultItem.item, mResultItemSlot, recipe.resultItem.count);

            // Ui 요소 초기화
            mCraftingTimeLabel.text = $"{recipe.craftingTime.ToString("F1")}s";
            mCraftingProgressImage.fillAmount = 1.0f;
            mCraftingButton.GetComponent<Image>().sprite = recipe.buttonSprite;

            // 제작 레시피의 재료 아이템 슬롯이 부족하면 개수에 맞게 인스턴스
            for (int i = mRecipeContentTransform.childCount; i <= recipe.reqItems.Length; ++i)
                Instantiate(mResultItemSlot, Vector3.zero, Quaternion.identity, mRecipeContentTransform);

            // 모든 재료 아이템 슬롯을 초기화
            for (int i = 0; i < mRecipeContentTransform.childCount; ++i)
            {
                // 슬롯 획득
                InventorySlot recipeSlot = mRecipeContentTransform.GetChild(i).GetComponent<InventorySlot>();

                // 레시피의 재료 개수보다 작은 인덱스 번호라면?
                if (i < recipe.reqItems.Length)
                {
                    // 슬롯에 아이템을 등록
                    recipeSlot.ClearSlot();
                    InventoryMain.Instance.AcquireItem(recipe.reqItems[i].item, recipeSlot, recipe.reqItems[i].count);
                    recipeSlot.gameObject.SetActive(true);
                }
                else
                {
                    recipeSlot.gameObject.SetActive(false);
                }
            }
        }

        public void ToggleSlotState(bool isCraftable)
        {
            mDisableImageGo.SetActive(!isCraftable);
            mCraftingButton.interactable = isCraftable;
        }

        /// <summary>
        /// 재료 아이템을 제거하고, 결과 아이템을 획득
        /// </summary>
        private void RefreshItems()
        {
            InventorySlot mainInventoryslot = null;

            // 재료 아이템 정보를 확인하여 메인 인벤토리의 아이템을 제거
            foreach (CraftingItemInfo info in CurrentRecipe.reqItems)
            {
                InventoryMain.Instance.HasItemInInventory(info.item.ID, out mainInventoryslot, info.count);
                mainInventoryslot.UpdateSlotCount(-info.count);
            }

            // 제작 후 결과 아이템을 인벤토리에 획득
            InventoryMain.Instance.AcquireItem(CurrentRecipe.resultItem.item, CurrentRecipe.resultItem.count);

            // 아이템을 교환 후 슬롯들을 업데이트
            CraftingManager.Instance.RefreshAllSlots();
        }

        private IEnumerator CoCraftItem()
        {
            mIsCrafting = true;

            // 사운드 재생
            SoundManager.Instance.PlaySound2D("Craft Sound " + SoundManager.Range(1, 2));

            float process = 0f;
            while (process < 1f)
            {
                process += Time.deltaTime / CurrentRecipe.craftingTime;
                mCraftingProgressImage.fillAmount = Mathf.Lerp(0f, 1f, process);

                yield return null;
            }

            // 아이템 획득
            RefreshItems();
            mIsCrafting = false;
        }

        #region Ui
        public void BTN_Craft()
        {
            if (mIsCrafting)
            {
                DialogBox.DialogBoxController dialogBox = DialogBoxGenerator.Instance.CreateSimpleDialogBox("알림", $"이미 제작중입니다.", "확인", DialogBox.DialogBoxController.RESERVED_EVENT_CLOSE, null, 160, 100);
                dialogBox.ModifyBottomLayoutPadding(50, 50);
                return;
            }

            if (mCoCraftItem is not null)
                StopCoroutine(mCoCraftItem);
            mCoCraftItem = StartCoroutine(CoCraftItem());
        }
        #endregion
    }
}

 

private static bool mIsCrafting = false;
public static bool IsCrafting
{
    get
    {
        return mIsCrafting;
    }
}
  • 현재 제작을 하고 있는지 확인하기위한 전역 변수입니다.
  • 제작을 중복으로 하지 않도록 하기위함, 또한 기타 조건에도 사용할 수 있습니다.

 

[Header("제작 결과 아이템의 슬롯")]
[SerializeField] private InventorySlot mResultItemSlot;
  • 제작 아이템의 결과를 보여줄 아이템 슬롯입니다.

 

[Header("제작에 필요한 재료 아이템을 담는 슬라이드 콘텐츠 트랜스폼")]
[SerializeField] private Transform mRecipeContentTransform;
  • 아이템 제작에 요구되는 재료 아이템들을 보여줄 때 요구되는 아이템의 종류가 많은경우 한 슬롯에 모두 표시할 수 없습니다.
  • 이 문제를 개선하기위해 스크롤 뷰를 사용하여 스크롤을 하여 어떤 아이템이 필요한지 모두 확인할 수 있게합니다.
  • 아이템 슬롯을 인스턴스할 때 위치시킬 스크롤 뷰의 콘텐츠 트랜스폼을 이곳에 등록합니다.

 

[Header("제작 버튼")]
[SerializeField] private Button mCraftingButton;
  • 제작 버튼을 활성화 / 비활성화 하기 위해 사용합니다.

 

[Header("제작 시간 텍스트 라벨")]
[SerializeField] private TextMeshProUGUI mCraftingTimeLabel;
  • 제작에 소요되는 시간 및 제작 중 남은 시간을 표시하기위한 라벨입니다.

 

[Header("제작 진행도 이미지")]
[SerializeField] private Image mCraftingProgressImage;
  • 제작시 진행중인 진척도를 표시하기위한 이미지입니다.

 

[Header("비활성화 상태시 보여줄 이미지 오브젝트")]
[SerializeField] private GameObject mDisableImageGo;
  • 제작이 불가능할 때 슬롯 위에 이 오브젝트를 덮어 시각적으로 제작이 불가능하다는것을 보여주기위해 사용하는 오브젝트입니다.

 

private void OnDisable()
{
    if (mCoCraftItem is not null)
        StopCoroutine(mCoCraftItem);

    mIsCrafting = false;
}
  • 제작 다이얼로그 창을 닫아 제작을 더 이상 하지 않거나, 이미 제작중인 아이템을 취소하는경우 호출됩니다.
  • 현재 실행중인 코루틴을 중단하고 mIsCrafting을 false로 설정합니다.

 

public void Init(CraftingRecipe recipe)
{
    CurrentRecipe = recipe;

    // 슬롯 활성화
    gameObject.SetActive(true);

    // 제작 결과 아이템을 슬롯에 등록
    mResultItemSlot.ClearSlot();
    InventoryMain.Instance.AcquireItem(recipe.resultItem.item, mResultItemSlot, recipe.resultItem.count);

    // Ui 요소 초기화
    mCraftingTimeLabel.text = $"{recipe.craftingTime.ToString("F1")}s";
    mCraftingProgressImage.fillAmount = 1.0f;
    mCraftingButton.GetComponent<Image>().sprite = recipe.buttonSprite;

    // 제작 레시피의 재료 아이템 슬롯이 부족하면 개수에 맞게 인스턴스
    for (int i = mRecipeContentTransform.childCount; i <= recipe.reqItems.Length; ++i)
        Instantiate(mResultItemSlot, Vector3.zero, Quaternion.identity, mRecipeContentTransform);

    // 모든 재료 아이템 슬롯을 초기화
    for (int i = 0; i < mRecipeContentTransform.childCount; ++i)
    {
        // 슬롯 획득
        InventorySlot recipeSlot = mRecipeContentTransform.GetChild(i).GetComponent<InventorySlot>();

        // 레시피의 재료 개수보다 작은 인덱스 번호라면?
        if (i < recipe.reqItems.Length)
        {
            // 슬롯에 아이템을 등록
            recipeSlot.ClearSlot();
            InventoryMain.Instance.AcquireItem(recipe.reqItems[i].item, recipeSlot, recipe.reqItems[i].count);
            recipeSlot.gameObject.SetActive(true);
        }
        else
        {
            recipeSlot.gameObject.SetActive(false);
        }
    }
}
  • 제작 슬롯을 초기화합니다.
  • 레시피를 매개변수로 사용하여 레시피를 기반으로 이 슬롯을 초기화하도록 합니다.
  • 레시피에 필요한 아이템의 개수만큼 인벤토리 슬롯을 생성하고 해당 슬롯에 등록합니다.
  • 만약 인벤토리 슬롯이 이미 있다면, 해당 슬롯을 재사용합니다.

 

public void ToggleSlotState(bool isCraftable)
{
    mDisableImageGo.SetActive(!isCraftable);
    mCraftingButton.interactable = isCraftable;
}
  • 해당 아이템을 제작할 수 있는지 토글합니다.

 

/// <summary>
/// 재료 아이템을 제거하고, 결과 아이템을 획득
/// </summary>
private void RefreshItems()
{
    InventorySlot mainInventoryslot = null;

    // 재료 아이템 정보를 확인하여 메인 인벤토리의 아이템을 제거
    foreach (CraftingItemInfo info in CurrentRecipe.reqItems)
    {
        InventoryMain.Instance.HasItemInInventory(info.item.ID, out mainInventoryslot, info.count);
        mainInventoryslot.UpdateSlotCount(-info.count);
    }

    // 제작 후 결과 아이템을 인벤토리에 획득
    InventoryMain.Instance.AcquireItem(CurrentRecipe.resultItem.item, CurrentRecipe.resultItem.count);

    // 아이템을 교환 후 슬롯들을 업데이트
    CraftingManager.Instance.RefreshAllSlots();
}
  • 아이템을 제작하여 해당 아이템 제작 소요시간이 지난경우, 아이템을 교환합니다.
  • 플레이어의 인벤토리에서 재료 해당 아이템을 제거하고 제작 결과 아이템을 인벤토리에 지급합니다.

 

private IEnumerator CoCraftItem()
{
    mIsCrafting = true;

    // 사운드 재생
    SoundManager.Instance.PlaySound2D("Craft Sound " + SoundManager.Range(1, 2));

    float process = 0f;
    while (process < 1f)
    {
        process += Time.deltaTime / CurrentRecipe.craftingTime;
        mCraftingProgressImage.fillAmount = Mathf.Lerp(0f, 1f, process);

        yield return null;
    }

    // 아이템 획득
    RefreshItems();
    mIsCrafting = false;
}
  • 아이템을 제작하는 소요시간만큼 UI요소를 갱신하고 시간이 경과했다면 아이템 교환을 하기위한 코루틴입니다.

 

public void BTN_Craft()
{
    if (mIsCrafting)
    {
        DialogBox.DialogBoxController dialogBox = DialogBoxGenerator.Instance.CreateSimpleDialogBox("알림", $"이미 제작중입니다.", "확인", DialogBox.DialogBoxController.RESERVED_EVENT_CLOSE, null, 160, 100);
        dialogBox.ModifyBottomLayoutPadding(50, 50);
        return;
    }

    if (mCoCraftItem is not null)
        StopCoroutine(mCoCraftItem);
    mCoCraftItem = StartCoroutine(CoCraftItem());
}

 

· UI 프리팹

  • 레시피를 보여줄 슬롯을 만듭니다.
  • 이 슬롯을 프리팹으로 만들고 사용할 수 있도록 합니다.

 

  • 왼쪽의 빈 영역이 재료 아이템이 들어갈 곳, 우측 영역이 지급되는 아이템입니다.
  • 망치 모양은 제작 모양입니다. 레시피에서 buttonSprite을 이용하여 해당 이미지를 바꿀 수 있습니다.
  • 화살표 이미지는 현재 제작중인 아이템의 진척도를 보여줍니다.
  • 화살표 이미지 중심에 텍스트를 이용하여 얼마나 소요되는지, 얼마나 남았는지 볼 수 있습니다.

 

  • 전체적인 구성은 다음과 같습니다.

 

 

bonnate