0.들어가기에 앞서
오늘 드디어 데이터 연동 작업에 첫 작업을 시작했다.
어제 UI에서의 에너지 부분 반영과 더불어, 수리 버튼에서 수리한 항목을 체크하고 수리가 완료된 항목은 재수리가 불가능하게 막는 조치를 추가했다.
일부 완성된 테이블에 대해 데이터 변환 작업을 진행했다. 현재 테이블을 담당한 팀원이 테이블 전체 작업을 완료하여 병합이 되고 나면 순차적으로 데이터 변환 작업을 진행할 것이다.
먼저 팀원이 맡은 구글 스프레드 시트로 테이블 데이터를 불러오는 방법이 구현됐다. 그 팀원 분이 사용 방법에 대한 상세 가이드를 작성하기도 했지만 코드 분석을 통해 어떤 식으로 작동하는지 내용을 살펴봤다.
여기서 유의해야 할 것은 테이블의 칸 중에 공백인 칸이 없어야 한다는 점, 테이블 좌측 혹은 우측에 불필요한 데이터가 없어야 한다는 점이 있다.
이와 같은 방식으로 나는 우선 해당 팀원의 부담을 덜어주는 겸 내 코드의 수정도 줄이기 위해 Table 자체는 전부 String으로 가져와주고 데이터를 변환하는 것 내 쪽에서 담당하기로 했다.
그러면 이제 테이블 데이터로 데이터를 가져와서 어떻게 스크립터블 오브젝트를 만들면 될까? 과정 자체는 CSV파일을 가져와서 변환한 거랑 아주 큰 차이가 나지는 않는다. 다만 방식은 좀 더 편하게 진행할 수 있었다.
using System;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
/// <summary>
/// Create Item base on Table Data.
/// How to use : Play Scene with Table manager - check the editor bar - Utilities - Generate Item
/// Should not be contained in the build file.
/// </summary>
public class TableItemToSO
{
[MenuItem("Utilities/Generate Item")]
public static void GenerateItems()
{
var itemTable = TableManager.Instance.GetTable<ItemTable>(TableType.Item);
List<ItemData> tItem = itemTable.TItem;
foreach (ItemData s in tItem)
{
ItemSO item = ScriptableObject.CreateInstance<ItemSO>();
int.TryParse(s.ItemID, out item.ItemId);
item.Name = s.ItemName;
Enum.TryParse<ItemType>(s.ItemType, true, out item.Type);
item.Description = s.ItemTooltip;
float.TryParse(s.ItemWeight, out item.Weight);
int.TryParse(s.ItemEnergy, out item.Energy);
int.TryParse(s.MaxPayloadPerPanel, out item.MaxStackSize);
bool.TryParse(s.IsDecomposable, out item.IsDecomposable);
int.TryParse(s.ItemStats, out item.ItemStats);
// If Item Path is selected, path will be edited.
item.Icon = AssetDatabase.LoadAssetAtPath<Sprite>($"Assets/05.Images/Items/{s.SpritePath}");
item.Prefab = AssetDatabase.LoadAssetAtPath<GameObject> ($"Assets/03.Prefabs/Objects/{s.PrefabPath}.prefab");
if (item.Icon == null)
Debug.LogWarning($"Sprite not found at path: {s.SpritePath}");
if (item.Prefab == null)
Debug.LogWarning($"Prefab not found at path: {s.PrefabPath}");
AssetDatabase.CreateAsset(item, $"Assets/08.ScriptableObjects/Item/{item.Name}.asset");
}
AssetDatabase.SaveAssets();
}
}
기존의 CSV파일의 경로를 찾는 부분을 생략할 수 있었고, 아이템 데이터 자체가 이미 해당 String에 대해 키워드로 들고 있다 보니 덜 헷갈렸다. 이와 같은 방식으로 테이블에 있는 데이터를 바로 가져오는 방식을 구현했다.
아이템의 경우에는 각각의 스크립터블 오브젝트로 생성한다고 해도, 이런 일자별 확률 같은 경우에는 차라리 일자별 확률을 가진 하나의 리스트 스크립터블 오브젝트로 생성하는 편이 나을 거란 생각이 들었다.
방법이 있을까? 해당 방법도 고민해본 결과 아래와 같이 구성할 수 있었다.
using System.Collections.Generic;
using UnityEngine;
[CreateAssetMenu(fileName = "New BoxProb", menuName = "Assets/Create New BoxProb")]
public class BoxProbSO : ScriptableObject
{
public List<BoxProb> _boxProbs = new List<BoxProb>();
}
[System.Serializable]
public class BoxProb
{
public int DayNum;
public float BoxType1Prob;
public float BoxType2Prob;
public float BoxType3Prob;
}
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
public class TableBoxProbToSO : MonoBehaviour
{
[MenuItem("Utilities/Generate BoxProb")]
public static void GenerateBoxProb()
{
var boxProbTable = TableManager.Instance.GetTable<BoxProbTable>(TableType.BoxProb);
List<BoxProbData> boxProbs = boxProbTable.TBoxProb;
BoxProbSO boxProb = ScriptableObject.CreateInstance<BoxProbSO>();
foreach (BoxProbData s in boxProbs)
{
int.TryParse(s.DayNum, out int boxProb_DayNum);
float.TryParse(s.BoxType1Prob, out float boxProb_BoxType1);
float.TryParse(s.BoxType2Prob, out float boxProb_BoxType2);
float.TryParse(s.BoxType3Prob, out float boxProb_BoxType3);
boxProb._boxProbs.Add(new BoxProb { DayNum = boxProb_DayNum, BoxType1Prob = 0.01f * boxProb_BoxType1, BoxType2Prob = 0.01f * boxProb_BoxType2, BoxType3Prob = 0.01f * boxProb_BoxType3 });
}
AssetDatabase.CreateAsset(boxProb, $"Assets/08.ScriptableObjects/BoxProb/BoxProb.asset");
AssetDatabase.SaveAssets();
}
}
이에 대한 최종 결과물은 아래와 같이 나온다.
상자 아이템 랜던 생성 시스템에 관해서는 어제 미리 의사코드를 짜 보았다. 그에 따라 실제로 코드를 작성해 보고 테스트한 결과 아래와 같이 코드가 완성되었다.
using System.Collections.Generic;
using UnityEngine;
enum BoxTier { Tier1, Tier2, Tier3 }
public class BoxItemRandomSystem : MonoBehaviour
{
[SerializeField] BoxTier _tier;
[SerializeField] BoxSystem _data;
[Header("Item - Normal")]
[SerializeField] ItemSO[] _itemA_Items1;
[Header("Item - Rare")]
[SerializeField] ItemSO[] _itemA_Items2;
[Header("Item - Unique")]
[SerializeField] ItemSO[] _itemA_Items3;
[Header("Item - Legendary")]
[SerializeField] ItemSO[] _itemA_Items4;
[Header("Journal - List")]
[SerializeField] CollectionSO[] _itemB_Journal;
[Header("Food")]
[SerializeField] ItemSO _itemC_Food;
[Header("Collectible - List")]
[SerializeField] CollectionSO[] _itemD_Collection;
private WeightedRandom<ItemSO> _weightedRandomA = new WeightedRandom<ItemSO>();
private WeightedRandom<CollectionSO> _weightedRandomD = new WeightedRandom<CollectionSO>();
private Queue<CollectionSO> _journalQueue = new Queue<CollectionSO>();
Dictionary<int, float> _itemBProbableDic = new Dictionary<int, float>();
Dictionary<int, float> _itemCProbableDic = new Dictionary<int, float>();
private void Awake()
{
// Item A init
switch (_tier)
{
case BoxTier.Tier1: ItemAInit(300, 145, 50, 10);
break;
case BoxTier.Tier2: ItemAInit(150, 200, 100, 100);
break;
case BoxTier.Tier3: ItemAInit(50, 125, 200, 250);
break;
}
// Item B init
ItemBInit();
// Item C init
ItemCInit();
// Item D init
ItemDInit();
}
private void OnEnable()
{
ItemAddToBox();
}
private void OnDisable()
{
_data.RemoveAllItem();
}
private void ItemAddToBox()
{
ItemASelect();
//ItemBSelect();
//ItemCSelect();
ItemDSelect();
}
/// <summary>
/// Select Item A.
/// Item A is definitely collecitible, and the number of A item is 4.
/// </summary>
private void ItemASelect()
{
if (_weightedRandomA.GetList() == null) return;
for (int i = 0; i < 4; i++)
{
_data.AddItemToBoxSlot(_weightedRandomA.GetRandomItem());
}
}
/// <summary>
/// Set Item A weightRandom. (Materials)
/// </summary>
/// <param name="normal"></param>
/// <param name="rare"></param>
/// <param name="unique"></param>
/// <param name="legendary"></param>
private void ItemAInit(int normal, int rare, int unique, int legendary)
{
for(int i = 0; i < _itemA_Items1.Length; i++)
{
_weightedRandomA.Add(_itemA_Items1[i], normal);
}
for(int i = 0; i < _itemA_Items2.Length; i++)
{
_weightedRandomA.Add(_itemA_Items2[i], rare);
}
for(int i = 0; i < _itemA_Items3.Length; i++)
{
_weightedRandomA.Add(_itemA_Items3[i], unique);
}
for (int i = 0; i < _itemA_Items4.Length; i++)
{
_weightedRandomA.Add(_itemA_Items4[i], legendary);
}
}
private void ItemBSelect()
{
if(_journalQueue.Count == 0) return;
float value = _itemBProbableDic[GameManager.Instance.DayNightManager.CurrentDay];
float randomNum = Random.Range(0.0f, value);
if (randomNum > value) return;
_data.AddCollection(_journalQueue.Dequeue(), 0);
}
/// <summary>
/// Set Item B weightRandom. (Diary)
/// </summary>
private void ItemBInit()
{
for(int i = 0; i < _itemB_Journal.Length; i++)
{
_journalQueue.Enqueue(_itemB_Journal[i]);
}
// 현재 일지 아이템이 없고 확률 테이블을 만들 방법에 대해 고민하고 있어
// 의사 코드로 먼저 적습니다.
// 일차 = key, 확률 = value로 된 dictionary를 생성하고, 데이터 테이블의 정보를 SO로 만들어놓는다.
// 딕셔너리 정보를 전부 저장(임시로 값을 전부 입력함)
_itemBProbableDic[1] = 1;
_itemBProbableDic[2] = 0.9f;
_itemBProbableDic[3] = 0.8f;
_itemBProbableDic[4] = 0.7f;
_itemBProbableDic[5] = 0.7f;
_itemBProbableDic[6] = 0.6f;
_itemBProbableDic[7] = 0.5f;
_itemBProbableDic[8] = 0.5f;
_itemBProbableDic[9] = 0.4f;
_itemBProbableDic[10] = 0.3f;
_itemBProbableDic[11] = 0.3f;
_itemBProbableDic[12] = 0.3f;
_itemBProbableDic[13] = 0.2f;
_itemBProbableDic[14] = 0.2f;
_itemBProbableDic[15] = 0.2f;
}
private void ItemCSelect()
{
if(_itemCProbableDic.Count == 0) return;
float value = _itemCProbableDic[GameManager.Instance.DayNightManager.CurrentDay];
float randomNum = Random.Range(0.0f, value);
if(randomNum > value) return;
_data.AddItemToBoxSlot(_itemC_Food);
}
/// <summary>
/// Set Item C weightRandom. (Food)
/// </summary>
private void ItemCInit()
{
// 현재 일지 아이템이 없고 확률 테이블을 만들 방법에 대해 고민하고 있어
// 의사 코드로 먼저 적습니다.
// 일차 = key, 확률 = value로 된 dictionary를 생성하고, 데이터 테이블의 정보를 SO로 만들어놓는다.
// 딕셔너리 정보를 전부 저장 - 임시로 직접 입력해봄
_itemCProbableDic[1] = 1;
_itemCProbableDic[2] = 0.5f;
_itemCProbableDic[3] = 0.1f;
_itemCProbableDic[4] = 0.2f;
_itemCProbableDic[5] = 0.2f;
_itemCProbableDic[6] = 0.2f;
_itemCProbableDic[7] = 0.1f;
_itemCProbableDic[8] = 0.1f;
_itemCProbableDic[9] = 0.1f;
_itemCProbableDic[10] = 0.2f;
_itemCProbableDic[11] = 0.2f;
_itemCProbableDic[12] = 0.2f;
_itemCProbableDic[13] = 0.1f;
_itemCProbableDic[14] = 0.1f;
_itemCProbableDic[15] = 0.1f;
}
/// <summary>
/// Select Item D. (Collective)
/// </summary>
private void ItemDSelect()
{
if (_weightedRandomD.GetList() == null) return;
float randomNum = Random.Range(0.0f, 1.0f);
if (randomNum > 0.7f) return;
_data.AddCollection(_weightedRandomD.GetRandomItem(), 1);
}
/// <summary>
/// Set Item D weightRandom. (Collective)
/// </summary>
private void ItemDInit()
{
for(int i = 0; i < _itemD_Collection.Length; i++)
{
_weightedRandomD.Add(_itemD_Collection[i], 1);
}
}
}
B, C는 당장 테스트할 환경이 되지 않아서 테스트 진행을 못했지만, A와 D는 의도대로 작동하는 것을 확인했다.
일지란 부분은 아무래도 기획 쪽에서도 처음에 기준이 모호했던 물건이었던 건지 모순거리가 좀 있었다.
일지는 처음에는 아이템으로 분류해서 1권부터 10권까지 있는 설정으로 만들었었지만, 이후 콜렉션의 한 종류로 분류되면서 실질적으로 드랍되어 먹는 아이템이 아니라 획득 시 바로 콜렉션에 저장되는 방식으로 바뀌었다.
또한 상자 아이템 내에서만 등장하다 보니 상자는 '콜렉션'을 담을 수 있어야 한다.
이를 위해서 우선적으로 내가 해야 할 작업은 상자에 콜렉션을 담을 수 있도록 설정하는 것이다.
[SerializeField] private ItemSO[] _boxItem;
[SerializeField] private int[] _boxStack;
[SerializeField] private CollectionSO[] _boxCollection;
public ItemSO[] BoxItem => _boxItem;
public int[] BoxStack => _boxStack;
public CollectionSO[] BoxCollection => _boxCollection;
이와 같이 아이템을 담을 수 있는 칸과 콜렉션을 담을 수 있는 칸을 따로 만들고, 콜렉션을 담을 수 있는 칸에 각각 일지와 수집형 아이템을 담는 방식으로 변경했다.
우선은 수집형 아이템을 담는 것까지는 아이템 랜덤 등장 테스트를 완료했지만 일지의 경우에는 아무래도 고민이 많아졌다.
텍스트 분량도 어마어마하고 이게 다이얼로그와 관련되어 있는 부분이다 보니 내 쪽에서 획득을 섣불리 구현하기가 어려웠다. 이 부분에 대해서는 아무래도 해당 UI를 만든 팀장님과의 회의가 필요해 보인다.