0. 들어가기 전에
이번에는 씬 전환시 데이터를 저장하고 적용하도록 했다.
1. 스크립트
이번엔 새로 만든 스크립트는 없지만 대신에 많은 스크립트가 수정됐다.
GameManager 와 DataManager 를 싱글톤으로 이용하고 있기 때문에 다른 스크립트에서 사용할 수 있는 장점을 갖는다. 하지만 이 때문에 GameManager 와 DataManager 둘 다 준비 되기 전까지 다른 스크립트에서는 이 둘을 사용하면 안 되는데 이를 관리하기 위해 코드를 수정했다.....

1.1 GameManager 스크립트
using System;
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.Rendering.Universal;
using UnityEngine.SceneManagement;
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; // 싱글톤 이용하기 위함
public string curScene;
public bool isInitialized = false; // 데이터 초기화 완료 여부
private Coroutine dataSetCoroutine; // 데이터 초기화 코루틴 저장
[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; // 여기에 빛 관리하는 메서드 연결해놓을 것(점토의 드래그 시작되면 이 델리게이트에 연결된 메서드를 호출하도록..)
// 야생 콘텐츠 관련
[Header("World Manager")]
// 게임 시작 종료 여부
public bool isGameStart = false;
public bool isGameEnd = false;
public bool isGameClear = false;
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 여기서 데이터 세팅하기
// DataManager 준비될 때까지 기다려!
if (dataSetCoroutine != null)
StopCoroutine(dataSetCoroutine); // 이미 시작한 코루틴 있으면 끝내고 다시 시작
dataSetCoroutine = StartCoroutine(OnSceneLoadedSetting()); // 정보 세팅
}
private void Awake()
{
// 싱글톤 이용
if (instance != null && instance != this)
{
// 만약 이미 존재하면 그냥 없애
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임
}
private void OnEnable()
{
// 씬이 로드될 때마다 알아서 호출될 수 있도록..
SceneManager.sceneLoaded += OnSceneLoaded; // 이벤트에 메서드 연결
}
private void OnDisable()
{
// 게임 오브젝트가 비활성화 될 때 이벤트에 연결해놓은 메서드 없애기
SceneManager.sceneLoaded -= OnSceneLoaded;
}
// 씬 로드될 때 수행되어야 하는 로직 모음집..
private IEnumerator OnSceneLoadedSetting()
{
// 데이터 매니저 준비될 때까지 기다려!!!
// DataManager 초기화 완료 기다리기
while (!DataManager.instance.isInitialized)
{
yield return null;
}
Debug.Log("음 이제 DataManager 이용할 수 있어용~");
curScene = SceneManager.GetActiveScene().name; // 씬 이름 가져오기
// 씬에 따라 정보 세팅 다르게..
if (curScene == "ClayHouse")
{
// 현재 씬이 클레이 하우스인 경우에만 호출 되도록..
poolManager = GameObject.Find("PoolManager").GetComponent<PoolManager>(); // 풀매니저 찾아서 할당
gameDataUI = GameObject.Find("GameDataUIController").GetComponent<GameDataUIController>(); // 게임 데이터 UI 찾아서 할당
for (int i = 0; i < effectsPrefabs.Length; i++)
{
// 이펙트 생성해서 넣어놓기
effects[i] = Instantiate(effectsPrefabs[i], GameObject.Find(effectGameObjectNames[i]).transform);
effects[i].gameObject.SetActive(false); // 비활성화
}
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(); // 데이터 반영
}
else if (curScene == "World")
{
// 현재 씬이 World 일 때
}
isInitialized = true; // 데이터 반영 완료
}
private void LoadDataSet()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 저장된 게임 데이터가 있는 경우 데이터 가져와서 반영
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()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
OnSetClayHouseInfo?.Invoke(clayHouseLevel, clayClickLevel); // UpgradePanel 클래스의 SetUpgardePanel() 메서드 호출
}
public void SetGoldLove()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 로드한 데이터에 맞게 데이터 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)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 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)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 이미 코루틴이 종료되지 않은 중에 동일한게 또 들어오면 겹쳐서 반영이 돼서 이상하게 될 수 있으므로 null 인지 판단해야함.
if (updateTextUICoroutine != null)
{
StopCoroutine(updateTextUICoroutine);
}
updateTextUICoroutine = StartCoroutine(gameDataUI.UpdateTextUI("love", love + capacity, love));
love += capacity;
//PlayerPrefs.SetFloat("Love", love); // 데이터 저장
}
public void SetSaveData()
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
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)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
// 연결된 메서드 실행시키기
OnSetInfoPanel?.Invoke(text);
}
public void SetLightAndUI(bool flag)
{
if (curScene == "World") return; // 만약 현재 씬이 야생이면 그냥 빠져나가도록..
OnSetLightHandler?.Invoke(flag); // flag 값을 전달해서 델리게이트에 연결된 메서드 호출
}
public void MoveScene()
{
// 게임 매니저와 데이터 매니저의 isInitailized 값을 다시 false 로 바꿔주기
isInitialized = false; // 다시 false 로..
DataManager.instance.isInitialized = false;
if (curScene == "ClayHouse")
{
DataManager.instance.SaveGameData(); // 씬 전환 하기 전 데이터 저장!
curScene = "World"; // 씬 이름 바꿔주깅
// 현재가 점토 집인 경우에는 World 씬으로 이동
SceneManager.LoadScene("World");
}
else if (curScene == "World")
{
curScene = "ClayHouse";
// 현재 World 씬인 경우 점토 집 씬으로 이동
SceneManager.LoadScene("ClayHouse");
}
}
public void WorldGameClear()
{
// 야생에 성공했을 때 호출되는 메서드
isGameStart = false;
isGameEnd = true;
isGameClear = true;
}
public void WorldGameFail()
{
// 야생에 성공했을 때 호출되는 메서드
isGameStart = false;
isGameEnd = true;
isGameClear = false;
}
}
1.2 GameManager 스크립트 변경 사항 설명
1. 변수
다음과 같은 변수가 추가되었다.
public bool isInitialized = false; // 데이터 초기화 완료 여부
private Coroutine dataSetCoroutine; // 데이터 초기화 코루틴 저장
2. OnSceneLoaded(Scene scene, LoadSceneMode mode)
SceneManager.sceneLoaded 이벤트에 연결할 메서드이다. 게임 매니저는 로드한 데이터를 반영해야 하는데 DataManager 가 다 준비될 때까지 기다리도록 했다.
private void OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 여기서 데이터 세팅하기
// DataManager 준비될 때까지 기다려!
if (dataSetCoroutine != null)
StopCoroutine(dataSetCoroutine); // 이미 시작한 코루틴 있으면 끝내고 다시 시작
dataSetCoroutine = StartCoroutine(OnSceneLoadedSetting()); // 정보 세팅
}
3. OnEnable(), OnDisable()
각각 게임 오브젝트가 활성화/비활성화 될 때 자동으로 호출되는 메서드이다. 씬 매니저의 sceneLoaded 이벤트에 메서드를 연결/연결해제 하도록 했다.
private void OnEnable()
{
// 씬이 로드될 때마다 알아서 호출될 수 있도록..
SceneManager.sceneLoaded += OnSceneLoaded; // 이벤트에 메서드 연결
}
private void OnDisable()
{
// 게임 오브젝트가 비활성화 될 때 이벤트에 연결해놓은 메서드 없애기
SceneManager.sceneLoaded -= OnSceneLoaded;
}
4. OnSceneLoadedSetting()
데이터 매니저가 준비될 때까지 기다린 후 로직을 수행하도록 했다. 게임 진행에 필요한 변수들의 값을 설정해주도록 했다.
// 씬 로드될 때 수행되어야 하는 로직 모음집..
private IEnumerator OnSceneLoadedSetting()
{
// 데이터 매니저 준비될 때까지 기다려!!!
// DataManager 초기화 완료 기다리기
while (!DataManager.instance.isInitialized)
{
yield return null;
}
Debug.Log("음 이제 DataManager 이용할 수 있어용~");
curScene = SceneManager.GetActiveScene().name; // 씬 이름 가져오기
// 씬에 따라 정보 세팅 다르게..
if (curScene == "ClayHouse")
{
// 현재 씬이 클레이 하우스인 경우에만 호출 되도록..
poolManager = GameObject.Find("PoolManager").GetComponent<PoolManager>(); // 풀매니저 찾아서 할당
gameDataUI = GameObject.Find("GameDataUIController").GetComponent<GameDataUIController>(); // 게임 데이터 UI 찾아서 할당
for (int i = 0; i < effectsPrefabs.Length; i++)
{
// 이펙트 생성해서 넣어놓기
effects[i] = Instantiate(effectsPrefabs[i], GameObject.Find(effectGameObjectNames[i]).transform);
effects[i].gameObject.SetActive(false); // 비활성화
}
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(); // 데이터 반영
}
else if (curScene == "World")
{
// 현재 씬이 World 일 때
}
isInitialized = true; // 데이터 반영 완료
}
1.3 DataManager 스크립트
DataManager 스크립트의 변경 사항은 GameManager 스크립트의 변경 사항과 비슷한 로직이기 때문에 설명을 생략한다.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.SceneManagement;
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 OnSceneLoaded(Scene scene, LoadSceneMode mode)
{
// 씬 로드될 때 자동으로 호출되는 메서드
if (scene.name != "ClayHouse") return; // 메인씬에서만 호출되도록..
// 여기서 데이터 세팅하기
OnSceneLoadedSetting();
}
private void Awake()
{
// 싱글톤 이용
if (instance != null && instance != this)
{
// 만약 이미 존재하면 그냥 없애
Destroy(gameObject);
return;
}
instance = this;
DontDestroyOnLoad(gameObject); // 얘는 다른 씬으로 전환되어도 안 없앨 거임
}
private void OnEnable()
{
// 씬이 로드될 때마다 알아서 호출될 수 있도록..
SceneManager.sceneLoaded += OnSceneLoaded; // 이벤트에 메서드 연결
}
private void OnDisable()
{
// 게임 오브젝트가 비활성화 될 때 이벤트에 연결해놓은 메서드 없애기
SceneManager.sceneLoaded -= OnSceneLoaded;
}
// 씬 로드될 때 수행되어야 하는 로직 모음집..
private void OnSceneLoadedSetting()
{
// 일단 맨 처음엔 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(); // 게임 데이터 가져와용
isInitialized = true; // 초기화 완료 표시
// 데이터 다 로드될 때까지 기다령!
while (isInitialized)
yield return null;
}
// 불러오기
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("불러오기 완료");
}
isInitialized = true; // 초기화 완료 표시
}
// 저장하기
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.4 그 외 스크립트
많은 스크립트를 변경했지만 변경한 내용이 다 비슷비슷하다. 핵심은 GameManager, DataManager 가 준비될 때까지 기다린 후에 자기 일을 하도록 변경했다는 것이다.
그리고 지금까지 델리게이트를 많이 이용했는데, 오늘 씬 전환시 데이터를 반영할 때 엄청나게 많은 오류들을 만나게 되었다.
공통적인 이유는 각 클래스에서 GameManager, DataManager 의 델리게이트에 메서드를 연결해놓는 부분이 있다. 그런데 씬이 전환되면 싱글톤이 아닌 게임 오브젝트들은 파괴되어 버린다. 그래서 다시 씬으로 돌아온 후 GameManager, DataManager 의 델리게이트에 연결된 메서드를 호출하려고 할 때, 파괴된 게임 오브젝트의 메서드를 이용하려고 해서 에러가 나는 거였다;;;;;;;;;;;;;;;;;;
위와 같은 상황을 해결하기 위해서는 게임 오브젝트가 파괴될 때 GameManager, DataManager 의 델리게이트에 연결해놓은 메서드를 제거해야했다.
OnDisable 또는 OnDestroy 메서드에서 GameManager, DataManager 델리게이트에 연결해놓은 메서드를 빼면 해결할 수 있다.
아래는 수정한 스크립트 내용이다.
1. 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 OnDisable()
{
// 게임 오브젝트 파괴될 때 델리게이트에 연결했던 메서드 빼주기
DataManager.instance.OnSave -= SetSaveClayData;
}
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); // 리스트에 추가하기
}
}
}
}
}
2. ClayPanel
using System.Collections;
using System.Collections.Generic;
using Unity.VisualScripting;
using UnityEngine;
using UnityEngine.UI;
public class ClayPanel : MonoBehaviour
{
[Header("UI")]
public Button pageLeftButton; // 페이지 왼쪽 버튼
public Button pageRightButton; // 페이지 오른쪽 버튼
public Button buyClayButton; // 점토 구매 버튼
public Button unlockClayButton; // 점토 해금 버튼
public Image lockedImage; // 잠금된 점토 이미지
public Image clayImage; // 점토 이미지
public Text clayName; // 점토 이름
public Text buyPrice; // 점토 가격
public Button closeButton;
public GameObject lockedPage; // 잠금 페이지
[Header("Control")]
public int pageIdx = 0; // 페이지 인덱스
public int maxIdx;
public int minIdx = 0;
[Header("Effect")]
public int effectIdx = 2;
private IEnumerator Start()
{
// 게임 매니저가 준비 될 때까지 기다령~~
while (!GameManager.instance.isInitialized)
yield return null;
maxIdx = GameManager.instance.poolManager.clayPrefabs.Length;
pageLeftButton.onClick.AddListener(DownPageIdx);
pageLeftButton.onClick.AddListener(SetClayPanel);
pageRightButton.onClick.AddListener(UpPageIdx);
pageRightButton.onClick.AddListener(SetClayPanel);
}
private void OnEnable()
{
// 활성화 될 때 호출되는 함수
ResetClayPanel();
}
public void ResetClayPanel()
{
pageIdx = 0;
SetButtonInfo(); // 버튼 설정해주기
}
public void SetClayPanel()
{
Clay clay = GameManager.instance.poolManager.clayPrefabs[pageIdx].GetComponent<Clay>();
if (GameManager.instance.unLockedClays[pageIdx] == false)
{
lockedPage.SetActive(true); // 해금이 안된 경우엔 잠금 판넬 활성화..
}
else
{
lockedPage.SetActive(false); // 해금된 경우엔 잠금 판넬 비활성화..
}
// 페이지 인덱스에 맞게 설정
lockedImage.sprite = clay.clay; // 점토의 이미지 가져오기
clayImage.sprite = clay.clay;
clayName.text = clay.clayName; // 점토 이름 가져오기
buyPrice.text = clay.buyPrice + ""; // 점토 가격 가져오기
}
public void UpPageIdx()
{
if (pageIdx >= maxIdx - 1) {
GameManager.instance.StartInfoPanel("마지막 페이지입니다.");
return; // 빠져나가기..
}
pageIdx++;
SetButtonInfo();
}
public void DownPageIdx()
{
if (pageIdx <= 0) {
GameManager.instance.StartInfoPanel("처음 페이지입니다.");
return; // 빠져나가기..
}
pageIdx--;
SetButtonInfo();
}
private void SetButtonInfo()
{
ColorBlock col = unlockClayButton.colors;
// 만약 야생에서 점토를 잡아왔으면 해금하기 버튼 노란색으로 바꿔주기
if (GameManager.instance.catchedClays[pageIdx])
{
col.normalColor = new Color32(255, 220, 90, 255); // 노란색
col.highlightedColor = new Color32(255, 220, 90, 255); // 노란색
unlockClayButton.colors = col; // 색 만들어 놓은거 할당해주기
}
else
{
col.normalColor = new Color32(255, 255, 255, 255); // 흰색
col.highlightedColor = new Color32(255, 255, 255, 255); // 흰색
unlockClayButton.colors = col; // 색 만들어 놓은거 할당해주기
}
unlockClayButton.onClick.RemoveAllListeners(); // 일단 다 지웡
unlockClayButton.onClick.AddListener(() => Unlock(pageIdx)); // Unlock 메서드 추가
buyClayButton.onClick.RemoveAllListeners(); // 일단 다 지웡
buyClayButton.onClick.AddListener(() => BuyClay(pageIdx)); // BuyClay 메서드 추가
}
public void Unlock(int idx)
{
// 야생에서 아직 안 잡았으면 해금 못하도록..
if (!GameManager.instance.catchedClays[pageIdx])
{
// 안내 판넬 띄우기
GameManager.instance.StartInfoPanel("야생에서 잡아와야 해요!");
return;
}
GameManager.instance.unLockedClays[idx] = true;
PlayEffect(effectIdx); // 이펙트 소환
SetClayPanel();
}
public void PlayEffect(int idx)
{
if (GameManager.instance.effects[idx].gameObject.activeSelf == false)
GameManager.instance.effects[idx].gameObject.SetActive(true); // 활성화하기
GameManager.instance.effects[idx].Play();
}
public void BuyClay(int idx)
{
int price = GameManager.instance.poolManager.clayPrefabs[idx].GetComponent<Clay>().buyPrice;
// 돈이 충분하고 공간이 있으면 점토 구매
if (GameManager.instance.gold >= price && (GameManager.instance.curPossibleClayNum > GameManager.instance.poolManager.curClayNum))
{
GameManager.instance.GetGold(-price);
GameManager.instance.poolManager.GetGameObject(idx); // 동물 get!
GameManager.instance.poolManager.curClayNum++; // 점토 개수 증가
}
else if (GameManager.instance.gold < price)
{
// 안내 판넬 띄우기
GameManager.instance.StartInfoPanel("돈이 부족해요 ㅠ_ㅠ");
return;
}
else if (GameManager.instance.curPossibleClayNum <= GameManager.instance.poolManager.curClayNum) {
// 안내 판넬 띄우기
GameManager.instance.StartInfoPanel("집이 너무 좁아요 ㅠ_ㅠ");
return;
}
}
public void ClosePanel()
{
gameObject.SetActive(false); // 활성화 끄기..
}
}
3. LightAndUIController
using System.Collections;
using System.Collections.Generic;
using UnityEditor.Build.Content;
using UnityEngine;
using UnityEngine.Rendering.Universal;
public class LightAndUIController : MonoBehaviour
{
[Header("Light & UI Control")]
public GameObject globalLight;
public GameObject canvas;
public GameObject sellButtonLight;
private void Start()
{
// 델리게이트에 메서드 연결
// Clay 스크립트에서 GameMananager 에 접근해서 델리게이트 호출할거임!
GameManager.instance.OnSetLightHandler -= SetLightAUI;
GameManager.instance.OnSetLightHandler += SetLightAUI;
}
private void OnDisable()
{
// 게임 오브젝트가 파괴될 때 GameManager 에 연결해놨던 메서드 연결을 끊어야함;;
GameManager.instance.OnSetLightHandler -= SetLightAUI;
}
public void SetLightAUI(bool flag)
{
// 점토가 들린 상태면 flag 값이 true 임
if (flag)
{
// 점토가 들렸으면 globalLight 의 색을 어둡게, canvas 활성화를 꺼야함
globalLight.GetComponent<Light2D>().color = new Color32(90, 85, 80, 255);
canvas.gameObject.SetActive(!flag);
// 판매 버튼의 빛도 켜주기
sellButtonLight.SetActive(flag);
}
else
{
// 점토를 내려놓았으면 globalLight 의 색을 밝게, canvas 활성화를 켜야함
globalLight.GetComponent<Light2D>().color = new Color32(255, 255, 255, 255);
canvas.gameObject.SetActive(!flag);
// 판매 버튼의 빛 꺼주기
sellButtonLight.SetActive(flag);
}
}
}
4. UpgradePanel
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using System.Net;
using UnityEditor;
public class UpgradePanel : MonoBehaviour
{
[Header("Plant Panel UI")]
public Text houseSubText; // 업그레이드 내용
public Text houseLoveText; // 필요한 애정 수치
public Button houseUpButton; // 업그레이드 버튼
public Text clickSubText;
public Text clickLoveText;
public Button clickUpButton;
[Header("Effect")]
public int effectIdx = 3;
[Header("Clay Toys")]
public Button[] toys; // 0: 캣타워, 1: 러그, 2: 블라인더, 3: 조명
public GameObject blindBackground; // 블라인더가 소환되면 바닥의 모습을 바꿔야함.
public GameObject lightBackground; // 블라인더가 소환되면 빛 없어져야함.
private void Awake()
{
// 게임 매니저 델리게이트에 연결해놓기
GameManager.instance.OnSetClayHouseInfo -= SetUpgardePanel; // 메서드 연결
GameManager.instance.OnSetClayHouseInfo += SetUpgardePanel; // 메서드 연결
}
private void OnDisable()
{
// 게임 오브젝트가 파괴될 때 GameManager 에 연결해놨던 메서드 연결을 끊어야함;;
GameManager.instance.OnSetClayHouseInfo -= SetUpgardePanel;
}
private IEnumerator Start()
{
// 게임 매니저가 준비 될 때까지 기다령~~
while (!GameManager.instance.isInitialized)
yield return null;
// 버튼에 메서드 연결
houseUpButton.onClick.AddListener(UpgradeHouse);
clickUpButton.onClick.AddListener(UpgradeClick);
}
// 게임매니저의 델리게이트에 연결할거임
// 얘는 게임이 처음 시작할 때 호출할 것..
public void SetUpgardePanel(int houseLevel, int clickLevel)
{
// 만렙이면 max 로, 아니면 레벨 반영해서 텍스트 설정
if (houseLevel == 5)
{
houseLoveText.text = "max";
}
else
{
houseSubText.text = "점토 수용량 " + (houseLevel + 1);
houseLoveText.text = GameManager.instance.clayHouseLoveList[houseLevel] + "";
}
// 집 레벨에 맞게 장난감 활성화
if (houseLevel >= 2)
{
// 블라인더 소환될 레벨이면 배경 바꾸도록..
if (houseLevel >= 4)
{
// 게임 오브젝트 활성화&비활성화 해서 배경 바꾸기..
lightBackground.SetActive(false);
blindBackground.SetActive(true);
}
for (int i=0; i<=houseLevel-2; i++)
{
// 장난감 활성화
toys[i].gameObject.SetActive(true);
}
}
// 만렙이면 max 로, 아니면 레벨 반영해서 텍스트 설정
if (clickLevel == 5)
{
clickLoveText.text = "max";
}
else
{
clickSubText.text = "클릭 생산량 x " + (clickLevel + 1);
clickLoveText.text = GameManager.instance.clayClickLoveList[clickLevel] + "";
}
}
// 얘는 게임이 진행되는 중에 언제든지 그냥 이용할 수 있음..
public void SetUpgardePanel()
{
// 만렙이면 max 로, 아니면 레벨 반영해서 텍스트 설정
if (GameManager.instance.clayHouseLevel == 5)
{
houseSubText.text = "점토 수용량 " + (GameManager.instance.clayHouseLevel);
houseLoveText.text = "max";
}
else
{
houseSubText.text = "점토 수용량 " + (GameManager.instance.clayHouseLevel + 1);
houseLoveText.text = GameManager.instance.clayHouseLoveList[GameManager.instance.clayHouseLevel] + "";
}
// 만렙이면 max 로, 아니면 레벨 반영해서 텍스트 설정
if (GameManager.instance.clayClickLevel == 5)
{
clickSubText.text = "클릭 생산량 x " + (GameManager.instance.clayClickLevel);
clickLoveText.text = "max";
}
else
{
clickSubText.text = "클릭 생산량 x " + (GameManager.instance.clayClickLevel + 1);
clickLoveText.text = GameManager.instance.clayClickLoveList[GameManager.instance.clayClickLevel] + "";
}
}
public void UpgradeHouse()
{
// 이미 레벨이 최고면 걍 빠져나가도록..
if (GameManager.instance.clayHouseLevel == 5) {
// 안내창 띄우기
GameManager.instance.StartInfoPanel("최대 레벨입니다!");
return;
}
else if (GameManager.instance.clayHouseLoveList[GameManager.instance.clayHouseLevel] > GameManager.instance.love)
{
// 안내창 띄우기
GameManager.instance.StartInfoPanel("애정이 부족해요 ㅠ_ㅠ");
return;
}
GameManager.instance.GetLove(-GameManager.instance.clayHouseLoveList[GameManager.instance.clayHouseLevel]); // 애정 차감
GameManager.instance.clayHouseLevel++;
GameManager.instance.curPossibleClayNum++; // 키울 수 있는 점토 수 +1
toys[GameManager.instance.clayHouseLevel - 2].gameObject.SetActive(true); // 장난감 게임 오브젝트 활성화
// 블라인더가 소환되면 배경이 바뀌어야함..
if (GameManager.instance.clayHouseLevel == 4)
{
// 배경 바꾸기..
lightBackground.SetActive(false);
blindBackground.SetActive(true);
}
if (GameManager.instance.clayHouseLevel == 5)
{
houseLoveText.text = "max";
}
PlayEffect(effectIdx); // 이펙트 소환
SetUpgardePanel();
}
public void UpgradeClick()
{
// 이미 레벨이 최고면 걍 빠져나가도록..
if (GameManager.instance.clayClickLevel == 5)
{
GameManager.instance.StartInfoPanel("최대 레벨입니다!");
return;
}
else if (GameManager.instance.clayClickLoveList[GameManager.instance.clayClickLevel] > GameManager.instance.love)
{
// 안내창 띄우기
GameManager.instance.StartInfoPanel("애정이 부족해요 ㅠ_ㅠ");
return;
}
GameManager.instance.GetLove(-GameManager.instance.clayClickLoveList[GameManager.instance.clayClickLevel]); // 애정 차감
GameManager.instance.clayClickLevel++;
if (GameManager.instance.clayClickLevel == 5)
{
clickLoveText.text = "max";
}
PlayEffect(effectIdx); // 이펙트 소환
SetUpgardePanel();
}
public void PlayEffect(int idx)
{
if (GameManager.instance.effects[idx].gameObject.activeSelf == false)
GameManager.instance.effects[idx].gameObject.SetActive(true); // 활성화하기
GameManager.instance.effects[idx].Play();
}
public void ClosePanel()
{
gameObject.SetActive(false); // 활성화 끄기..
}
}
5. InfoPanelController
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
public class InfoPanelController : MonoBehaviour
{
[Header("Animation")]
public Animator anim;
[Header("UI")]
public Text infoText;
private void Start()
{
anim = gameObject.GetComponent<Animator>(); // 할당하기
// 메서드 연결
GameManager.instance.OnSetInfoPanel -= SetAndPlayInfoPanel;
GameManager.instance.OnSetInfoPanel += SetAndPlayInfoPanel;
}
private void OnDestroy()
{
// 게임 오브젝트가 파괴될 때 GameManager 에 연결해놨던 메서드 연결을 끊어야함;;
GameManager.instance.OnSetInfoPanel -= SetAndPlayInfoPanel;
}
public void SetAndPlayInfoPanel(string text)
{
infoText.text = text; // 안내 문구 설정
anim.SetTrigger("Started"); // 애니메이션 실행
}
}
6. ToyController
using System.Collections;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine;
using UnityEngine.EventSystems;
public class ToyController : MonoBehaviour, IPointerEnterHandler, IPointerExitHandler
{
[Header("Toy Info")]
public int toyIdx; // 장난감 인덱스(0: 캣타워, 1: 카펫, 2: 블라인더, 3: 조명)
[Header("UI")]
public Button toyButton;
private void Awake()
{
toyButton = GetComponent<Button>(); // 버튼 할당받기
SetButtonOnClick(); // onClick 컴포넌트 설정
}
private void SetButtonOnClick()
{
toyButton.onClick.RemoveAllListeners(); // 일단 다 지우기
toyButton.onClick.AddListener(() => GameManager.instance.StartInfoPanel(GameManager.instance.toyInfo[toyIdx])); // 인덱스에 알맞는 가구 정보 가져와서 메서드 연결
}
public void OnPointerEnter(PointerEventData eventData)
{
GameManager.instance.curToyIdx = toyIdx; // 현재 가구 인덱스 값으로 설정..
}
public void OnPointerExit(PointerEventData eventData)
{
GameManager.instance.curToyIdx = -1; // 아무 가구도 선택되지 않았음을 알리기..
}
}
2. 결과물
3. 참고자료
유니티에서 제공하는 씬 전환 기능에 대해 공부할 수 있는 기회였다.

