0.들어가기에 앞서
인벤토리를 다시 설계해 보자. 그리고 기존에 진행해야 하는 크래프팅 및 분해 관련 개발을 진행해야 한다.
정확히는 어제 저녁부터 지금까지 진행한 업무라고 할 수 있겠다. 인벤토리는 새로 만든 구조로 복구했고, 버그 한 가지 정도 발생하는 것 외에는 거의 원형에 가깝게 복구했다.
인벤토리를 새로운 방식으로 구현
인벤토리 내 스택 아이템 분할 빼고는 거의 모든 기능을 복구했다.
인벤토리 전반적 기능 테스트 및 크래프팅 레시피 버튼 연동 테스트
인벤토리 관련으로 전반적으로 제작하는 방식을 많이 변경했다.
UI 부분에서 연동되는 것을 최소화하고자 구조를 짰고, 특히 데이터 관리를 하는 부분을 더 간단하게 하는 데 집중했다.
싱글톤으로 구사하는 걸 시도해 보는 참에 데이터 관리 쪽은 최대한 손이 덜 가는 방향으로 만드는 것이 목적이다.
앞으로 기능을 구현하거나 개편을 해야 할 수도 있기 때문에 클래스 다이어그램을 만들어놓기로 했다.
이전에는 거의 10개의 스크립트를 사용한 구현방법이었지만, 이번에는 가능한 복잡한 방향으로 가지 않도록 하기 위해, UI 쪽에서는 연산과정 - 아이템의 교체 및 기타 복잡한 과정이 거의 들어가지 않도록 설계했다.
복잡한 공간 계산 부분은 전부 시스템 부문의 InventoryManager에 몰아주고, UI는 말 그대로 출력만 할 수 있게 한 것이다.
이를 MVC 패턴과 비교해보자면, InventoryController와 HotbarController가 사실상 View라고 할 수 있고, 실질적인 컨트롤 부분은 ItemSlotUnit과 그 상속된 유닛들이 담당하고 있다. 여기서의 조작을 InventoryManager의 Model의 변동에 따라 Invoke로 컨트롤러가 다시 연동하여 작동하는 방식이다.
게임 내에 인벤토리 매니저를 갖는다. 어차피 인벤토리가 중요한 게임이다 보니, 꼭 플레이어가 갖고 있는 것이 아닌, 아예 매니저 형태로 빼놓은 것이다.
using System;
using UnityEngine;
public class InventoryManager : MonoBehaviour
{
#region Singleton
public static InventoryManager Instance { get; private set; }
private void Awake()
{
if (Instance != null && Instance != this)
{
Destroy(gameObject);
return;
}
Instance = this;
DontDestroyOnLoad(gameObject);
Initialize();
}
#endregion // Singleton
#region Init
private void Initialize()
{
_inventoryItem = new ItemSO[25];
_inventoryStack = new int[_inventoryItem.Length];
}
#endregion
public int InventoryCount => _inventoryItem.Length;
private ItemSO[] _inventoryItem;
private int[] _inventoryStack;
public static event Action OnInventorySlotChanged;
/// <summary>
/// Read Data from inventory.
/// </summary>
/// <param name="index"></param>
/// <param name="stack"></param>
/// <returns></returns>
public ItemSO ReadFromInventory(int index, out int stack)
{
stack = _inventoryStack[index];
return _inventoryItem[index];
}
/// <summary>
/// Item Moving in Inventory
/// </summary>
/// <param name="startIndex"></param>
/// <param name="endIndex"></param>
public void MoveItemInInventory(int startIndex, int endIndex)
{
// if there is no item in start cell.
if (startIndex == -1)
{
Debug.Log("시작칸에 아이템 없음");
return;
}
// if there is item in start cell and drag outside the inventory - Drop Method.
if (_inventoryItem[startIndex] != null && endIndex == -1)
{
Debug.Log("아이템 버리기");
Vector2 position = GameObject.FindWithTag("Player").GetComponent<Transform>().position;
for(int i = 0; i < _inventoryStack[startIndex]; i++)
{
// TODO : Where to Instantiate item?
Instantiate(_inventoryItem[startIndex].Prefab, position + Vector2.right, Quaternion.identity);
}
_inventoryItem[startIndex] = null;
_inventoryStack[startIndex] = 0;
OnInventorySlotChanged?.Invoke();
}
// if there is item in start cell and drag into empty cell.
else if (_inventoryItem[startIndex] != null && endIndex != -1 && _inventoryItem[endIndex] == null)
{
Debug.Log("아이템 옮기기");
InventoryTryAdd(_inventoryItem[startIndex], endIndex, _inventoryStack[startIndex]);
_inventoryItem[startIndex] = null;
_inventoryStack[startIndex] = 0;
OnInventorySlotChanged?.Invoke();
}
// if there is item both in start
else if (_inventoryItem[startIndex] != null && _inventoryItem[endIndex] != null)
{
// if item is same and stack is same, return.
if (_inventoryItem[startIndex] == _inventoryItem[endIndex] && _inventoryStack[startIndex] == _inventoryStack[endIndex]) return;
Debug.Log("아이템 위치 바꾸기");
ItemSO tempItem = _inventoryItem[endIndex];
int tempStack = _inventoryStack[endIndex];
_inventoryItem[endIndex] = _inventoryItem[startIndex];
_inventoryStack[endIndex] = _inventoryStack[startIndex];
_inventoryItem[startIndex] = tempItem;
_inventoryStack[startIndex] = tempStack;
OnInventorySlotChanged?.Invoke();
}
}
/// <summary>
/// Add item to Inventory.
/// </summary>
/// <param name="item"></param>
/// <param name="amount"></param>
/// <returns></returns>
public bool AddItemToInventory(ItemSO item, int amount = 1)
{
int remain = amount;
while (remain > 0)
{
for (int i = 0; i < _inventoryItem.Length; i++)
{
if (_inventoryItem[i] == item)
{
remain = InventoryTryAdd(item, i, amount);
if (remain <= 0) break;
}
}
if (remain <= 0) break;
for (int i = 0; i < _inventoryItem.Length; i++)
{
if (_inventoryItem[i] == null)
{
remain = InventoryTryAdd(item, i, amount);
if (remain <= 0) break;
}
}
if (remain <= 0) break;
else
{
Debug.Log("inventory full"); return false;
}
}
return true;
}
/// <summary>
/// Use Item.
/// TODO - Item Using Method - Equip/Potions
/// </summary>
/// <param name="index"></param>
public void UseItem(int index)
{
if (_inventoryItem[index] && _inventoryItem[index].Type != ItemType.Material)
{
_inventoryStack[index]--;
if( _inventoryStack[index] <= 0 )
{
_inventoryItem[index] = null;
_inventoryStack[index] = 0;
}
OnInventorySlotChanged?.Invoke();
}
}
/// <summary>
/// Try add item.
/// </summary>
/// <param name="item"></param>
/// <param name="index"></param>
/// <param name="amount"></param>
/// <returns></returns>
private int InventoryTryAdd(ItemSO item, int index, int amount)
{
_inventoryItem[index] = item;
// If the stack is enough.
if (amount <= (item.MaxStackSize - _inventoryStack[index]))
{
_inventoryStack[index] += amount;
OnInventorySlotChanged?.Invoke();
return 0;
}
// If the stack is not enough and has space.
else if (item.MaxStackSize > _inventoryStack[index])
{
_inventoryStack[index] = item.MaxStackSize;
amount -= (item.MaxStackSize - _inventoryStack[index]);
OnInventorySlotChanged?.Invoke();
return amount;
}
// If the stack is full.
else
{
return amount;
}
}
}
전체적인 로직을 짜는 데 시간이 걸리긴 했지만 다행히도 이전에 이미 만들었던 인벤토리 방식에서 어느 정도 구조를 가져와서 사용할 수 있었다. 다만 구조 그대로 쓰는 것보다, 내 나름대로 조금 더 눈에 들어오는 코드 형태로 만들었다.
그 다음으로 시간을 많이 들였던 부분이 ItemSlotUnit이다
using TMPro;
using UnityEngine;
using UnityEngine.EventSystems;
using UnityEngine.UI;
public abstract class ItemSlotUnit : MonoBehaviour, IPointerClickHandler, IBeginDragHandler, IDragHandler, IEndDragHandler, IDropHandler
{
[SerializeField] protected int _index;
[SerializeField] protected Image _image;
[SerializeField] protected TMP_Text _text;
protected ItemSO _item;
public ItemSO Item => _item;
protected int _itemStack;
public int ItemStack => _itemStack;
// It should be saved as static, because each cells save different data
protected static int _startDragPoint;
protected static int _endDragPoint;
public abstract void Awake();
public abstract void UpdateUI(int index);
/// <summary>
/// Item Use when click.(Interface)
/// </summary>
/// <param name="eventData"></param>
public void OnPointerClick(PointerEventData eventData)
{
if (_item != null)
{
_item.Prefab.GetComponent<ItemController>().Use();
InventoryManager.Instance.UseItem(_index);
UpdateUI(_index);
}
}
/// <summary>
/// When Drag Start.(Interface)
/// </summary>
/// <param name="eventData"></param>
public void OnBeginDrag(PointerEventData eventData)
{
_startDragPoint = -1;
_endDragPoint = -1;
if (_item != null)
{
DragSlot.Instance.DragSetSlot(this);
DragSlot.Instance.transform.position = eventData.position;
DragSlot.Instance.CurrentSlot = this;
_startDragPoint = this._index;
}
Debug.Log($"{_startDragPoint} start");
Debug.Log($"{_endDragPoint} end");
}
/// <summary>
/// While Dragging.(interface)
/// </summary>
/// <param name="eventData"></param>
public void OnDrag(PointerEventData eventData)
{
if (_item != null) DragSlot.Instance.transform.position = eventData.position;
}
/// <summary>
/// When mouse button Up.(interface)
/// </summary>
/// <param name="eventData"></param>
public void OnDrop(PointerEventData eventData)
{
_endDragPoint = this._index;
}
/// <summary>
/// End Dragging.(interface)
/// </summary>
/// <param name="eventData"></param>
public void OnEndDrag(PointerEventData eventData)
{
DragSlot.Instance.ClearDragSlot();
InventoryManager.Instance.MoveItemInInventory(_startDragPoint, _endDragPoint);
}
}
인벤토리 내 아이템의 이동 방식을 구현하기 위해서 인터페이스를 활용했다.
특히 언제 어떤 이벤트가 발생해야 하고 신중하게 진행해야 해서, 꽤 고전했던 부분인데 여기서 발생했던 문제와 해결 방식은 후술하고자 한다.
딱 여기까지만 로직 구성 방식이 복잡하고, 이후로는 정말 별 거 없다. 나머지 부분은 이제 UI출력만 하면 되는 부분으로 구성했다.
using UnityEngine;
/// <summary>
/// For Inventory Slot Unit.
/// </summary>
public class InventorySlotUnit : ItemSlotUnit
{
public override void Awake()
{
_image.color = Color.clear;
_text.text = "";
}
/// <summary>
/// Update UI.
/// </summary>
/// <param name="index"></param>
public override void UpdateUI(int index)
{
_item = InventoryManager.Instance.ReadFromInventory(index, out int stack);
if(_item == null)
{
_image.color = Color.clear;
_itemStack = 0;
_text.text = "";
}
else
{
_image.color = Color.white;
_image.sprite = _item.Icon;
_itemStack = stack;
_text.text = stack > 1 ? stack.ToString() : "";
}
}
}
using UnityEngine;
/// <summary>
/// For static Hotbar(quickslot) Slot Unit.
/// </summary>
public class HotBarSlotUnit : ItemSlotUnit
{
public override void Awake()
{
_image.color = Color.clear;
_text.text = "";
}
/// <summary>
/// Update UI.
/// </summary>
/// <param name="index"></param>
public override void UpdateUI(int index)
{
_item = InventoryManager.Instance.ReadFromInventory(index, out int stack);
if (_item == null)
{
_image.color = Color.clear;
_text.text = "";
}
else
{
_image.color = Color.white;
_image.sprite = _item.Icon;
_text.text = stack > 1 ? stack.ToString() : "";
}
}
}
using TMPro;
using UnityEngine;
using UnityEngine.UI;
/// <summary>
/// Drag image Slot.
/// </summary>
public class DragSlot : MonoBehaviour
{
static public DragSlot Instance;
public ItemSlotUnit CurrentSlot;
[SerializeField] private Image _image;
[SerializeField] private TMP_Text _text;
private void Start()
{
Instance = this;
_image.color = Color.clear;
_text.text = "";
}
/// <summary>
/// If item is Selected, image apply.
/// </summary>
/// <param name="slot"></param>
public void DragSetSlot(ItemSlotUnit slot)
{
if (slot.Item != null)
{
_image.color = Color.white;
_image.sprite = slot.Item.Icon;
_text.text = slot.ItemStack.ToString();
}
}
/// <summary>
/// If item is Deselected, clear image.
/// </summary>
public void ClearDragSlot()
{
_image.color = Color.clear;
_text.text = "";
}
}
using TMPro;
using UnityEngine;
public class InventoryController : MonoBehaviour
{
[SerializeField] InventorySlotUnit[] slots;
[SerializeField] TMP_Text _weightText;
private void OnEnable()
{
InventoryManager.OnInventorySlotChanged += UpdateUISlot;
InventoryManager.OnInventorySlotChanged += UpdateWeightText;
UpdateUISlot();
UpdateWeightText();
}
private void OnDisable()
{
InventoryManager.OnInventorySlotChanged -= UpdateUISlot;
InventoryManager.OnInventorySlotChanged -= UpdateWeightText;
}
private void UpdateUISlot()
{
for (int i = 0; i < slots.Length; i++)
{
slots[i].UpdateUI(i);
}
}
private void UpdateWeightText()
{
float weight = 0;
for (int i = 0; i < slots.Length; i++)
{
if (slots[i].Item != null) weight += slots[i].Item.Weight * slots[i].ItemStack;
}
_weightText.text = $"kg {weight.ToString()} / 25";
}
}
using UnityEngine;
public class HotbarController : MonoBehaviour
{
[SerializeField] HotBarSlotUnit[] _hotBarSlot;
[SerializeField] int[] _inventoryitemSlot;
private void OnEnable()
{
InventoryManager.OnInventorySlotChanged += UpdateUISlot;
}
private void OnDisable()
{
InventoryManager.OnInventorySlotChanged -= UpdateUISlot;
}
private void UpdateUISlot()
{
for(int i = 0; i < _hotBarSlot.Length; i++)
{
_hotBarSlot[i].UpdateUI(_inventoryitemSlot[i]);
}
}
}
이 부분의 구성 방식은 사실 기존의 인벤토리 구현 방식과 좀 달라진 부분이다. 기존에는 이런 핫바 또한 인벤토리의 일부로 보면서 진행하는 튜토리얼을 보고서 만들었었다. 따라서 핫바 부분에 공간이 먼저 채워지고 그 다음에 인벤토리가 채워지는 방식으로 진행했었다.
하지만 이 게임에는 무게 시스템도 있고, 핫바도 인벤토리로 포함하면 뭔가 문제가 생기지 않을까 고민이 들었다. 특히나 기획에서도 이것을 퀵슬롯의 개념으로 생각하는 경향이 있는 것으로 보여, 처음에는 핫바용 공간을 따로 만들려던 생각을 변경해, 아예 핫바를 인벤토리 UI와 연동했다.
다만 퀵슬롯이 연동할 인벤토리 칸의 아이템의 경우 아직은 자유롭게 설정하진 못한 상황이다. 또 기획의 의도처럼 재료는 퀵슬롯에 들어가지 못하게 처리하는 것도 아직은 R&D 진행 중에 있다.
인스펙트 상으로는 조금 무식하게 만들어 놓긴 했다. 우선은 인벤토리의 0 ~ 7 슬롯까지 퀵슬롯으로 만들어놨으며 구현이 필요하다고 생각되는 부분은 다음과 같다.
이 부분이 오늘 오랫동안 붙잡았던 아이템 이동과 관련된 문제 및 해결과정으로, 두 파트로 나눠서 서술하고자 한다.
이번에 인벤토리의 전체적인 기능을 변경하면서, 아이템의 이동 방식은 인터페이스를 활용하는 방식으로 구성했었다.
하지만 이상한 현상을 목격하게 되었는데 바로 IDropHandler로 구성한 OnDrop이 이상하게 작동한다는 것이었다.
분명히 이와 같이 내용을 살펴봐도, OnDrop이란 기능은 마우스를 뗐을 때의 지점에 있는 오브젝트에서 호출되는 기능이라고 생각했다. 그러므로 이 아이템슬롯 추상클래스의 모든 자식 클래스는 해당 효과를 받을 것이므로 이 OnDrop 부분에서 가진 인덱스를 가져올 것이라고 생각했다.
하지만 막상 이 부분에서 eventData의 좌표상 인덱스를 호출하면 문제가 생겼다. 분명 다른 위치의 좌표에서 마우스버튼을 뗐는데, 이상하게 처음 시작한 부분의 인덱스가 호출되는 것이었다. 이와 같은 문제가 왜 발생하는 걸까?
처음 사용했던 코드를 다시 살펴보면 다음과 같다.
_endDragPoint = eventData.pointerDrag.GetComponent<ItemSlotUnit>()._index;
이와 같은 방식이 지금 현재의 위치를 가져오는 것이라고 생각했는데, 알고 보니 드래그를 한 시점의 인덱스를 다시 불러오는 것이었다.
사용방식을 잘못 이해한 경우였다. 그래서 위와 같은 코드를 아래와 같이 고치니 문제가 해결되었다.
_endDragPoint = this._index;
이제 좌표는 멀쩡하게 찍히니 아이템이 정상적으로 옮겨져야 하는데 또 문제가 생겼다. 아이템을 이동시키고 나서는 제대로 옮겨지지 않고 심지어 이동을 시도했던 칸은 상호작용이 되지 않는 문제가 발생했다.
이런 문제가 왜 발생할까, 고민을 해 보다가 문득 아이템을 옮길 때 따라오는 이미지가 마우스와 겹쳐져서 인벤토리 슬롯을 가리는 것이 아닐까 생각이 들었다.
DragSlot의 위치를 보니 그게 맞는 것 같기는 했지만, 그렇다면 해당 문제를 어떻게 해결해야 할까.
IDragHandler 등 인터페이스들의 기능 작동방식을 찾아보니, 레이캐스트 방식이 들어간다는 것을 확인했다. 그래서 이걸 방지하기 위해서 DragIcon과 관련된 부분을 모두 Ignore Raycast로 처리했다. 이렇게 하면 해결될 줄 알았는데, 이상하게도 이와 같이 무시 처리해도 해결되지 않았다.
결국 여러 방면으로 알아보다가 팁 같은 것을 보게 된 게, DragIcon 같은 경우는 별개의 캔버스로 분리하는 것이 좋다는 걸 보고서 우선 분리 처리를 했다. 하지만 이렇게 분리만 하니 문제가 생긴게, DragIcon 자체가 그냥 인벤토리 슬롯의 뒤로 가버리고 아이템의 이동은 정상 작동되는 방식이 되어버렸다. 그래도 아이콘이 보여야 하니까, 아이콘의 출력 레이어를 높여주니 이젠 또 인벤토리 내 이동이 불가능해졌다.
참 난관이었다. 이걸 이제 어떻게 해결해야 할까 고민하던 끝에, 결국 알아낸 방법은 다음과 같다.
IDragHandler와 같은 방식의 인터페이스는 Raycast 방식으로 동작하는 것이 맞으나, 단순히 Ignore Raycast만 하는 것으로 문제가 바로 해결되지 않을 수 있다. 이를 온전히 동작시키기 위해선, Canvas Group의 Blocks Raycasts를 false로 해야 온전히 해결될 수 있다.
처음에는 이게 무슨 소리인지 바로 이해가 되지 않았는데, 알고 보니 이 Canvas Group라는 컴포넌트가 따로 있었다.
알고 보니 이건 캔버스를 만들 때 기본적으로 생성되는 컴포넌트가 아니고, 추가로 넣어야 하는 컴포넌트였다. 기본 설정은 true로 되어 있기 때문에 이걸 false로까지 해 줘야지 완전히 Raycast 무시가 되는 것이다.
이와 같은 것까지는 생각지도 못했었는데, 유의해야 할 부분으로 보인다.
인벤토리 쪽을 어느 정도 구성한 이후로 슬슬 크래프팅과 관련된 부분에 들어가보려고 했다.
여기서 어떻게든 좀 더 쉽게 기능을 구현할 수 있는 방법에 대해 고민을 많이 했는데, 생각한 방식은 다음과 같았다.
UI 표시와 같은 부분은 최대한 컴팩트하게 처리해 보자. 정말 간단한 기능만 들어가는 UI의 경우는, 버튼 같은 것으로 OnClick() 이벤트를 활용해서 차라리 인스펙터로 연결시켜놓고 해당 UI를 프리팹화시키는 것도 나쁘지 않을지도 모른다.
그래서 이와 같은 생각으로 제일 먼저 구현하려 한 부분은 레시피를 눌렀을 때 해당 아이템의 상세정보를 띄우는 걸 OnClick()으로 처리하는 방법이었다.
구체적으로 생각한 아이디어는 다음과 같았다.
그래서 해당 기능을 구현하기 위해 먼저 이와 같은 방식으로 데이터를 보내보고자 했다.
public CraftingRecipe OnClick()
{
return _recipe;
}
이와 같이 만들고서 OnClick 버튼 이벤트와 연동하려 했는데, 함수가 뜨질 않았다. 이유는 다음과 같았다.
유니티 버튼의 OnClick 이벤트에는 public으로 선언된 메서드, 즉 MonoBehaviour를 상속받는 클래스의 메서드를 할당할 수 있습니다. 이 메서드는 매개변수를 가지지 않아야 하며, 반환값은 void여야 합니다. 간단하게는 버튼을 클릭했을 때 실행될 함수를 스크립트에서 작성하고, 유니티 에디터에서 해당 버튼의 OnClick 이벤트에 스크립트와 함수를 연결해주면 됩니다.
그렇다면 void 형식으로 만든 함수만 OnClick 이벤트로 연결할 수 있다는 건데, 어떻게 void형식으로 데이터를 보낼 수 있을까. 이 부분에 대해서는 뚜렷하게 답이 나오지 않았다.
지금 보내려는 데이터가 단순히 String이나 숫자 데이터 같은 경우였으면 보낼 방법이 message 등의 함수를 사용할 수 있었지만, 지금 보내려는 건 아무래도 Scriptable Object다 보니 쉽지 않았다. 정녕 방법이 없을까 고민하던 때에 결국 사용한 방식은 다음과 같다.
public void OnClick()
{
GetComponentInParent<CraftingController>().SelectRecipe(_recipe);
Debug.Log("전달");
}
레시피를 가져오는 함수 자체를 상대 컴포넌트에 구현해 놓고, 함수를 가져와서 데이터를 넘겨주는 방식이다.
이와 같은 방식으로 어렵게 데이터를 넘기는 방식을 구현했다.
현재 인벤토리에서 발생하는 버그는 하나로, 게임을 실행했을 때의 딱 초기 한 번, 인벤토리가 바로 업데이트가 되지 않는 문제가 있다.
예를 들어 초기에 인벤토리를 열지 않고 아이템을 먹고 난 후 인벤토리를 열면 인벤토리가 비어있는 것을 확인할 수 있었다.
(막상 실제 칸에는 아이템이 들어있다. 그래서 빈칸으로 보이는 아이템이 들어있는 칸을 누르면 바로 싹 다 상태가 업데이트된다.)
Awake로 UpdateUI 처리나 여러 방면으로 분석 중이긴 한데 생성되지마자 바로 꺼지는 문제 때문에 Update가 바로 적용되지 않는 건지, 원인은 좀 살펴봐야 할 것으로 보인다.
상세정보 업데이트 내용을 구현했고, 데이터를 받아오는 방식을 이용하여 '현재 선택된 레시피 데이터'를 저장할 수 있는 방법도 구현했다. 이제 남은 건 아이템 제작 여부를 판정하고 결과를 내보내는 것까지 하는 것이다.
결과 아이템 슬롯도 마찬가지로 버튼을 누르면 아이템이 인벤토리에 추가되는 방식 같은 걸 생각하고 있긴 하다.
(처음엔 복잡하게 결과 아이템 슬롯을 따로 만들어서 드래그가 가능하게 해야 하나 생각하기도 했는데, 당장은 제일 빠르게 구현할 수 있는 버튼 클릭 형식으로 보여주는 데 집중해보고자 한다.
분해 시스템의 경우 크래프팅보다는 후순위로 미룬 것이, 아무래도 해당 UI는 내가 만든 게 아니라 팀장님이 만들었기 때문에 바로 건들기 애매하다는 부분이 있다. 또한 분해 매커니즘에 관련하여 조금 고민이 필요해서, 분해 슬롯을 따로 만드는 방식을 생각해봐야 할 것으로 보인다.
팀장님이 작업하던 테이블 매니저의 경우 팀장님의 업무 과다로 다른 팀원한테 넘겨졌었고, 해당 부분의 구현이 완료된 것으로 알고 있다. 따라서 해당 데이터 테이블 생성방식에 따라 스크립터블 오브젝트 생성기의 생성 방식을 조금 고쳐줘야 한다.