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

 

📺 미리보기

 

💬 목차

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

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

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

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

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

 

📖 구현 내용

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

 

✅ 구현

  • 이번 글에서는 제작소가 자신의 제작 레시피와 설정정보를 전달하여 UI 다이얼로그창을 띄워 플레이어가 제작소를 이용할 수 있도록 하는 매니저를 구현합니다.

 

· CraftingManager.cs

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

namespace CraftingSystem
{
    public class CraftingManager : Singleton<CraftingManager>
    {
        private static bool mIsDialogActive = false;
        public static bool IsDialogActive
        {
            get
            {
                return mIsDialogActive;
            }
        }

        [Header("Crafting Station에 상관 없이 전역으로 사용 가능한 레시피")]
        [SerializeField] private CraftingRecipe[] mGlobalRecipes;

        [Header("전역 레시피를 사용하지 않을때 슬롯들을 임시로 둘 트랜스폼")]
        [SerializeField] private Transform mGlobalRecipesTemporaryPlacement;        

        [Header("Crafting 다이얼로그 창 오브젝트")]
        [SerializeField] private GameObject mCraftingDialogGo;

        [Header("Crafting 다이얼로그에 레시피를 배치할 Content 트랜스폼")]
        [SerializeField] private Transform mRecipeContentTransform;

        [Header("Crafting Slot 프리팹")]
        [SerializeField] private GameObject mRecipeSlotPrefab;

        [Space(30)][Header("Ui 요소들")]
        [Header("제작 가능한 아이템만 보도록 하는 토글")] [SerializeField] private Toggle mViewCraftableOnlyToggle;
        [Header("다이얼로그 창 타이틀")] [SerializeField] private TextMeshProUGUI mTitleLabel;

        CraftingSlot[] mGlobalRecipeSlots; // 글로벌 레시피 슬롯들
        List<CraftingSlot> mStationOnlyRecipeSlots = new List<CraftingSlot>(); // 스테이션 고유의 레시피 슬롯들

        private int mCurrentCraftingCount; // 현재 제작 스테이션의 별도 레시피 개수

        private void Awake()
        {
            // 초기화시 전역 활성화상태 해제
            CraftingManager.mIsDialogActive = false;

            Init();
        }

        /// <summary>
        /// 전역 레시피를 초기화
        /// </summary>
        private void Init()
        {
            List<CraftingSlot> globalRecipeSlots = new List<CraftingSlot>();
            foreach(CraftingRecipe recipe in mGlobalRecipes)
            {
                // 인스턴스 및 초기화
                CraftingSlot craftingSlot = Instantiate(mRecipeSlotPrefab, Vector3.zero, Quaternion.identity, mGlobalRecipesTemporaryPlacement).GetComponent<CraftingSlot>();
                craftingSlot.Init(recipe);

                // 리스트에 삽입
                globalRecipeSlots.Add(craftingSlot);
            }

            mGlobalRecipeSlots = globalRecipeSlots.ToArray();
        }

        /// <summary>
        /// 다이얼로그 열기를 시도
        /// </summary>
        /// <param name="recipes">다이얼로그에 포함시킬 레시피</param>
        /// <param name="useGlobalRecipes">전역 레시피를 사용하는가?</param>
        public void TryOpenDialog(CraftingRecipe[] recipes, bool useGlobalRecipes, string title)
        {
            // 이미 다이얼로그가 켜져있으면?
            if(mIsDialogActive)
                return;

            // 부족한 레시피 슬롯 오브젝트를 인스턴스 및 리스트에 관리
            for(int i = mStationOnlyRecipeSlots.Count; i < recipes.Length; ++i)
            {
                CraftingSlot craftingSlot = Instantiate(mRecipeSlotPrefab, Vector3.zero, Quaternion.identity, mRecipeContentTransform).GetComponent<CraftingSlot>();
                mStationOnlyRecipeSlots.Add(craftingSlot);
            }

            // 모든 슬롯을 검사하여 활성화 및 비활성화
            for(int i = 0; i < mStationOnlyRecipeSlots.Count; ++i)
            {
                // 레시피의 개수보다 작은 인덱스 번호라면?
                if (i < recipes.Length)
                    mStationOnlyRecipeSlots[i].Init(recipes[i]);
                else
                    mStationOnlyRecipeSlots[i].gameObject.SetActive(false);
            }

            // 글로벌 레시피 사용 유무 설정
            if (useGlobalRecipes)
                foreach (CraftingSlot globalRecipe in mGlobalRecipeSlots)
                    globalRecipe.transform.SetParent(mRecipeContentTransform);

            // 다이얼로그 박스 활성화
            mCraftingDialogGo.gameObject.SetActive(true);

            mTitleLabel.text = title;
            mCurrentCraftingCount = recipes.Length;
            mIsDialogActive = true;
            UtilityManager.UnlockCursor();

            // 모든 슬롯을 갱신
            RefreshAllSlots();
        }

