0.들어가기에 앞서
오늘 6시까지 작업을 끝내고 나서 TIL을 쓰려고 프로젝트를 다시 열었다.
이게 대체 무슨 일인지는 모르겠지만, 분명 충돌 없이 Merge를 했는데 그 이후 프로젝트가 맛탱이가 가 버렸다. 나만 이런 건지 알 수도 없고 일단 디스코드로 해당 문제를 공유해놓기는 했지만 도저히 해결할 방법이 안 보인다.
TMP는 그렇다고 처도 JSON 쪽에서 대체 무슨 문제가 생겼길래 이렇게 터저벼린걸까. 이해하지 못하고 그대로 멘탈까지 터져버렸다. 어차피 아이템 생성과정 관련해서도 논의해야 할 부분이 있어서 작업할 게 없어서 애매했는데 그냥 6시 이후로 쉬어야겠다.
오늘은 PR을 두 번에 나눠서 올렸고 인벤토리의 전반적인 내용 구현 이후 바로 아이템 생성으로 넘어갔다.
팝업형 인벤토리 영상도 테스트본을 남기려 했으나 프로젝트가 터져서 못 남기고...
아이템 생성 기능만 테스트 영상을 남기고자 한다.
오늘은 인벤토리 구현과 아이템의 구현 두 가지 작업을 했는데 인벤토리 쪽에서는 별로 어려운이 없었다.
아이템 생성 구현 쪽에 좀 더 많은 시간을 들였고, 이 부분이 특히 어려웠다.
지금까지 핫 바 형식으로 떴던 인벤토리와 달리, 이번에 구현할 건 팝업형으로 나타났다 사라지는 인벤토리이다. DynamicInventoryDisplay라는 InventoryDisplay를 상속하는 스크립트를 만들어보자.
기능에서 아주 어려운 부분은 없다. 다만 아이템을 획득할 때 백팩 인벤토리에도 들어가게 하기 위해선 AddItem 방식을 조금 수정해야 했다.
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
/// <summary>
/// Inventory UI Display, that is not permanently activated in scene.
/// </summary>
public class LHWDynamicInventoryDisplay : LHWInventoryDisplay
{
[SerializeField] protected LHWInventorySlot_UI _slotPrefab;
protected override void Start()
{
base.Start();
}
/// <summary>
/// Update the UI when display is activated.
/// </summary>
/// <param name="invToDisplay"></param>
public void RefreshDynamicInventory(InventorySystem invToDisplay)
{
ClearSlots();
_inventorySystem = invToDisplay;
if(_inventorySystem != null) _inventorySystem.OnInventorySlotChanged += UpdateSlot;
AssignSlot(invToDisplay);
}
/// <summary>
/// Print out the current status of inventory.
/// </summary>
/// <param name="invToDisplay"></param>
// It need to be applied Object Pool Patterns?
public override void AssignSlot(InventorySystem invToDisplay)
{
_slotDictionary = new Dictionary<LHWInventorySlot_UI, InventorySlots>();
if (invToDisplay == null) return;
for(int i = 0; i < invToDisplay.InventorySize; i++)
{
var uiSlot = Instantiate(_slotPrefab, transform);
_slotDictionary.Add(uiSlot, invToDisplay.InventorySlots[i]);
uiSlot.Init(invToDisplay.InventorySlots[i]);
uiSlot.UpdateUISlot();
}
}
/// <summary>
/// Clear the inventory.
/// </summary>
// It need to be applied Object Pool Patterns?
private void ClearSlots()
{
foreach(var item in transform.Cast<Transform>())
{
Destroy(item.gameObject);
}
if(_slotDictionary != null) _slotDictionary.Clear();
}
private void OnDisable()
{
if (_inventorySystem != null) _inventorySystem.OnInventorySlotChanged -= UpdateSlot;
}
}
다음으로 해당 Dynamic Invneotory 들아 아이템을 저장하고 로드할 수 있도록 Backpack 인벤토리와 Chest Inventory를 붙이고, 각각의 컴포넌트에 만들어 준다.
using UnityEngine;
/// <summary>
/// Chest Inventory.
/// Interaction method is not complete, so test not conducted.
/// </summary>
public class ChestInventory : Inventory, IInteractable
{
/// <summary>
/// Activate Interaction UI
/// </summary>
/// <returns></returns>
public string GetDescription()
{
return "Press E Key to Interact";
}
/// <summary>
/// Get Key.
/// </summary>
/// <returns></returns>
public KeyCode GetKey()
{
return KeyCode.E;
}
/// <summary>
/// Activate Chest Inventory UI.
/// </summary>
public void Interact()
{
OnDynamicInventoryDisplayRequested?.Invoke(_inventorySystem);
}
}
using System;
using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// PopUp Inventory that player can hold.(Backpack Inventory)
/// </summary>
public class PlayerInventoryHolder : Inventory
{
[SerializeField] protected int _playerInventorySize;
[SerializeField] protected InventorySystem _playerInventorySystem;
public InventorySystem PlayerInventorySystem => _playerInventorySystem;
public static Action<InventorySystem> OnPlayerBackpackDisplayRequested;
protected override void Awake()
{
base.Awake();
_playerInventorySystem = new InventorySystem(_playerInventorySize);
}
private void Update()
{
if(Keyboard.current.tabKey.wasPressedThisFrame)
OnPlayerBackpackDisplayRequested?.Invoke(_playerInventorySystem);
}
/// <summary>
/// Add item to inventory.
/// Item is add primary to player hot bar, and into the backpack.
/// </summary>
/// <param name="data"></param>
/// <param name="amount"></param>
/// <returns></returns>
public bool AddItem(LHWTestItem data, int amount)
{
if (_inventorySystem.AddItem(data, amount))
{
return true;
}
else if(_playerInventorySystem.AddItem(data, amount))
{
return true;
}
return false;
}
}
마지막으로 InventoryUIController를 통해 팝업 UI의 출력을 관리한다.
using UnityEngine;
using UnityEngine.InputSystem;
/// <summary>
/// Controls Dynamic Inventory UIs.
/// </summary>
public class LHWInventoryUIController : MonoBehaviour
{
public LHWDynamicInventoryDisplay ChestPanel;
public LHWDynamicInventoryDisplay PlayerBackpackPanel;
private void Awake()
{
ChestPanel.gameObject.SetActive(false);
PlayerBackpackPanel.gameObject.SetActive(false);
}
private void OnEnable()
{
Inventory.OnDynamicInventoryDisplayRequested += DisplayInventory;
PlayerInventoryHolder.OnPlayerBackpackDisplayRequested += DisplayPlayerBackpack;
}
private void OnDisable()
{
Inventory.OnDynamicInventoryDisplayRequested -= DisplayInventory;
PlayerInventoryHolder.OnPlayerBackpackDisplayRequested -= DisplayPlayerBackpack;
}
// I've set esc key to exit temporary. Can change inactivate key.
void Update()
{
if(ChestPanel.gameObject.activeInHierarchy && Keyboard.current.escapeKey.wasPressedThisFrame)
ChestPanel.gameObject.SetActive(false);
if (PlayerBackpackPanel.gameObject.activeInHierarchy && Keyboard.current.escapeKey.wasPressedThisFrame)
PlayerBackpackPanel.gameObject.SetActive(false);
}
/// <summary>
/// Display Dynamic Inventory.(Chest)
/// </summary>
/// <param name="chestInvToDisplay"></param>
private void DisplayInventory(InventorySystem chestInvToDisplay)
{
ChestPanel.gameObject.SetActive(true);
ChestPanel.RefreshDynamicInventory(chestInvToDisplay);
}
/// <summary>
/// Display Player Backpack Inventory.
/// </summary>
/// <param name="playerInvToDisplay"></param>
private void DisplayPlayerBackpack(InventorySystem playerInvToDisplay)
{
PlayerBackpackPanel.gameObject.SetActive(true);
PlayerBackpackPanel.RefreshDynamicInventory(playerInvToDisplay);
}
}
아이템의 버리기 구현은 비교적 간단하게 했다. 다만 게속 Instatiate 하는 방식 때문에 오브젝트 풀링을 고려할 필요가 있어보인다.
...
private void Update()
{
// Mouse Control
if(AssignedInventorySlot.Data != null)
{
transform.position = Mouse.current.position.ReadValue();
if (Mouse.current.leftButton.wasPressedThisFrame && !IsPointerOverUIObject())
{
// I don't know where to drop items, so selected direction temporary.
// Need to gather opinion where to drop item / object pooling need to be applied?
if (AssignedInventorySlot.Data.Prefab != null)
Instantiate(AssignedInventorySlot.Data.Prefab, _playerTransform.position + _playerTransform.right * 1f, Quaternion.identity);
// Drop one items for each click
if (AssignedInventorySlot.StackSize > 1)
{
AssignedInventorySlot.AddToStack(-1);
UpdateMouseSlot();
}
else
{
ClearSlot();
}
}
}
}
...
인벤토리의 기능이 완전하게 구현된 건 아니지만, 어느 정도 구현되었다고 생각한 시점에서 바로 아이템 생성 작업에 돌입했다.
아이템은 기획팀에서 CSV파일 등으로 만든 것을 가져와서 스크립터블 오브젝트로 생성한다는 걸 감안하여, 해당 변환 과정이 가능한지 리서치를 시작했다.
using UnityEngine;
public enum ItemType { Material, Usable, Equip }
/// <summary>
/// Scriptable Object that contains Item Data.
/// </summary>
[CreateAssetMenu(fileName = "New Item", menuName = "Assets/New Item")]
public class ItemSO : ScriptableObject
{
public string Name;
public string Description;
public float Weight;
public ItemType Type;
// if is stackable, input number larger than 1.
// if is unStackable, input 1.
public int MaxStackSize;
public int Energy;
public Sprite Icon;
public GameObject Prefab;
}
우선 테스트용 아이템 스크립터블 오브젝트 대신 새로운 스크립터블 오브젝트를 생성했다.
크게 달라진 부분은 없지만, Description과 Energy 요소를 추가했다.
이와 같은 스크립터블 오브젝트를 바탕으로 테스트용 CSV 파일을 만들었다.
임시로 엑셀 데이터는 다음과 같이 제작했고, 코드 특성상 목록을 넣을 수 없는게 조금 불편한 요소이긴 하다. 이 부분은 어떻게 개선이 가능한지 알아보려고 한다.
이와 같이 CSV파일을 생성한 뒤로 유니티 에디터에 직접 커스텀 기능을 넣을 수 있는 코드를 작성했다.
using UnityEngine;
using UnityEditor;
using System.IO;
using System;
/// <summary>
/// Create Item base on CSV file.
/// How to use : check the editor bar - Utilities - Generate Item
/// Should not be contained in the build file.
/// </summary>
public class CSVToSO
{
private static string _itemCSVPath = "/LHW/Scripts/ItemCreator/Editor/CSV/TestItem.csv";
[MenuItem("Utilities/Generate Item")]
public static void GenerateItems()
{
string[] allLines = File.ReadAllLines(Application.dataPath + _itemCSVPath);
foreach(string s in allLines)
{
string[] splitData = s.Split(",");
if(splitData.Length != 6 )
{
Debug.Log($"{s} could not be imported.");
return;
}
ItemSO item = ScriptableObject.CreateInstance<ItemSO>();
item.Name = splitData[0];
item.Description = splitData[1];
float.TryParse(splitData[2], out item.Weight);
Enum.TryParse<ItemType>(splitData[3], true, out item.Type);
int.TryParse(splitData[4], out item.MaxStackSize);
int.TryParse(splitData[5], out item.Energy);
AssetDatabase.CreateAsset(item, $"Assets/LHW/ItemData/{item.Name}.asset");
}
AssetDatabase.SaveAssets();
}
}
익숙하지는 않았지만 방법 자체는 어렵지 않았다. CSV파일을 넣은 파일 경로를 찾아 해당 텍스트를 전부 분리하고, 각 텍스트 내용에 맞춰 스크립터블 아이템에 대입하는 방식이다.
여기서 팁이라고 할 수 있는 새로운 내용을 하나 배웠는데, 바로 이 스크립트와 CSV파일을 Editor라는 파일에 넣어서 관리하는 것이다.
이와 같이 하는 이유는 이 에디터가 Resources와 같은 특수 파일의 종류라서 그러한 것인데, Editor 파일이 갖는 특성은 다음과 같다.
Editor 라는 이름으로 생성한 폴더 내부의 구성요소는 파일이 빌드될 때 같이 빌드되지 않는다.
즉 에디터 상으로만 사용하고 빌드시에는 빼야 할 구성요소의 경우 이와 같이 Editor라는 파일 내에서 관리하면 되는 것이다.
CSV파일은 변조의 위험성을 대비해 빌드될 때 같이 딸려나가지 않도록 하고 해당 에디터 기능 또한 빌드 시에 문제를 일으킬 수도 있기 때문에 Editor로 따로 빼 주는 것이 좋다고 한다.
오후 Merge 작업 진행 이후 팀장님으로부터 연락이 왔다.
팀장님이 데이터 테이블 같은 걸 구성하는 과정에 대해서는 얼핏 들은 적이 있었지만, 아무래도 변환 과정에 대한 이해가 조금 부족했던 모양이었다.
그래서 이 부분에 대해서 간략하게 설명해주신 내용은 다음과 같았다.
그러니까 구글 스프레드시트를 가져와서 CSV 파일로 변환하는데, 여기에서 이걸 한 번 더 데이터 테이블로 변환을 한 다음에 사용한다는 얘기였다.
이 부분은 아마 데이터를 가져오는 과정을 조금 건들면 금방 해결될 문제로 보이는데, 내일 프로젝트 터진 문제까지 같이 해결하여 기능을 다시 구현해 봐야 하지 싶다.
이 부분은 오늘 미해결 상태로 남았다. PR에서도 해당 문제를 현재 구현중이라고 서술하긴 했지만, 텍스트로 된 데이터는 어떻게든 CSV로 가져온다고 해도 이미지나 프리팹을 가져올 수 있는 걸까? 라는 고민이 남았다.
현재로서 생각하는 방법은 이미지나 프리팹을 프로젝트 내에 전부 정리한 다음, 해당 경로를 참조하는 방식으로 구현을 시도해보고자 한다.