유니티 프로젝트/점토게임

[개발일지] 22. 데이터 로드 & 반영 로직 수정

dubu0721 2025. 1. 21. 22:02

0. 들어가기 전에

이번에는 데이터를 로그하고 반영하는 로직을 수정했다. DataManager 가 준비될 때까지 기다렸다가 후에 가져온 데이터를 반영하도록 하기 위함이다.

 

 


 

 

1. 스크립트

이번에 새로 만든 스크립트는 따로 없고 DataManager, GameManager, PoolManager 를 수정했다.

 

1.1 DataManager 스크립트

using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.UI;

public class DataManager : MonoBehaviour
{
    [Header("Data Manager")]
    public static DataManager instance;
    public bool isInitialized = false; // 데이터 초기화 완료 여부


    [Header("Save Datas")]
    string GameDataFileName = "GameData.json";
    public Datas data = new Datas(); // 저장용 클래스 변수
    public delegate void SaveDataHandler(); // 데이터 저장 관리
    public event SaveDataHandler OnSave;

    [Header("Save Button")]
    public Button saveButton; // 얘는 게임 시작할 때랑, 다른 씬에서 돌아올 때마다 값 할당해줘야함.



    private void Awake()
    {
        // 싱글톤 이용
        if (instance != null && instance != this)
        {
            // 만약 이미 존재하면 그냥 없애
            Destroy(gameObject);
            return;
        }

        instance = this;
        DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임

        // 일단 맨 처음엔 null 로 설정..
        data.unlockClays = null;
        data.catchClays = null;
        data.valueDatas = null;

        // 데이터를 비동기로 불러오기 시작
        StartCoroutine(LoadDataAsync());

        saveButton = GameObject.Find("OptionPanelParent").transform.Find("Option Panel").transform.Find("Image").transform.Find("Save Button").GetComponent<Button>(); // 버튼 찾아서 할당
        saveButton.onClick.AddListener(SaveGameData); // 데이터 저장 메서드 연결
    }

    private IEnumerator LoadDataAsync()
    {
        // 이때 데이터 로딩 화면 띄우면 될 것 같은디..
        Debug.Log("데이터 로드 시작...");
        LoadGameData(); // 게임 데이터 가져와용
        yield return new WaitForSeconds(1f);
        isInitialized = true; // 초기화 완료 표시
    }

    // 불러오기
    public void LoadGameData()
    {
        string filePath = Application.persistentDataPath + "/" + GameDataFileName;

        // 저장된 게임이 있다면
        if (File.Exists(filePath))
        {
            // 저장된 파일 읽어오고 Json 을 클래스 형식으로 전환해서 할당
            string FromJsonData = File.ReadAllText(filePath);
            print(FromJsonData);

            data = JsonUtility.FromJson<Datas>(FromJsonData);
            print("불러오기 완료");
        }
    }

    // 저장하기
    public void SaveGameData()
    {
        OnSave?.Invoke(); // OnSave 에 연결된 메서드 모두 실행

        // 클래스를 Json 형식으로 변환(true: 가독성 좋게 작성)
        string ToJsonData = JsonUtility.ToJson(data, true);
        Debug.Log(ToJsonData + "저장할건디용");

        string filePath = Application.persistentDataPath + "/" + GameDataFileName;

        // 이미 저장된 파일이 있다면 덮어쓰고, 없다면 새로 만들어서 저장
        File.WriteAllText(filePath, ToJsonData);

        // 올바르게 저장됐는지 확인
        print("저장완료");
    }
}

 

1.2 DataManager 스크립트 변경 사항 설명

더보기

1. 변수

다음과 같은 변수가 추가되었다. DataManager 에서 데이터 초기화가 완료 되면 true 로 값이 바뀐다.

public bool isInitialized = false; // 데이터 초기화 완료 여부

 

 

2. Awake()

기존에 데이터를 불러오는 메서드를 호출하는 부분이 StartCoroutine(LoadDataAsync()); 로 대체되었다.

private void Awake()
{
    // 싱글톤 이용
    if (instance != null && instance != this)
    {
        // 만약 이미 존재하면 그냥 없애
        Destroy(gameObject);
        return;
    }

    instance = this;
    DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임

    // 일단 맨 처음엔 null 로 설정..
    data.unlockClays = null;
    data.catchClays = null;
    data.valueDatas = null;

    // 데이터를 비동기로 불러오기 시작
    StartCoroutine(LoadDataAsync());

    saveButton = GameObject.Find("OptionPanelParent").transform.Find("Option Panel").transform.Find("Image").transform.Find("Save Button").GetComponent<Button>(); // 버튼 찾아서 할당
    saveButton.onClick.AddListener(SaveGameData); // 데이터 저장 메서드 연결
}

 

 