3.1 씬 전환
유니티 씬(Scene)과 씬 전환 (Scene Transition)
유니티 씬(Scene)과 씬 전환 (Scene Transition)
1. 씬 (Scene) 경찰, 범죄 등과 관련된 영화나 드라마를 보게 되면, Crime Scene이라는 노란색 테이프로 현장을 막아놓은 장면들이 나온다. 범죄에 사용된 물건, 사물 등이 존재하는 사건 현장이 씬이
notyu.tistory.com
[Unity] 씬이 로딩될 때 실행되는 함수 (SceneManager.sceneLoaded) — 민규야 개발하자
[Unity] 씬이 로딩될 때 실행되는 함수 (SceneManager.sceneLoaded)
https://docs.unity3d.com/ScriptReference/SceneManagement.SceneManager-sceneLoaded.html Unity - Scripting API: SceneManagement.SceneManager.sceneLoaded You've told us this page needs code samples. If you'd like to help us further, you could provide a code s
mingyu0403.tistory.com
10.2 게임 데이터 저장하기
이번에는 게임 중간부터 이어서 할수 있는 기능을 추가합니다. 지금까지는 씬이 변경된 후 변수의 값을 유지하기 위해 static 변수를 활용했습니다. static도 게임이 종료하면 없어지므로 데이터를
uniti.tistory.com