내일배움캠프/TIL

[내배캠][Unity6기][TIL] CSV 데이터 파싱 방법(Feat. UGS)

binary는 호남선 2024. 12. 17. 21:50

현재 Wave 데이터를 UGS를 이용해 CSV 데이터를 불러와 파싱하여 사용중이다.

그런데 스테이지 별로 클래스가 나오니 하드코딩하게 되어서 스테이지가 아주 많아질 경우를 대비해 리팩토링을 시도했다.

현재는 스테이지가 많지 않아 리플렉션으로 인한 성능 부하를 상쇄할 만한 정도는 아닌 것 같아서 첫 번째 방법을 사용하고 있다.

스테이지 클래스 별로 매번 코드 추가하여 파싱

장점: 직관적이고 가독성 좋음

단점: 스테이지 추가할 때마다 코드 추가 필요, 낮은 확장성

using System.Collections.Generic;
using UnityEngine;

public class WaveData
{
    public int WaveIdx;         // 웨이브 인덱스
    public List<int> HumanID;   // 웨이브 당 등장 인간 종류 리스트
    public List<int> Count;     // 종류에 따라 등장하는 인원 수 리스트
}
public class WaveDataLoader : SingletonBase<WaveDataLoader>
{
    // 스테이지 별 웨이브 정보를 저장하는 딕셔너리
    // key: 스테이지 인덱스, value: 스테이지에 포함된 웨이브 정보(WaveData)
    public Dictionary<int, List<WaveData>> WaveDataDict = new Dictionary<int, List<WaveData>>();

    protected override void Awake()
    {
        base.Awake();
        DontDestroyOnLoad(this);
    }

    // 특정 스테이지 웨이브 데이터 로드하고 딕셔너리에 추가
    public void SetWaveDataIdxStage(int stageIdx)
    {
        switch (stageIdx)
        {
            case 0:
                WaveDataDict.Add(0, SetWaveDataListFromStage(Wave_Data.Stage1.GetList()));
                break;
            case 1:
                WaveDataDict.Add(1, SetWaveDataListFromStage(Wave_Data.Stage2.GetList()));
                break;
            default:
                Debug.LogAssertion("Wrong stage index. WaveData unloaded.");
                break;
        }
    }
    
    // 특정 스테이지의 웨이브 데이터 객체 리스트를 일반적인 WaveData 리스트로 변환
    // (웨이브 리스트 데이터를 묶어 스테이지의 웨이브 리스트로 생성)
    private List<WaveData> SetWaveDataListFromStage<T>(List<T> waveDatas)
    {
        List<WaveData> waveDataList = new List<WaveData>();

        foreach (var waveData in waveDatas)
        {
            WaveData newData = CreateWaveData(waveData);
            if (newData != null)
            {
                waveDataList.Add(newData);
            }
        }
        return waveDataList;
    }
    
    // 스테이지별 웨이브 데이터 파싱하여 일반적인 WaveData 인스턴스를 생성
    // (스테이지 내의 개별 웨이브 데이터 생성, Json -> class로 만드는 실질적 파싱 부분을 포함)
    private WaveData CreateWaveData<T>(T waveData)
    {
        if (waveData is Wave_Data.Stage1 stage1Data)
        {
            return new WaveData
            {
                WaveIdx = stage1Data.waveIdx,
                HumanID = new List<int>(stage1Data.humanId),
                Count = new List<int>(stage1Data.count)
            };
        }
        else if (waveData is Wave_Data.Stage2 stage2Data)
        {
            return new WaveData
            {
                WaveIdx = stage2Data.waveIdx,
                HumanID = new List<int>(stage2Data.humanId),
                Count = new List<int>(stage2Data.count)
            };
        }
        else
        {
            Debug.LogAssertion($"Unsupported type {typeof(T).Name}");
            return null;
        }
    }
}

리플렉션 사용하여 일반화된 데이터 파싱

장점: 데이터 추가해도 코드 수정 X, 확장성 좋음

단점: 스테이지 적으면 리플렉션 사용 의미 x, 리플렉션으로 인한 오류 및 성능 저하 가능성 有, 잦은 유효성 검사로 인한 오버헤드 발생

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using UnityEngine;

public class WaveData
{
    public int WaveIdx;
    public List<int> HumanID;
    public List<int> Count;
}
public class WaveDataLoader : SingletonBase<WaveDataLoader>
{
    public Dictionary<int, List<WaveData>> WaveDataDict = new Dictionary<int, List<WaveData>>();

    protected override void Awake()
    {
        base.Awake();
        DontDestroyOnLoad(this);
    }
    