3. LoadDataAsync()

코루틴을 새로 만들었다. 데이터를 가져오는 시간동안 기다리기 위해서 이용한다.

 

LoadGameData() 메서드를 호출한 후 1초 동안 기다리도록 했는데 지금은 임의로 설정한 값이라 다음에 변경할 가능성이 크다.

 

1초가 지나면 isInitialized 값을 true 로 바꿔서 데이터를 성공적으로 로드했음을 알리도록 했다.

private IEnumerator LoadDataAsync()
{
    // 이때 데이터 로딩 화면 띄우면 될 것 같은디..
    Debug.Log("데이터 로드 시작...");
    LoadGameData(); // 게임 데이터 가져와용
    yield return new WaitForSeconds(1f);
    isInitialized = true; // 초기화 완료 표시
}

 

1.3 GameManager 스크립트

using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.UI;
using static UnityEditor.Experimental.GraphView.Port;

public class GameManager : MonoBehaviour
{
    [Header("Game Data")]
    public float love; // 애정
    public float gold; // 골드
    public bool[] unLockedClays; // 점토들의 해금 여부
    public bool[] catchedClays; // 야생에서 잡아왔는지 확인용
    public int clayHouseLevel = 1; // 점토 아파트 레벨
    public int clayClickLevel = 1; // 점토 클릭 레벨
    public int[] clayHouseLoveList; // 업그레이드 비용 
    public int[] clayClickLoveList; // 업그레이드 비용
    public int curPossibleClayNum = 1; // 최대로 키울 수 있는 점토의 개수(1레벨은 1마리, 2레벨은 2마리...)

    [Header("Game Manager")]
    public static GameManager instance; // 싱글톤 이용하기 위함

    [Header("GameDataUI")]
    public GameDataUIController gameDataUI;
    public delegate void SetInfoPanelHandler(string text); // 델리게이트 선언 
    public event SetInfoPanelHandler OnSetInfoPanel;

    [Header("Pool Manager")]
    public PoolManager poolManager;

    [Header("Coroutine")]
    public Coroutine updateTextUICoroutine;

    [Header("Game Exit")]
    public Button gameExitButton;

    [Header("Effect")]
    // 0: 점토 레벨업, 1: 점토 판매, 2: 점토 해금, 3: 업그레이드
    public ParticleSystem[] effectsPrefabs; // 프리팹 넣어놓기
    public ParticleSystem[] effects; // 관리용 변수
    public string[] effectGameObjectNames;

    [Header("Toy Control")]
    public int curToyIdx = -1; // 현재 선택된 장난감
    public RuntimeAnimatorController[] clayToyAnimators; // 가구랑 상호작용하는 애니메이터
    public string[] toyInfo; // 가구를 클릭하면 안내 판넬에 띄울 내용
    public delegate void SetClayHouseLevel(int houseLevel, int clickLevel);
    public SetClayHouseLevel OnSetClayHouseInfo;

    // Light & UI Control
    public delegate void SetLightHandler(bool flag);
    public event SetLightHandler OnSetLightHandler; // 여기에 빛 관리하는 메서드 연결해놓을 것(점토의 드래그 시작되면 이 델리게이트에 연결된 메서드를 호출하도록..)


    private void Awake()
    {
        // 싱글톤 패턴
        if (instance != null && instance != this)
        {
            // 이미 존재하면 새로 만든거 없애
            Destroy(gameObject);
            return;
        }

        instance = this;
        DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임

        poolManager = GameObject.Find("PoolManager").GetComponent<PoolManager>(); // 풀매니저 찾아서 할당

        for (int i=0; i < effectsPrefabs.Length; i++)
        {
            // 이펙트 생성해서 넣어놓기
            effects[i] = Instantiate(effectsPrefabs[i], GameObject.Find(effectGameObjectNames[i]).transform);
            effects[i].gameObject.SetActive(false); // 비활성화
        }    
    }