        /// <summary>
        /// 다이얼로그를 닫음
        /// </summary>
        public void CloseDialog()
        {
            // 글로벌 레시피를 모두 옮김
            foreach (CraftingSlot globalRecipe in mGlobalRecipeSlots)
                globalRecipe.transform.SetParent(mGlobalRecipesTemporaryPlacement);

            // 다이얼로그 비활성화
            mCraftingDialogGo.SetActive(false);

            mIsDialogActive = false;
            UtilityManager.TryLockCursor();
        }        

        /// <summary>
        /// 해당 슬롯을 이용 가능한지 검사하여 슬롯에 상태를 적용
        /// </summary>
        /// <param name="craftingSlot"></param>
        private void CheckCraftingSlot(CraftingSlot craftingSlot)
        {
            // 슬롯을 활성화
            craftingSlot.gameObject.SetActive(true);

            // 요구 아이템이 플레이어의 인벤토리에 있는지 검사
            for(int i = 0; i < craftingSlot.CurrentRecipe.reqItems.Length; ++i)
            {
                // 하나라도 아이템 재료가 없다면 비활성화 상태로 전환
                if (InventoryMain.Instance.HasItemInInventory(craftingSlot.CurrentRecipe.reqItems[i].item.ID, out _, craftingSlot.CurrentRecipe.reqItems[i].count) == false)
                {
                    // 제작이 불가능한 상태에서 ViewCraftableOnly가 켜져있다면?
                    if(mViewCraftableOnlyToggle.isOn)
                        craftingSlot.gameObject.SetActive(false); // 오브젝트 자체를 비활성화
                    else
                        craftingSlot.ToggleSlotState(false); // 제작이 불가능한 상태로 보여지게함

                    return;
                }
            }

            // 요구 아이템이 모두 있는경우 활성화 상태로 전환
            craftingSlot.ToggleSlotState(true);
        }

        /// <summary>
        /// 모든 슬롯을 갱신
        /// </summary>
        public void RefreshAllSlots()
        {
            // 다이얼로그가 비활성화 상태라면?
            if(mIsDialogActive == false)
                return;

            for(int i = 0; i < mCurrentCraftingCount; ++i)
                    CheckCraftingSlot(mStationOnlyRecipeSlots[i]);

            // mGlobalRecipesTemporaryPlacement의 자식 개수가 0이라면?
            if(mGlobalRecipesTemporaryPlacement.childCount == 0)
                foreach(CraftingSlot globalRecipeSlot in mGlobalRecipeSlots)
                    CheckCraftingSlot(globalRecipeSlot);
        }

        #region Ui
        public void TOGGLE_ViewCraftableOnly()
        {
            RefreshAllSlots();
        }

        #endregion
    }
}

 

private static bool mIsDialogActive = false;
public static bool IsDialogActive
{
    get
    {
        return mIsDialogActive;
    }
}
  • 현재 제작 다이얼로그 창이 열려있는지(제작 시스템을 사용 중인지) 확인하기 위한 변수입니다.

 

[Header("Crafting Station에 상관 없이 전역으로 사용 가능한 레시피")]
[SerializeField] private CraftingRecipe[] mGlobalRecipes;
  • 제작소에서 사용할 수 있는 글로벌 레시피입니다.
  • 제작소 별도의 레시피에서 이 글로벌 레시피를 포함할 수 있습니다.

 

[Header("전역 레시피를 사용하지 않을때 슬롯들을 임시로 둘 트랜스폼")]
[SerializeField] private Transform mGlobalRecipesTemporaryPlacement;
  • 전역 레시피를 사용하지 않을 때 해당 레시피들을 임시로 옮겨둘 트랜스폼입니다.

 

