XR플밍 - 11. 사전 합반 프로젝트 12일차(7/4)

이형원·2025년 7월 4일
0

XR플밍

목록 보기
124/215

0.들어가기에 앞서

오늘 드디어 데이터 연동 작업에 첫 작업을 시작했다.

1. 금일 한 업무 정리, 구현 내용


  • 어제 UI에서의 에너지 부분 반영과 더불어, 수리 버튼에서 수리한 항목을 체크하고 수리가 완료된 항목은 재수리가 불가능하게 막는 조치를 추가했다.

  • 일부 완성된 테이블에 대해 데이터 변환 작업을 진행했다. 현재 테이블을 담당한 팀원이 테이블 전체 작업을 완료하여 병합이 되고 나면 순차적으로 데이터 변환 작업을 진행할 것이다.

  • 상자 아이템 랜덤 생성 시스템에 대한 구현 자체는 거의 완료된 상태이고, 테스트 가능한 부분에 대해 우선적으로 테스트를 진행했다.

2. 문제의 발생과 해결 과정

2.1 테이블 데이터 불러오기와 스크립터블 오브젝트 생성

먼저 팀원이 맡은 구글 스프레드 시트로 테이블 데이터를 불러오는 방법이 구현됐다. 그 팀원 분이 사용 방법에 대한 상세 가이드를 작성하기도 했지만 코드 분석을 통해 어떤 식으로 작동하는지 내용을 살펴봤다.

  1. 테이블매니저는 싱글톤으로 생성하고 Awake와 동시에 모든 테이블을 로드하는 코루틴을 실행한다.
    테이블은 테이블 타입 - 테이블 베이스를 상속하는 모든 데이터에 대한 dictionary를 생성하여, 제네릭처럼 테이블 베이스인 모든 데이터 형태에 대해 테이블을 생성할 수 있도록 한다.
  2. 테이블 베이스 추상클래스는 Load() 코루틴에 대한 오버라이드 함수를 구현해야 한다. 예를 들어 ItemTable의 경우 해당 테이블이 있는 URL을 가져오고, UnityWebRequest, SendWebRequest와 같이 네트워크와 관련된 함수를 통해 테이블을 가져오게 된다. 이후 downloadHandler를 통해 가져온 데이터를 Split으로 나누어 각각의 데이터를 한 칸 씩 저장하는 방식으로 테이블을 가져온다.

여기서 유의해야 할 것은 테이블의 칸 중에 공백인 칸이 없어야 한다는 점, 테이블 좌측 혹은 우측에 불필요한 데이터가 없어야 한다는 점이 있다.

이와 같은 방식으로 나는 우선 해당 팀원의 부담을 덜어주는 겸 내 코드의 수정도 줄이기 위해 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에 대해 키워드로 들고 있다 보니 덜 헷갈렸다. 이와 같은 방식으로 테이블에 있는 데이터를 바로 가져오는 방식을 구현했다.

  • 2번째 생성기 - 상자의 일자별 생성확률을 하나의 스크립터블 오브젝트 리스트로 생성하기

아이템의 경우에는 각각의 스크립터블 오브젝트로 생성한다고 해도, 이런 일자별 확률 같은 경우에는 차라리 일자별 확률을 가진 하나의 리스트 스크립터블 오브젝트로 생성하는 편이 나을 거란 생각이 들었다.
방법이 있을까? 해당 방법도 고민해본 결과 아래와 같이 구성할 수 있었다.

  • BoxProbSO
    해당 스크립터블 오브젝트 자체를 BoxProb라는 클래스의 리스트를 담는 스크립터블 오브젝트로 생성한다.
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;
}
  • TableBoxProbToSO
    그 다음 해당 변환과정에서 리스트의 항목에 값을 넣어주면서 Add 해 주는 방식으로 구성한다.
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();
    }
}

이에 대한 최종 결과물은 아래와 같이 나온다.

2.2 상자 아이템 랜덤 생성 시스템

상자 아이템 랜던 생성 시스템에 관해서는 어제 미리 의사코드를 짜 보았다. 그에 따라 실제로 코드를 작성해 보고 테스트한 결과 아래와 같이 코드가 완성되었다.

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는 의도대로 작동하는 것을 확인했다.

2.3 일지의 분류와 일지를 출력할 방법

일지란 부분은 아무래도 기획 쪽에서도 처음에 기준이 모호했던 물건이었던 건지 모순거리가 좀 있었다.

일지는 처음에는 아이템으로 분류해서 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를 만든 팀장님과의 회의가 필요해 보인다.

3. 개선점 및 과제

3.1 테이블 데이터 마저 변환하기

3.2 수리 완료 기능에 대한 효과 추가 및 엔딩 영향 요소로서 연결하기

3.3 일지에 대해 어떻게 출력할 것인가에 대한 고민

3.4 연결 작업 이후 버그 테스트

profile
게임 만들러 코딩 공부중

0개의 댓글