    // 특정 스테이지 웨이브 데이터 로드하고 딕셔너리에 추가
    public void SetWaveDataIdxStage(int stageIdx)
    {
        // 스테이지 인덱스로 불러올 스테이지 클래스명 생성
        string stageClassName = "Wave_Data.Stage" + (stageIdx + 1).ToString();

        // 스테이지 클래스명과 일치하는 타입 가져오기
        Type stageType = Type.GetType(stageClassName);
        if (stageType == null)
        {
            Debug.LogAssertion($"Stage class '{stageClassName}' not found. WaveData unloaded.");
            return;
        }
        // Wave_Data.StageXX에 있는 GetList 메서드 가져오기
        MethodInfo getListMethod = stageType.GetMethod("GetList", BindingFlags.Public | BindingFlags.Static);
        if (getListMethod == null)
        {
            Debug.LogAssertion($"GetList method not found in '{stageClassName}'. WaveData unloaded.");
            return;
        }
        // GetList 호출해서 웨이브 데이터 리스트 가져오기
        object waveDataListObj = getListMethod.Invoke(null, null);
        if (waveDataListObj == null)
        {
            Debug.LogAssertion($"GetList returned null in '{stageClassName}'. WaveData unloaded.");
            return;
        }
        // IList로 캐스팅하여 반복할 수 있도록 변경
        IList waveDataListInterface = waveDataListObj as IList;
        if (waveDataListInterface == null)
        {
            Debug.LogAssertion($"GetList did not return a list in '{stageClassName}'. WaveData unloaded.");
            return;
        }
        // IList를 List<object>로 변환
        List<object> waveDataList = new List<object>();
        for (int i = 0; i < waveDataListInterface.Count; i++)
        {
            waveDataList.Add(waveDataListInterface[i]);
        }

        // 웨이브 데이터 리스트를 처리하여 WaveData로 변환
        List<WaveData> parsedWaveData = SetWaveDataListFromStage(waveDataList);

        // 딕셔너리에 변환된 웨이브 데이터 추가
        if (!WaveDataDict.ContainsKey(stageIdx))
        {
            WaveDataDict.Add(stageIdx, parsedWaveData);
            Debug.Log($"Success Stage{stageIdx + 1} WaveData");
        }
        else
        {
            Debug.Log($"Stage{stageIdx} WaveData already exists");
        }
    }
    
    // 특정 스테이지의 웨이브 데이터 객체 리스트를 일반적인 WaveData 리스트로 변환
    // (웨이브 리스트 데이터를 묶어 스테이지의 웨이브 리스트로 생성)
    private List<WaveData> SetWaveDataListFromStage(List<object> waveDatas)
    {
        List<WaveData> waveDataList = new List<WaveData>();

        foreach (WaveData waveData in waveDatas)
        {
            WaveData newData = CreateWaveData(waveData);
            if (newData != null)
            {
                waveDataList.Add(newData);
            }
        }
        return waveDataList;
    }
    
    // 스테이지별 웨이브 데이터 파싱하여 일반적인 WaveData 인스턴스를 생성
    // (스테이지 내의 개별 웨이브 데이터 생성)
    private WaveData CreateWaveData(object waveData)
    {
        if (waveData == null)
        {
            Debug.LogAssertion("waveData is null. Cannot create WaveData.");
            return null;
        }

        Type waveDataType = waveData.GetType();

        // waveIdx 필드와 정보 가져오기
        FieldInfo waveIdxField = waveDataType.GetField("waveIdx", BindingFlags.Public | BindingFlags.Instance);
        if (waveIdxField == null)
        {
            Debug.LogAssertion($"'waveIdx' field not found in '{waveDataType.FullName}'. Cannot create WaveData.");
            return null;
        }
        object waveIdxObj = waveIdxField.GetValue(waveData);
        if (!(waveIdxObj is int waveIdx))
        {
            Debug.LogAssertion($"'waveIdx' is not an int in '{waveDataType.FullName}'. Cannot create WaveData.");
            return null;
        }

        // humanId 필드와 정보 가져오기
        FieldInfo humanIdField = waveDataType.GetField("humanId", BindingFlags.Public | BindingFlags.Instance);
        if (humanIdField == null)
        {
            Debug.LogAssertion($"'humanId' field not found in '{waveDataType.FullName}'. Cannot create WaveData.");
            return null;
        }
        object humanIdObj = humanIdField.GetValue(waveData);
        List<int> humanIdList = humanIdObj as List<int>;
        if (humanIdList == null)
        {
            Debug.LogAssertion($"'humanId' is not a List<int> in '{waveDataType.FullName}'. Cannot create WaveData.");
            return null;
        }

        // count 필드와 정보 가져오기
        FieldInfo countField = waveDataType.GetField("count", BindingFlags.Public | BindingFlags.Instance);
        if (countField == null)
        {
            Debug.LogAssertion($"'count' field not found in '{waveDataType.FullName}'. Cannot create WaveData.");
            return null;
        }
        object countObj = countField.GetValue(waveData);
        List<int> countList = countObj as List<int>;
        if (countList == null)
        {
            Debug.LogAssertion($"'count' is not a List<int> in '{waveDataType.FullName}'. Cannot create WaveData.");
            return null;
        }

        // WaveData 인스턴스 생성해 반환
        WaveData newWaveData = new WaveData
        {
            WaveIdx = waveIdx,
            HumanID = new List<int>(humanIdList),
            Count = new List<int>(countList)
        };
        return newWaveData;
    }
}