[Header("Crafting 다이얼로그 창 오브젝트")]
[SerializeField] private GameObject mCraftingDialogGo;
  • 제작 다이얼로그 창을 활성화 및 비활성화하기 위한 게임오브젝트입니다.

 

[Header("Crafting 다이얼로그에 레시피를 배치할 Content 트랜스폼")]
[SerializeField] private Transform mRecipeContentTransform;
  • 스크롤 뷰에서 제작 레시피 슬롯을 배치하기 위한 콘텐츠 트랜스폼입니다.

 

[Space(30)][Header("Ui 요소들")]
[Header("제작 가능한 아이템만 보도록 하는 토글")] [SerializeField] private Toggle mViewCraftableOnlyToggle;
[Header("다이얼로그 창 타이틀")] [SerializeField] private TextMeshProUGUI mTitleLabel;
  • UI 요소들을 정의합니다.
  • 제작 가능한 아이템만 볼 수 있도록 설정하는 토글과 다이얼로그 창의 타이틀 라벨입니다.

 

/// <summary>
/// 전역 레시피를 초기화
/// </summary>
private void Init()
{
    List<CraftingSlot> globalRecipeSlots = new List<CraftingSlot>();
    foreach(CraftingRecipe recipe in mGlobalRecipes)
    {
        // 인스턴스 및 초기화
        CraftingSlot craftingSlot = Instantiate(mRecipeSlotPrefab, Vector3.zero, Quaternion.identity, mGlobalRecipesTemporaryPlacement).GetComponent<CraftingSlot>();
        craftingSlot.Init(recipe);

        // 리스트에 삽입
        globalRecipeSlots.Add(craftingSlot);
    }

    mGlobalRecipeSlots = globalRecipeSlots.ToArray();
}
  • 씬이 로드되면 글로벌 레시피를 미리 로드하고 프리팹화하여 사용 준비를 완료합니다.
  • 임시로 mGlobalRecipesTemporaryPlacement에 위치하여 글로벌 레시피를 사용할 때만 적절한 위치로 이동시킵니다.

 

/// <summary>
/// 다이얼로그 열기를 시도
/// </summary>
/// <param name="recipes">다이얼로그에 포함시킬 레시피</param>
/// <param name="useGlobalRecipes">전역 레시피를 사용하는가?</param>
public void TryOpenDialog(CraftingRecipe[] recipes, bool useGlobalRecipes, string title)
{
    // 이미 다이얼로그가 켜져있으면?
    if(mIsDialogActive)
        return;

    // 부족한 레시피 슬롯 오브젝트를 인스턴스 및 리스트에 관리
    for(int i = mStationOnlyRecipeSlots.Count; i < recipes.Length; ++i)
    {
        CraftingSlot craftingSlot = Instantiate(mRecipeSlotPrefab, Vector3.zero, Quaternion.identity, mRecipeContentTransform).GetComponent<CraftingSlot>();
        mStationOnlyRecipeSlots.Add(craftingSlot);
    }

    // 모든 슬롯을 검사하여 활성화 및 비활성화
    for(int i = 0; i < mStationOnlyRecipeSlots.Count; ++i)
    {
        // 레시피의 개수보다 작은 인덱스 번호라면?
        if (i < recipes.Length)
            mStationOnlyRecipeSlots[i].Init(recipes[i]);
        else
            mStationOnlyRecipeSlots[i].gameObject.SetActive(false);
    }

    // 글로벌 레시피 사용 유무 설정
    if (useGlobalRecipes)
        foreach (CraftingSlot globalRecipe in mGlobalRecipeSlots)
            globalRecipe.transform.SetParent(mRecipeContentTransform);

    // 다이얼로그 박스 활성화
    mCraftingDialogGo.gameObject.SetActive(true);

    mTitleLabel.text = title;
    mCurrentCraftingCount = recipes.Length;
    mIsDialogActive = true;
    UtilityManager.UnlockCursor();

    // 모든 슬롯을 갱신
    RefreshAllSlots();
}
  • CraftingStation.cs로부터 호출되어 매개변수로 레시피와 글로벌 레시피를 사용하는지, 타이틀은 무엇인지 넘겨주며 초기화를 할 수 있도록 합니다.
  • 부족한 레시피 슬롯 프리팹을 인스턴스 하여 레시피 개수에 맞게 만든 후 해당 슬롯 프리팹에 Init을 호출하여 레시피를 설정합니다.

 

