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. 결과물