    private IEnumerator Start()
    {
        // DataManager 초기화 완료 기다리기
        while (!DataManager.instance.isInitialized)
        {
            yield return null;
        }

        Debug.Log("음 이제 DataManager 이용할 수 있어용~");
        gameExitButton = GameObject.Find("OptionPanelParent").transform.Find("Option Panel").transform.Find("Image").transform.Find("Exit Button").GetComponent<Button>();
        gameExitButton.onClick.AddListener(DataManager.instance.SaveGameData); // 게임 데이터 저장 메서드 연결
        gameExitButton.onClick.AddListener(GameExit); // 게임 종료 메서드 연결
        

        // 메서드 연결하기
        DataManager.instance.OnSave -= SetSaveData; // 중복 방지하기 위해 먼저 빼줌
        DataManager.instance.OnSave += SetSaveData;

        LoadDataSet(); // 데이터 반영
        //GetLove(0);
        //GetGold(1000);
    }


    private void LoadDataSet()
    {
        // 저장된 게임 데이터가 있는 경우 데이터 가져와서 반영
        if (DataManager.instance.data.unlockClays != null)
        {
            // 저장된 데이터 반영해서 가져오기
            for (int i = 0; i < unLockedClays.Length; i++)
            {
                unLockedClays[i] = DataManager.instance.data.unlockClays[i];
                catchedClays[i] = DataManager.instance.data.catchClays[i];
            }
        }

        // 저장된 게임 수치 데이터가 있는 경우 데이터 가져와서 반영
        if (DataManager.instance.data.valueDatas != null)
        {
            // 저장된 데이터 반영해서 가져오기
            gold = DataManager.instance.data.valueDatas.gold;
            love = DataManager.instance.data.valueDatas.love;
            clayHouseLevel = DataManager.instance.data.valueDatas.clayHouseLevel;
            clayClickLevel = DataManager.instance.data.valueDatas.clayClickLevel;
            curPossibleClayNum = DataManager.instance.data.valueDatas.curPossibleClayNum;

            SetGoldLove(); // 로드한 데이터 반영해서 데이터 UI 업데이트..
            SetUpgradePanel(); // 델리게이트 호출
        }
    }

    public void SetUpgradePanel()
    {
        OnSetClayHouseInfo?.Invoke(clayHouseLevel, clayClickLevel); // UpgradePanel 클래스의 SetUpgardePanel() 메서드 호출
    }

    public void SetGoldLove()
    {
        // 로드한 데이터에 맞게 데이터 UI 변경할 수 있도록..

        // 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함. 
        if (updateTextUICoroutine != null)
        {
            StopCoroutine(updateTextUICoroutine);
        }

        updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("gold", gold, gold));
        updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love, love));
    }

    // 재화 얻는 함수
    public void GetGold(float capacity)
    {
        // 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함. 
        if (updateTextUICoroutine != null)
        {
            StopCoroutine(updateTextUICoroutine);
        }

        updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("gold", gold + capacity, gold));
        gold += capacity;

        //PlayerPrefs.SetFloat("Gold", gold); // 데이터 저장
    }

    public void GetLove(float capacity)
    {
        // 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함. 
        if (updateTextUICoroutine != null)
        {
            StopCoroutine(updateTextUICoroutine);
        }

        updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love + capacity, love));
        love += capacity;

        //PlayerPrefs.SetFloat("Love", love); // 데이터 저장
    }

    public void SetSaveData()
    {
        int size = unLockedClays.Length;

        DataManager.instance.data.unlockClays = new List<bool>();
        DataManager.instance.data.catchClays = new List<bool>();

        for (int i = 0; i < size; i++)
        {
            DataManager.instance.data.unlockClays.Add(unLockedClays[i]); // 해금 여부 저장
            DataManager.instance.data.catchClays.Add(catchedClays[i]); // 포획 여부 저장
        }

        // 수치 데이터 저장
        DataManager.instance.data.valueDatas = new ValueDatas();
        DataManager.instance.data.valueDatas.gold = gold;
        DataManager.instance.data.valueDatas.love = love;
        DataManager.instance.data.valueDatas.clayHouseLevel = clayHouseLevel;
        DataManager.instance.data.valueDatas.clayClickLevel = clayClickLevel;
        DataManager.instance.data.valueDatas.curPossibleClayNum = curPossibleClayNum;
    }


    public void GameExit()
    {
        // 게임 종료
        Application.Quit();
    }

    
    public void StartInfoPanel(string text)
    {
        // 연결된 메서드 실행시키기
        OnSetInfoPanel?.Invoke(text);
    }

    public void SetLightAndUI(bool flag)
    {
        OnSetLightHandler?.Invoke(flag); // flag 값을 전달해서 델리게이트에 연결된 메서드 호출 
    }
}

 

1.4 GameManager 스크립트 변경 사항 설명

