카테고리 없음

유니티 Save and Load

akama 2024. 9. 25. 22:13

게임에서 플레이 진행 상황을 저장하고 다시 불러오는 기능은 매우 중요합니다. 오랜 시간 게임을 지속해서 하기 어렵기 때문에 내 캐릭터의 레벨, 스테이터스, 재화, 탐험 구간, 퀘스트 진행 데이터로 만들어서 게임을 저장하고 다시 게임할 때 불러 올 필요가 있습니다. 하이퍼 캐주얼 장르 같은 간단한 게임에서는 게임저장 및 불러오기를 안 하는 경우도 있으나 최소한의 진행 레벨 상황이나 캐릭터 스킨 적용 목적을 위한 재화 때문에 간단한 게임저장 및 불러오기 기능을 사용하기도 합니다. 

유명한 하이퍼 캐주얼 게임

구글 플레이 스토어나 앱 스토어에서 다운로드 및 플레이 가능한 Helix jump라는 게임이 있는데 이 게임은 공이 계속 통통 튕길 때 플레이어가 좌우 조작만으로 공을 위험 지역이 아닌 안전 지역에서만 점프하면서 내려가도록 하는 게임입니다. 이런 단순한 게임에서도 스테이지가 존재하기 때문에 게임저장과 불러오기가 있기 마련입니다. 지금 제작하는 예시 게임인 cleanBot 게임 역시 간단한 게임 플레이 스타일이고 목표물 회수 시 미션 클리어가 되면서 다음 스테이지로 이동하도록 구성을 해놓은 상태입니다. 게임 스테이지는 레벨 선택 씬에서 최초에는 Stage1부터 선택해서 플레이가 가능하지만 Stage2 ~ Stage5를 클리어 하면 잠금해제가 되면서 클릭 선택 및 플레이 가능하도록 할 예정입니다. Stage의 클리어 상황을 LevelDataList의 Element에 있는 isOpen에 저장하고 레벨 씬에서 이 데이터를 활용할 예정입니다. isOpen은 int 타입이고 최초 디폴트 값은 0입니다. 즉 isOpen값이 0이면 레벨이 잠긴 상태로 해당 레벨 버튼을 비활성화 처리할 것이고, isOpen값이 1이면 잠금 해제 상태로 해당 레벨 버튼은 활성화 처리하여 클릭 및 플레이가 가능하도록 하는 것입니다. isOpen의 0과 1값을 저장하거나 불러오는 기능은 유니티에서 PlayerPrefs를 이용할 수 있습니다. 대부분의 로컬 게임 저장 및 불러오기 기능은 PlayerPrefs를 사용합니다. 만드는 예시 게임은 게임서버를 사용하지 않기 때문에 게임의 저장과 불러오기 또한 로컬 즉 모바일이나 PC 같은 디바이스에 저장해서 사용합니다.

유니티의 PlayerPrefs에 대해 간략하게 알아보겠습니다.

유니티 디바이스 저장 및 불러오기에 사용되는 PlayerPrefs

유니티 로컬 게임저장 & 불러오기 예제

public class GameSaveExample : MonoBehaviour
{
     int saveData;
     
     void Start () 
     {
          saveData = PlayerPrefs.GetInt("GameCleared");
     }

    void SaveGame () 
     {
          PlayerPrefs.SetInt("GameCleared", saveData);
     }
}
public class GameSaveExample : MonoBehaviour
{
     int[] saveData;
     
     void Start () 
     {
        for (int i = 0; i < saveData.Count; i++)
        {
            saveData[i] = PlayerPrefs.GetInt("GameCleared" + i);
        }

     }

    void SaveGame () 
     {
          for (int i = 0; i < saveData.Count; i++)
        {
             PlayerPrefs.SetInt("GameCleared" + i, saveData[i]);
        }

     }
}

첫번째 스크립트 예제는 시작과 동시에 GameCleared라는 곳에서 saveData로 값을 불러와 지정합니다. 불러오기는 게임이 시작되면 불러와야 하기 때문에 대부분 Start()에서 처리하게 됩니다. 만약 saveData에 디폴트값을 지정해주고 싶다면 int saveData = 7 이런 식으로 지정해주면 되며 별도의 지정이 없으면 0부터 시작합니다. 게임저장하기는 SaveGame 함수를 만드는데 PlayerPrefs를 이용하여 GameCleared곳에 새로 획득한 데이터를 saveData를 저장합니다.

