(유니티) 로드 기능으로 다른 씬을 불러오기

  • DKLaw 

게임에서 필수적으로 요구되는 기능은 세이브/로드입니다. 하지만 이게 구현하기가 상당히 귀찮습니다.

보통 다들 Brackey나 유니티 공식 튜터리얼을 참고하여 세이브 기능을 구현 할 것입니다 .

참고 Brackey 강의 : https://www.youtube.com/watch?v=XOjd_qU2Ido&t=521s

이러한 강의는 한번 들어볼 필요는 있습니다. 왜냐하면 Serialization 하는 방법도 알게 되고, 어떻게 IO를 호출하는지도 알게되니까요.

근데 아무래도 직접 구현하려다 보면 할게 너무 많습니다. 특히 유니티 특유의 자료형들인 Vector3 등은 c#에서 공식적으로 지원하는 자료형이 아니므로 일일히 분해해서(Flaot 3개) 저장해줘야 합니다.

특히 플레이어의 위치만 저장하는데도, Transform.position에 float3개, rotation에 float3개, scale에 float 3개를 각각 따로 저장해야 하므로, 이를 저장할 수 있도록 별도의 함수를 만들어줘야 합니다.

이지세이브는 이러한 귀찮은 작업을 편하게 해주므로, 저는 이 애셋을 이용하기로 하였습니다.

1. 스토어 링크

https://assetstore.unity.com/packages/tools/input-management/easy-save-the-complete-save-load-asset-768?locale=ko-KR

2. 기본적인 이지세이브 사용방법

가. 씬에 매니저 추가

일단 먼저 이지세이브를 사용하려면 설치 이후 씬에 매니저를 추가해야 합니다. [Aseet] -> [Easy Save 3] -> [Add Manager to Scene]을 차례로 클릭하여 매니저를 추가합니다. 그 이후 사용방법은 다음과 같이 매우 간단합니다.

씬에 이지세이브 매니저 추가

나. 세이브

ES3.Save 메서드를 호출한 후 다음에는 자료형이나 클라스를 선언해주고, 그 다음에 세이브 해야 할 이름을 지정하고 세이브할 자료를 입력해주면 됩니다.

다음과 같습니다.

        if (Input.GetKeyDown(KeyCode.F5))
        {
            ES3.Save<int>("health", (int)this.currentHealth);
            ES3.Save<int>("score", this.currentScore);
            ES3.Save<Transform>("Position", this.transform);
        }        

위에서 보다시피 매우 직관적이고 간답합니다. 특히 Transform과 같이 유니티에 특유한 자료형을 바로 저장해 줍니다.

이지 세이브에서 지원하는 자료형은 아래 링크와 같습니다.

이지세이브 자료형 링크

다. 로드

다음과 같이 저장했던 이름대로 바로 호출해주면 됩니다.

        if (Input.GetKeyDown(KeyCode.F6))
        {
            // Load the integer back again.
            int test = ES3.Load<int>("score");
            ES3.Load<Transform>("Position");

            Debug.Log(test);
           
        }

위에서 아무런 할당도 없이 바로 Transform을 로드한게 보이시나요? 저렇게 로드만 하면 저절로 플레이어의 트랜스폼에 할당되어 플레이어가 저장된 위치로 이동하게 되더군요.

일단, 유니티에서 세이브/로드를 구현하는 방법을 배우신 분들은 한번 이지세이브를 사용해 보시기 바랍니다. 상당히 코드가 깔끔해집니다.

3. 씬 로드에 응용

근데 다른 씬을 불러올 때는 문제가 생깁니다. 유니티는 다른 씬을 불러 올 때는 씬에 있는 요소들을 모두 제거(디스트로이)하고 다시 불러오므로 다른 씬을 불러올 때는 씬과 상관 없는 글로벌 오브젝트에 저장할 필요가 있습니다.

가. 글로벌 오브젝트 생성

따라서 SaveManager는 다음과 같이 별도의 글로벌 오브젝트로 만들어주어야 합니다.

    // Singleton instance.
    public static SaveManager Instance = null;
    public bool isLoaded = false; 
    #region Singleton 
    void Awake()
    {
        // If there is not already an instance of SoundManager, set it to this.
        if (Instance == null)
        {
            Instance = this;
        }
        //If an instance already exists, destroy whatever this object is to enforce the singleton.
        else if (Instance != this)
        {
            Destroy(gameObject);
        }
        //Set SaveManager to DontDestroyOnLoad so that it won't be destroyed when reloading our scene.
        DontDestroyOnLoad(gameObject);
    }
    #endregion

