300x250

이전에 C#으로 Json데이터를 불러와서 Datamanager 스크립트를 작성해서 관리 하게 한게 있어요.

[C#] Json 데이터 불러와서 활용하기 :: 별빛상자 (tistory.com)

 

[C#] Json 데이터 불러와서 활용하기

studen_data.xlsx => student_data.json [ { "id": 5000, "name": "길동이", "korean": 100, "math": 90, "english": 80 }, { "id": 5001, "name": "철수", "korean": 70, "math": 60, "english": 50 }, { "id":..

starlightbox.tistory.com

 

    public sealed class DataManager
    {
        // 외부에서 접근 읽기 상태로만 접근할 수 있어 데이터 수정을 막고 데이터를 읽기만 할 수 있는 싱글톤 DataManager 선언
        public static readonly DataManager instance = new DataManager();

        // 딕셔너리로 데이터관리
        private Dictionary<int, RawData> dicDatas = new Dictionary<int, RawData>();

        private DataManager()
        {
        }

        // json 데이터를 불러와서 딕셔너리에 저장하는 메소드
        public void LoadData<T>(string path) where T : RawData
        {
            var json = File.ReadAllText(path);
            T[] arr = JsonConvert.DeserializeObject<T[]>(json);
            arr.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
        }

        // id 값으로 딕셔너리에서 검색하는 메소드
        public T GetData<T>(int id) where T : RawData
        {
            var collection = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
            return (T)collection.ToList().Find(x => x.id == id);
        }

        // 특정 데이터 그룹을 검색하고싶을대 해당 데이터 타입을 호출하면 해당 데이터타입 객체만 반환한다.
        // ex(GetDatas<Student>()) = 딕셔너리에 저장된 데이터들중 Student타입을 가진 객체들만 반환
        public IEnumerable<T> GetDatas<T>() where T : RawData
        {
            IEnumerable<RawData> col = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
            return col.Select(x => (T)Convert.ChangeType(x, typeof(T)));
        }

이때 당시에는 Datamanager에서 Json 데이터를 로드화는 과정에서 넣어야 하는 데이터의 타입과 Json데이터 파일 위치를 받아서 로드하는 과정을 가졌는데 이렇게 하면 데이터를 로드하는 과정이 아래처럼 나와요.

 public class DataTest
 {
        public DataTest()
        {
            // json데이터를 불러와서 딕셔너리에 저장
            DataManager.instance.LoadData<StudentData>("./student_data.json");
            DataManager.instance.LoadData<StudyData>("./study_data.json");
            DataManager.instance.LoadData<UserData>("./user_data.json");
        }
 }

 

이코드를 처음 사용할때는 문제점을 못느꼈는데 Unity에서 게임을 만들때 아래처럼 데이터가 로드되는 과정을 연출하고싶을때 문제가 되더라구요.

 

먼저 이런 연출을 하기 위해선 데이터파일의 총 갯수를 알아야 해요.

 

그래서 Datamanager에 데이터파일의 경로를 관리하는 리스트를 선언하고 데이터들의 경로가 담겨있는 데이터파일을 작성한다음 Init함수 에서 dataPathList에 데이터를 추가 해줬어요.

 

DatapathData.cs

public class DatapathData
{
    public int id;
    public string res_name;
    public string type;
}

 

 

datapath_data.json

[
  {
    "id": 0,
    "res_name": "basket_data",
    "type": "BasketData"
  },
  {
    "id": 1,
    "res_name": "item_data",
    "type": "ItemData"
  },
  {
    "id": 2,
    "res_name": "item1_data",
    "type": "ItemData"
  },
  {
    "id": 3,
    "res_name": "item2_data",
    "type": "ItemData"
  },
  {
    "id": 4,
    "res_name": "item3_data",
    "type": "ItemData"
  }
]

id는 중복값을 피하기 위함이고, res_name에는 데이터파일의 경로가 들어가요. type은 해당 json데이터들의 클래스를 적어줘요.

    private IEnumerator LoadAllDataRoutine()
    {
        int idx = 0;
        foreach (var data in this.dataPathList)
        {
            var path = string.Format("Datas/{0}", data.res_name);
            ResourceRequest req = Resources.LoadAsync<TextAsset>(path);
            yield return req;
            float progress = (float)(idx + 1) / this.dataPathList.Count;
            TextAsset asset = (TextAsset)req.asset;

            LoadData<DataType>(asset.text);

            yield return new WaitForSeconds(0.3f);
            idx++;
        }
        yield return null;
    }

자 이제 위에서 Init 함수를 정상적으로 작동 했다면 dataPathList에는 json 파일들의 경로가 저장되어 있어요.

리소스폴더 경로 + json파일의 경로를 해서 데이터의 Full 경로를 가져왔다면 이 경로로 Resource.LoadAsync<TextAsset>(풀경로)를 이용해서 ResourceRequest 데이터를 받아올수 있어요.

asset.text = json 데이터파일

ResourceRequest.asset은 바로 Newtonsoft의 JsonConvert로 역직렬화를 할수 없기 때문에 string형식을 지원하는 TextAsset.text를 사용하기 위해서 TextAsset형식으로 바꿔준다음 LoadData에 Json문자열을 넣고 를 호출해주면 되요

근데 이때 기존 LoadData를 사용하면 문제가 발생해요.

    // json 데이터를 불러와서 딕셔너리에 저장하는 메소드
    public void LoadData<T>(string json) where T : RawData
    {
        var datas = JsonConvert.DeserializeObject<T[]>(json);
        datas.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
    }

Json 문자열을 역직렬화한후에 딕셔너리에 넣는 과정에 데이터 타입을 알아야해서 <T> 동적으로 데이터타입을 받아야하는데 지금 코드로는 LoadData를 호출할때 데이터타입을 넣어줄수가 없어요. 동적으로 데이터타입을 받는 방법은 없을까? 알아보던중 해결방법이 담긴 링크를 찾았는데 자세히 이해는 못해서 링크만 남겨요.

c# - Using a string value as a generic type parameter - Stack Overflow

 

Using a string value as a generic type parameter

Let's say I have a method that looks like this: public bool Execute<T>() { } ...and I have a string variable that describes the class name of the type I need to pass in, like string type...

stackoverflow.com

직역 하면 문자열 값을 제네릭 형식 매개 변수로 사용하는 방법에 대한 방법이에요.

그래서 LoadAllDataRoutine를 조금 수정 해줬어요.

    private IEnumerator LoadAllDataRoutine()
    {
        int idx = 0;
        foreach (var data in this.dataPathList)
        {
            var path = string.Format("Datas/{0}", data.res_name);
            ResourceRequest req = Resources.LoadAsync<TextAsset>(path);
            yield return req;
            float progress = (float)(idx + 1) / this.dataPathList.Count;
            this.onDataLoadComplete.Invoke(data.res_name, progress);
            TextAsset asset = (TextAsset)req.asset;

            var typeDef = Type.GetType(data.type);
            this.GetType().GetMethod(nameof(LoadData))
                .MakeGenericMethod(typeDef).Invoke(this, new string[] { asset.text });

            idx++;
        }
        yield return null;
        this.onDataLoadFinished.Invoke();
    }

여기서 봐야하는부분이 LoadData를 호출하는 방법이 바꼈는데 Json 파일 데이터 경로를 넘기는 방법에서 직접 Json문자열을 넘기고 이때 데이터타입을 넘겨 줘야하는데 기존에는 

 public class DataTest
 {
        public DataTest()
        {
            // json데이터를 불러와서 딕셔너리에 저장
            DataManager.instance.LoadData<StudentData>("./student_data.json");
            DataManager.instance.LoadData<StudyData>("./study_data.json");
            DataManager.instance.LoadData<UserData>("./user_data.json");
        }
 }

이런식으로 데이터 타입을 넘겨주었지만 이방법을 지금은 쓸수 없으니 

            var typeDef = Type.GetType(data.type);
            this.GetType().GetMethod(nameof(LoadData))
                .MakeGenericMethod(typeDef).Invoke(this, new string[] { asset.text });

데이터타입을 문자열로 먼저 저장해놓은 다음 Type.GetType으로 데이터타입을 받아온다음에 해당형식을 써주면

    public void LoadData<T>(string json) where T : RawData
    {
        var datas = JsonConvert.DeserializeObject<T[]>(json);
        datas.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
    }

이렇게 데이터타입을 받을수 있게 됬어요.

이때 고민점은 DataManager가 아닌 외부에서는 

 

    public void LoadAllData()
    {
        App.instance.StartCoroutine(LoadAllDataRoutine());
    }

이런식으로 LoadAllData 메소드를 통해서 LoadLoadAllDataRoutine 코루틴만 실행하면 정상적으로 모든데이터를 불러올수있어서 사실상 LoadData<T>를 외부에서 호출할 이유는 없어요. 그래서 private 으로 잠굴려고 했으나

외부에서는 처음에 Json데이터들의 타입과 주소를 저장하는 InIt메소드와 LoadAllData메소드만 호출함에도 불구하고 에러가 나오고 있는데 해결방법이 있으신분은 알려주세요.

 

DataManager.cs

using Newtonsoft.Json;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.Events;
using System;

public class DataManager
{
    public UnityEvent<string, float> onDataLoadComplete = new UnityEvent<string, float>();
    public UnityEvent onDataLoadFinished = new UnityEvent();

    public static readonly DataManager instance = new DataManager();
    private Dictionary<int, RawData> dicDatas = new Dictionary<int, RawData>();
    private List<DatapathData> dataPathList;

    public void Init()
    {
        var datapath = "Datas/datapath_data";
        var asset = Resources.Load<TextAsset>(datapath);
        var json = asset.text;
        var datas = JsonConvert.DeserializeObject<DatapathData[]>(json);

        this.dataPathList = datas.ToList();
    }
    public void LoadData<T>(string json) where T : RawData
    {
        var datas = JsonConvert.DeserializeObject<T[]>(json);
        datas.ToDictionary(x => x.id).ToList().ForEach(x => dicDatas.Add(x.Key, x.Value));
    }

    public void LoadAllData()
    {
        App.instance.StartCoroutine(LoadAllDataRoutine());
    }
    private IEnumerator LoadAllDataRoutine()
    {
        int idx = 0;
        foreach (var data in this.dataPathList)
        {
            var path = string.Format("Datas/{0}", data.res_name);
            ResourceRequest req = Resources.LoadAsync<TextAsset>(path);
            yield return req;
            float progress = (float)(idx + 1) / this.dataPathList.Count;
            this.onDataLoadComplete.Invoke(data.res_name, progress);
            TextAsset asset = (TextAsset)req.asset;
            Debug.Log(req.asset);
            var typeDef = Type.GetType(data.type);
            this.GetType().GetMethod(nameof(LoadData))
                .MakeGenericMethod(typeDef).Invoke(this, new string[] { asset.text });

            yield return new WaitForSeconds(0.3f);
            idx++;
        }
        yield return null;
        this.onDataLoadFinished.Invoke();
    }


    // id 값으로 딕셔너리에서 검색하는 메소드
    public T GetData<T>(int id) where T : RawData
    {
        var collection = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
        return (T)collection.ToList().Find(x => x.id == id);
    }

    // 특정 데이터 그룹을 검색하고싶을대 해당 데이터 타입을 호출하면 해당 데이터타입 객체만 반환한다.
    // ex(GetDatas<Student>()) = 딕셔너리에 저장된 데이터들중 Student타입을 가진 객체들만 반환
    public IEnumerable<T> GetDataList<T>() where T : RawData
    {
        IEnumerable<RawData> col = this.dicDatas.Values.Where(x => x.GetType().Equals(typeof(T)));
        return col.Select(x => (T)Convert.ChangeType(x, typeof(T)));
    }

}

UnityEvent를 이용해서 데이터가 로드될때마다 이벤트를 호출하여 현재진행상황을 알려주어서 로딩씬 연출을 했어요.

지금은 데이터가 너무 적어서 너무 빠르게 진행되서 올라가는게 잘안보여서 중간에

yield return new WaitForSeconds(0.3f); 를써서 올라가는게 눈으로 보이게 테스트를 한 모습이에요.

 

감사합니다.

300x250