더보기

1. Start()

기존 Start 메서드를 없애고 새로 만들었다. 코루틴으로 Start 를 만들었다.

 

일단 Start 에 진입하면 DataManager 의 isInitialized 값이 true 가 될 때까지 기다리도록 했다.

 

데이터를 성공적으로 가져왔다면 그제서야 DataManager 를 사용하도록 했다. DataManager 를 사용하는 로직은 기존의 로직과 달라진 부분이 없다.

private IEnumerator Start()
{
    // DataManager 초기화 완료 기다리기
    while (!DataManager.instance.isInitialized)
    {
        yield return null;
    }

    Debug.Log("음 이제 DataManager 이용할 수 있어용~");
    gameExitButton = GameObject.Find("OptionPanelParent").transform.Find("Option Panel").transform.Find("Image").transform.Find("Exit Button").GetComponent<Button>();
    gameExitButton.onClick.AddListener(DataManager.instance.SaveGameData); // 게임 데이터 저장 메서드 연결
    gameExitButton.onClick.AddListener(GameExit); // 게임 종료 메서드 연결
    

    // 메서드 연결하기
    DataManager.instance.OnSave -= SetSaveData; // 중복 방지하기 위해 먼저 빼줌
    DataManager.instance.OnSave += SetSaveData;

    LoadDataSet(); // 데이터 반영
    //GetLove(0);
    //GetGold(1000);
}

 

 

2. LoadDataSet()

기존 로직을 메서드로 빼서 만들었다.

private void LoadDataSet()
{
    // 저장된 게임 데이터가 있는 경우 데이터 가져와서 반영
    if (DataManager.instance.data.unlockClays != null)
    {
        // 저장된 데이터 반영해서 가져오기
        for (int i = 0; i < unLockedClays.Length; i++)
        {
            unLockedClays[i] = DataManager.instance.data.unlockClays[i];
            catchedClays[i] = DataManager.instance.data.catchClays[i];
        }
    }

    // 저장된 게임 수치 데이터가 있는 경우 데이터 가져와서 반영
    if (DataManager.instance.data.valueDatas != null)
    {
        // 저장된 데이터 반영해서 가져오기
        gold = DataManager.instance.data.valueDatas.gold;
        love = DataManager.instance.data.valueDatas.love;
        clayHouseLevel = DataManager.instance.data.valueDatas.clayHouseLevel;
        clayClickLevel = DataManager.instance.data.valueDatas.clayClickLevel;
        curPossibleClayNum = DataManager.instance.data.valueDatas.curPossibleClayNum;

        SetGoldLove(); // 로드한 데이터 반영해서 데이터 UI 업데이트..
        SetUpgradePanel(); // 델리게이트 호출
    }
}

 

 

3. SetGoldLove()

새로 만든 메서드이다. 로드한 데이터에 맞게 데이터 UI 를 변경할 수 있도록 하기 위함이다.

public void SetGoldLove()
{
    // 로드한 데이터에 맞게 데이터 UI 변경할 수 있도록..

    // 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함. 
    if (updateTextUICoroutine != null)
    {
        StopCoroutine(updateTextUICoroutine);
    }

    updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("gold", gold, gold));
    updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love, love));
}

 

1.5 PoolManager 스크립트

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


public class PoolManager : MonoBehaviour
{
    [Header("Clay Info")]
    public GameObject[] clayPrefabs;

    [Header("Clay Pool")]
    public List<GameObject>[] pools;
    public int curClayNum; // 현재 점토 개수

    [Header("Clay Spawn")]
    public GameObject spawnPoint; // 중앙에서 스폰될 것..

    [Header("Clay Sell")]
    public Clay curClay; // 현재 클릭된 대상 클레이
    public bool isSellZone; // 판매 버톤에 들어가있는지 확인용
    public bool isPossibleSell; // 이 값이 true 면 팔아!
    public delegate void SellClayHandler();
    public event SellClayHandler OnSellClay;

    private void Awake()
    {
        pools = new List<GameObject>[clayPrefabs.Length];

        for (int index = 0; index < pools.Length; index++)
            pools[index] = new List<GameObject>();
    }

    private IEnumerator Start()
    {
        // DataManager 초기화 완료 기다리기
        while (!DataManager.instance.isInitialized)
        {
            yield return null;
        }

        // 중복 연결 막기 위해서 미리 뺐다가 다시 더하기
        // - 는 연결되어 있지 않아도 에러 발생시키지 않음.
        DataManager.instance.OnSave -= SetSaveClayData;
        DataManager.instance.OnSave += SetSaveClayData;

        LoadDataSet(); // 불러온 데이터 반영하기
    }

