아이템 제작 시스템은 게임에 더 많은 선택과 자유도를 부여하며, 캐릭터의 능력을 강화할 수 있고, 게임의 재미와 플레이 시간을 증가시킬 수 있기 때문에 필요합니다. 아이템 제작 시스템을 구현하고 이를 정리하였습니다.
📺 미리보기
💬 목차
- 총 네 개의 목차로 구성되어 있습니다.
- 글 별로 아직 언급되지 않은 클래스의 호출이 있을 수 있습니다. 이 사항은 모든 글을 참조하면 충분히 이해할 수 있습니다.
[📌현재 글] 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"이라는 이름으로 아이템들을 교환하기위한 기능으로 사용할 수 있습니다.
- 제작을 위한 아이콘이 망치가 아닌 악수를 하는 이미지로 바뀌어 제작 시스템과 동일한 구성이지만 다른 느낌의 기능을 보여줍니다.
'unity game modules' 카테고리의 다른 글
[유니티] 상자 시스템(2) - 상자 다이얼로그 (0) | 2023.04.02 |
---|---|
[유니티] 상자 시스템(1) - 상자 데이터 (0) | 2023.04.02 |
[유니티] 제작 시스템(3) - 제작소 (0) | 2023.04.02 |
[유니티] 제작 시스템(2) - 제작 슬롯 (0) | 2023.04.02 |
[유니티] 제작 시스템(1) - 레시피 (0) | 2023.04.02 |