[개발일지] 32. 씬 전환시 데이터 저장&적용

2025. 1. 29. 20:33·유니티 프로젝트/점토게임

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 게임 데이터 저장하기 :: 유니티

 

10.2 게임 데이터 저장하기

이번에는 게임 중간부터 이어서 할수 있는 기능을 추가합니다. 지금까지는 씬이 변경된 후 변수의 값을 유지하기 위해 static 변수를 활용했습니다. static도 게임이 종료하면 없어지므로 데이터를

uniti.tistory.com

 

 

 

'유니티 프로젝트/점토게임' 카테고리의 다른 글
  • [개발일지] 33. 메인 씬 오디오 적용 & UI 뒤쪽 게임 오브젝트 클릭 방지
  • [개발일지] 32. 야생에서 잡아온 점토 생성
  • [개발일지] 31. 야생 판넬 제작 & 체력바 애니메이션 적용 & 씬 이동
  • [개발일지] 30. 야생 난이도에 따른 맵 생성 & 배경 설정
dubu0721
dubu0721
dubu0721 님의 블로그 입니다.
  • dubu0721
    dubu0721 님의 블로그
    dubu0721
  • 전체
    오늘
    어제
    • 분류 전체보기 (335)
      • 프로그래밍언어론 정리 (0)
      • 컴퓨터네트워크 정리 (5)
      • 알고리즘&자료구조 공부 (64)
        • it 취업을 위한 알고리즘 문제풀이 입문 강의 (60)
        • 학교 알고리즘 수업 (3)
        • 실전프로젝트I (0)
      • 백준 문제 (193)
        • 이분탐색 (7)
        • 투포인트 (10)
        • 그래프 (7)
        • 그리디 (24)
        • DP (25)
        • BFS (15)
        • MST (7)
        • KMP (4)
        • Dijkstra (3)
        • Disjoints Set (4)
        • Bellman-Ford (2)
        • 시뮬레이션 (3)
        • 백트래킹 (15)
        • 위상정렬 (5)
        • 자료구조 (25)
        • 기하학 (1)
        • 정렬 (11)
        • 구현 (8)
        • 재귀 (8)
        • 수학 (8)
      • 유니티 공부 (11)
        • 레트로의 유니티 게임 프로그래밍 에센스 (11)
        • 유니티 스터디 자료 (0)
        • C# 공부 (0)
      • 유니티 프로젝트 (48)
        • 케이크게임 (13)
        • 점토게임 (35)
      • 언리얼 공부 (10)
        • 이득우의 언리얼 프로그래밍 (10)
      • 진로 (1)
      • 논문 읽기 (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • 공지사항

  • 인기 글

  • 태그

    골드메탈
    맵
    이득우
    오블완
    BFS
    레트로의 유니티 프로그래밍
    정렬
    자료구조
    백트래킹
    dp
    백준
    투포인터
    우선순위큐
    수학
    유니티
    그래프
    이벤트 트리거
    그리디
    유니티 공부 정리
    티스토리챌린지
    유니티 프로젝트
    언리얼
    이분탐색
    바킹독
    스택
    재귀
    해시
    시뮬레이션
    C#
    큐
  • 최근 댓글

  • 최근 글

  • hELLO· Designed By정상우.v4.10.3
dubu0721
[개발일지] 32. 씬 전환시 데이터 저장&적용
상단으로

티스토리툴바