두번째 스크립트 예제는 방식이 똑같지만 배열과 for 루프문을 사용해서 다량의 데이터를 순차적으로 저장하거나 불러옵니다. 일반적으로 불러오기는 for루프문에서 한꺼번에 불러오기로 처리하지만 게임저장은 특정한 조건(if)을 붙여서 특정 배열만 저장하도록 합니다. 게임 저장과 불러오기 기능은 유니티 에셋스토어에 좋은 에셋들이 많이 있습니다. 그러나 스크립트를 분석하고 수정할 수 있는 법을 키우려면 책, 동영상 또는 관련 포스팅을 통해서 직접 익혀 놓는 것이 좋습니다.

좀 더 PlayerPrefs에 대해 알아보면 PlayerPrefs는 저장 및 불러오기를 할 때 " " 안에 문자열(string)로 구분하며 그 방법에는 3가지 타입이 있습니다.

 

- 정수 값 저장 타입

PlayerPrefs.SetInt("저장장소이름", 정수저장값);

정수불러온값 = PlayerPrefs.GetInt("저장장소이름");

 

- 문자 값 저장 타입

PlayerPrefs.SetString("저장장소이름", 문자저장값);

문자불러온값 = PlayerPrefs.GetString("저장장소이름");

 

- 소수 값 저장 타입

PlayerPrefs.SetFloat("저장장소이름", 소수저장값);

소수불러온값 = PlayerPrefs.GetFloat("저장장소이름");

 

게임 할 때 옵션창이 있는데 여기에는 주로 BGM이나 SFX같은 효과음을 조절할 수 있습니다. 대부분이 이 기능에 PlayerPrefs로 저장해서 불러오는 방법을 택합니다. 물론 서버를 이용할 수도 있으나 이 설정 값이 플레이 할 때 또는 플레이어에게 매우 중요한 가치가 있는 데이터는 아니기 때문에 로컬 저장 방식을 택하는 경우가 많습니다. 그래서 게임을 완전히 삭제하고 재설치할 경우 대부분의 게임은 게임 캐릭터 데이터는 서버로부터 받아와서 그대로 이지만 옵션값은 초기화 되어 있는 경우를 볼 수 있습니다.

using UnityEngine;

public class SettingOptions : MonoBehaviour
{
    public AudioSource BGM;
    float BGMVolume = 0.5f;

    void Start()
    {
        float tempBGMVolume = PlayerPrefs.GetFloat("BGM", BGMVolume);
        BGM.volume = tempBGMVolume;
    }

    public void SaveBGM(float volume)
    {
        BGM.volume = volume;
        PlayerPrefs.SetFloat("BGM", volume);
    }
}

음악 볼륨의 경우 정수가 아닌 소수값을 지니므로 Float 타입으로 사용합니다. 이 스크립트에서는 BGM 초기값을 0.5f로 설정해서 임시로 만든 변수에 저장하고 AudioSource의 볼륨에 해당값을 할당하면서 시작합니다. 볼륨 저장하기는 SaveBGM 함수를 만들어서 새로 들어오는 인자 값(volume)을 바로 BGM.volume에 적용하면서 동시에 "BGM"에 해당 값을 저장합니다. 만약 volume이 0.9f로 바뀌어 저장된다면 다음에 시작함수에서는 PlayerPrefs.GetFloat("BGM", BGMVolume) 때문에 초기설정인 0.5f가 아닌 0.9f를 tempBGMVolume에 할당하면서 우리가 기대하는 불러오기 값을 확인할 수 있게 됩니다.

 

자 이제 본론으로 돌아와서 유니티에서 LevelDataList 스크립트를 열어서 Save 함수부터 만들겠습니다.

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

public class LevelDataList : SingletonBehaviour
{
    public string levelName;
    public int isOpen;
    public int currentLevel;

    [SerializeField]
    public List data;

    void Start()
    {
        GetClearLevelData();
    }

    void GetClearLevelData ()
    {
        for (int i = 0; i < data.Count; i++)
        {
            data[i].isOpen = PlayerPrefs.GetInt("Cleared" + i);
        }
    }