/// <summary>
/// 다이얼로그를 닫음
/// </summary>
public void CloseDialog()
{
    // 글로벌 레시피를 모두 옮김
    foreach (CraftingSlot globalRecipe in mGlobalRecipeSlots)
        globalRecipe.transform.SetParent(mGlobalRecipesTemporaryPlacement);

    // 다이얼로그 비활성화
    mCraftingDialogGo.SetActive(false);

    mIsDialogActive = false;
    UtilityManager.TryLockCursor();
}
  • 다이얼로그 창을 비활성화합니다.

 

/// <summary>
/// 해당 슬롯을 이용 가능한지 검사하여 슬롯에 상태를 적용
/// </summary>
/// <param name="craftingSlot"></param>
private void CheckCraftingSlot(CraftingSlot craftingSlot)
{
// 슬롯을 활성화
craftingSlot.gameObject.SetActive(true);

// 요구 아이템이 플레이어의 인벤토리에 있는지 검사
for(int i = 0; i < craftingSlot.CurrentRecipe.reqItems.Length; ++i)
{
    // 하나라도 아이템 재료가 없다면 비활성화 상태로 전환
    if (InventoryMain.Instance.HasItemInInventory(craftingSlot.CurrentRecipe.reqItems[i].item.ID, out _, craftingSlot.CurrentRecipe.reqItems[i].count) == false)
    {
        // 제작이 불가능한 상태에서 ViewCraftableOnly가 켜져있다면?
        if(mViewCraftableOnlyToggle.isOn)
            craftingSlot.gameObject.SetActive(false); // 오브젝트 자체를 비활성화
        else
            craftingSlot.ToggleSlotState(false); // 제작이 불가능한 상태로 보여지게함

        return;
    }
}

// 요구 아이템이 모두 있는경우 활성화 상태로 전환
craftingSlot.ToggleSlotState(true);
}
  • 해당 슬롯이 현재 제작이 가능한지 검사하여 검사 후 제작 여부를 토글 합니다.

 

/// <summary>
/// 모든 슬롯을 갱신
/// </summary>
public void RefreshAllSlots()
{
    // 다이얼로그가 비활성화 상태라면?
    if(mIsDialogActive == false)
        return;

    for(int i = 0; i < mCurrentCraftingCount; ++i)
            CheckCraftingSlot(mStationOnlyRecipeSlots[i]);

    // mGlobalRecipesTemporaryPlacement의 자식 개수가 0이라면?
    if(mGlobalRecipesTemporaryPlacement.childCount == 0)
        foreach(CraftingSlot globalRecipeSlot in mGlobalRecipeSlots)
            CheckCraftingSlot(globalRecipeSlot);
}
  • 특정 조건에 의해 모든 슬롯이 갱신이 될 필요가 있다면 호출하여 갱신을 합니다.
  • 예를 들어 플레이어가 제작을 하여 소지 아이템이 바뀐 경우 호출할 수 있습니다.

 

public void TOGGLE_ViewCraftableOnly()
{
    RefreshAllSlots();
}
  • 토글에 의해 호출되며 제작 가능한 대상만 볼 것인지 선택합니다.

 

· UI

  • 전체적인 디자인입니다.
  • 상점 시스템과 비슷하게 스크롤 뷰를 이용하여 슬롯들을 보여줄 수 있도록 구현하였습니다.

 

  • 이 UI의 전체적이 구성요소는 다음과 같습니다.
  • CraftingManager은 싱글턴 매니저로 런타임도중 비활성화되지 않도록 만들어준 후 내부 멤버변수를 설정해 줍니다.

 

✅ 활용

  • 꼭 제작소에서 아이템을 제작하는것이 아닌 이미지의 우측과 같이 "SUPPLIER"이라는 이름으로 아이템들을 교환하기위한 기능으로 사용할 수 있습니다.
  • 제작을 위한 아이콘이 망치가 아닌 악수를 하는 이미지로 바뀌어 제작 시스템과 동일한 구성이지만 다른 느낌의 기능을 보여줍니다.
bonnate