    private void LoadDataSet()
    {
        for (int index = 0; index < DataManager.instance.data.clayInfos.Count; index++)
        {
            ClayDatas tmpData = DataManager.instance.data.clayInfos[index];
            int idx = tmpData.clayIdx;
            int level = tmpData.clayLevel;
            int cnt = tmpData.curTouchCnt;

            GetGameObject(index).GetComponent<Clay>().ResetInfo(level, cnt); // 점토 정보 설정
        }
    }

    public void TrySellClay()
    {
        if (isPossibleSell)
        {
            isPossibleSell = false; // 상태 초기화
            OnSellClay?.Invoke(); // 이벤트 호출
        }
    }

    public GameObject GetGameObject(int index)
    {
        GameObject select = null;

        foreach (GameObject gameObj in pools[index])
        {
            // 풀을 돌면서 놀고 있는 게임 오브젝트 찾기
            if (gameObj.activeSelf == false)
            {
                // 찾으면 반환
                select = gameObj;
                ActivateClay(select);
                break;
            }
        }
        // 놀고 있는 게임 오브젝트가 없다면..
        if (!select)
        {
            // 새로 생성해서 반환..
            // 새로 생성한 게임 오브젝트는 풀 매니저 하위에 놓을 것.. 그래서 부모에 transform 넣어줌..
            select = Instantiate(clayPrefabs[index], transform);
            pools[index].Add(select);
            ActivateClay(select);
        }

        return select;
    }

    public void ActivateClay(GameObject clay)
    {
        clay.SetActive(true);
        clay.transform.position = spawnPoint.transform.position; // 점토의 위치를 중앙으로..
        clay.GetComponent<Clay>().ResetInfo(); // 점토 상태 초기화시키기..
    }

    public void ReturnToPool(GameObject clay)
    {
        clay.SetActive(false);
    }

    // 점토 데이터를 저장할 것(활성돠 된 점토만..)
    public void SetSaveClayData()
    {
        // 프리팹 사이즈만큼 점토 정보 저장할 리스트 만들기
        DataManager.instance.data.clayInfos = new List<ClayDatas>();

        for (int index = 0; index < pools.Length; index++)
        {
            foreach (GameObject gameObj in pools[index])
            {
                // 풀을 돌면서 활성화 되어 있는 애들만 저장
                if (gameObject.activeSelf == true)
                {
                    // 저장할 내용 만들기
                    ClayDatas clayData = new ClayDatas();
                    Clay tmpClay = gameObj.GetComponent<Clay>();
                    clayData.clayIdx = tmpClay.clayIdx;
                    clayData.clayLevel = tmpClay.clayLevel;
                    clayData.curTouchCnt = tmpClay.curTouchCnt;

                    DataManager.instance.data.clayInfos.Add(clayData); // 리스트에 추가하기
                }
            }
        }
    }
}

 

1.6 PoolManager 스크립트 변경 사항 설명

더보기

1. Start()

기존 Start 메서드를 없애고 코루틴으로 새로 만들었다.

 

DataManager 가 초기화 완료할 때까지 기다린 후 DataManager 를 이용하도록 했다. 

private IEnumerator Start()
{
    // DataManager 초기화 완료 기다리기
    while (!DataManager.instance.isInitialized)
    {
        yield return null;
    }

    // 중복 연결 막기 위해서 미리 뺐다가 다시 더하기
    // - 는 연결되어 있지 않아도 에러 발생시키지 않음.
    DataManager.instance.OnSave -= SetSaveClayData;
    DataManager.instance.OnSave += SetSaveClayData;

    LoadDataSet(); // 불러온 데이터 반영하기
}

 

 

2. LoadDataSet()

기존 로직을 메서드로 빼서 만들었다. 가져온 점토 데이터를 반영해서 게임 월드에 점토를 만들도록 하는 메서드이다.

private void LoadDataSet()
{
    for (int index = 0; index < DataManager.instance.data.clayInfos.Count; index++)
    {
        ClayDatas tmpData = DataManager.instance.data.clayInfos[index];
        int idx = tmpData.clayIdx;
        int level = tmpData.clayLevel;
        int cnt = tmpData.curTouchCnt;

        GetGameObject(index).GetComponent<Clay>().ResetInfo(level, cnt); // 점토 정보 설정
    }
}

 

 


 

 

2. 결과물