    public void GetLevel (int lv)
    {
        for(int i=0; i
        {
            currentLevel = lv;
            if (data[i].level == lv)
            {
                levelName = data[i].name;
                isOpen = data[i].isOpen;
                break;
            }
        }
        SceneManager.LoadScene(currentLevel);
    }

    public void SaveClearLevel ()
    {
        for (int i=0; i<data.Count; i++)
        {
             if (currentLevel == data[i].level)
             {
                data[i+1].isOpen = 1;
             }
            PlayerPrefs.SetInt("Cleared" + i, levelDataList.data[i].isOpen);
        }
    }
}

기존 스크립트에 굵은 글씨로 몇 가지 명령줄이 추가되었습니다. 게임 씬이 시작되면 시작함수와 함께 GetClearLevelData()를 실행해서 for루프로 PlayerPrefs.GetInt로 불러온 isOpen 값들을 순차적으로 data[i].isOpen 값에 대입시켜 줍니다. SaveClearLevel()함수는 public으로 선언해서 GameController에서 미션 성공 시 컴포넌트를 가져다 사용할 수 있도록 해줍니다. 불러오기와 달리 게임저장은 for루프로 한꺼번에 저장하면 안 됩니다. 특정 조건만 변경해서 저장해야 하기 때문에 for루프 내에서 조건문(if)을 사용합니다. 게임 상황의 저장은 레벨 선택 씬이 아닌 게임 플레이 씬이기 때문에 지금 플레이 해서 클리어한 스테이지 정보를 알아야 합니다. 다행히도 레벨 씬에서 버튼 선택을 통해 지정된 레벨 값을 currentLevel에 할당시켰고 이를 싱글톤을 통해서 플레이 씬까지 가져온 상태입니다. currentLevel 값과 일치하는 data[i].level 값을 찾고 일치하는 배열값 i에 맞는 데이터를 변경하고 저장합니다. 예를 들어 Stage3을 플레이 했다고 가정하면 currentLevel 값이 3이 되고 data[i].level 값도 3인 조건이 되며 i는 0부터 카운트 하기 때문에 i는 2가 됩니다.

보기 쉽게 쓰면 'currentLevel = data[2].level = 3' 이렇게 된다는 뜻입니다.

근데 우리가 Stage3을 클리어 했으면 Stage4는 잠금해제가 되어야 합니다. 그래서 'currentLevel = data[3].level = 4' 이 되어야 원하는 값을 갖게 됩니다. 따라서 data[i].isOpen이 플레이 클리어된 상태에서는 i가 2가 아닌 3이어야 하므로 data[i+1].isOpen로 바꿔 줍니다. 그러면 Stage4의 isOpen 상태를 1로 바꿔서 저장하게 되고 레벨 씬에서 불러올 때는 Stage1~4까지는 잠금해제 된 상태로 보여지게 되는 것입니다. 참고로 테스트 시 해당 데이터 중 특정 값 "OnlyThisTempName"f를 초기화 하고 싶다면 PlayerPrefs.DeleteKey("OnlyThisTempName"); 를 사용하거나 모든 저장 데이터를 초기화 하고 싶다면 PlayerPrefs.DeleteAll(); 을 명령어에 넣어 주면 됩니다.

GameController 스크립트에 있는 winBoard() 함수에 다음 명령어를 추가해 줍니다.

using UnityEngine.UI;

public class GameController : MonoBehaviour
{

      LevelDataList levelDataList;

      void Start()
      {
            levelDataList = GameObject.Find("LevelDataList").GetComponent<LevelDataList>();
      }

      void winBoard()
      {
             levelDataList.SaveClearLevel();
      }
}

지금까지 유니티의 게임 저장하기와 불러오기를 할 수 있도록 하는 PlayerPrefs에 대해 알아 보았으며 이를 통해서 게임의 진행 상황을 간단하게 저장하고 불러오는 방법까지 해보았습니다. PlayerPrefs는 사용법이 매우 간단한 장점이 있는 반면에 데이터 형식이 복잡한 부분은 취급하기 어려우며 보안에 취약하고 게임 삭제에 의해 데이터가 초기화 되는 단점이 있습니다. 그래서 앞서 언급한 게임에 큰 영향을 주지 않는 기능의 저장과 불러오기에만 사용하며, 하이퍼 캐주얼 장르와 같이 단순 스테이지 클리어 상황만 체크할 때에 사용됩니다. 다음은 LevelDataList 스크립트에서 불러오기를 통해 형성된 데이터로 레벨 씬의 버튼 활성화와 비활성화 처리에 대해 포스팅하겠습니다.