저기에 불리언 함수로 ‘isLoaded’가 있는 것이 보일 겁니다. 로드 기능으로 씬을 불러오느냐, 아니면 그냥 바로 씬을 시작하느냐를 구분하기 위해 불리언을 이용했습니다.

나. 세이브와 로드 기능 구현

다음과 같이 세이브와 로드 함수를 만들었습니다.

    public void SaveFile()
    {

        Stat playerstat = FindObjectOfType<Stat>();

        ES3.Save<string>("Scene", SceneManager.GetActiveScene().name);  // 현재 레벨 저장 
        ES3.Save<int>("health", (int)playerstat.currentHealth);
        ES3.Save<int>("MaxHealth", (int)playerstat.maxHealth);
        ES3.Save<double>("stamina", playerstat.currentStamina);
        ES3.Save<double>("MaxStamina", playerstat.maxStamina);
        ES3.Save<int>("acorn", playerstat.acorn);
        ES3.Save<int>("bomb", playerstat.bomb);
        ES3.Save<int>("pickaxe", playerstat.pickaxe);


        ES3.Save<int>("score", playerstat.currentScore);
        ES3.Save<int>("coin", playerstat.currentCoin);
        ES3.Save<Vector3>("position", playerstat.transform.position);

    }

    private void Update()
    {
        if (Input.GetKeyDown(KeyCode.F5))
        {
            SaveFile();
        }
    }

    // 저장한 파일 로드  
    public void LoadFile()
    {
        SceneManager.LoadScene(ES3.Load<string>("Scene"));
        isLoaded = true;

    }

로드 할 때는 씬을 불러 온 후, isLoaded 불리언으로 이게 로드라는 것만 플래그 해줍니다.

다. 씬 로드할 때 함수 구현

결국 씬을 로드할 때 로드된 자료들을 불러와야 하므로, 다음과 같이 씬 로드시에 플레이어에게 적용할 자료들을 세팅해 줍니다. 게임 상에서 ‘로드’를 눌렀을 경우에만 작동되게 한 거죠.

    // called first
    void OnEnable()
    {
        SceneManager.sceneLoaded += OnSceneLoaded;
    }

    // called second
    void OnSceneLoaded(Scene scene, LoadSceneMode mode)
    {
        if(isLoaded)
        {
            Stat playerstat = FindObjectOfType<Stat>();


            //SceneManager.LoadScene("MainMenu");
            playerstat.currentHealth = (double)ES3.Load<int>("health");
            playerstat.maxHealth = (double)ES3.Load<int>("MaxHealth");
            playerstat.currentStamina = ES3.Load<double>("stamina");
            playerstat.maxStamina = ES3.Load<double>("MaxStamina");
            playerstat.acorn = ES3.Load<int>("acorn");
            playerstat.bomb = ES3.Load<int>("bomb");
            playerstat.pickaxe = ES3.Load<int>("pickaxe");

            playerstat.currentScore = ES3.Load<int>("score");
            playerstat.currentCoin = ES3.Load<int>("coin");

            playerstat.transform.position = ES3.Load<Vector3>("position");

            Time.timeScale = 1;

            isLoaded = false;
        }

    }

4. UI에 연결

이 정도면 사실 다 끝났습니다. 다만 UI에 연결할려면 별도의 글로벌 오브젝트를 불러오는 함수가 필요합니다. 바로 위 static 오브젝트를 UI에 연결하면, 씬이 로드 후에는 연결된 오브젝트를 전 씬에 연결된 것으로 인식하므로 새롭게 생겨난 UI에서는 missing component를 나옵니다. 따라서 동적으로 생성되는 불러오기 함수가 필요합니다.

다음과 같이 연결시켜주는 메서드를 생선한 후에 이를 UI에 연결합니다.

    public void LoadGame()
    {
        SaveManager.Instance.LoadFile();
    }

이렇게 하면 제대로 로드가 되더군요.

이제 글로벌 오브젝트는 맨 첫 메인화면에만 삽입하면(계속 살아있으므로) 계속하여 세이브 로드 기능이 될 수 있다는 것을 알 수 있습니다.