게임에서 저장 및 불러오기 기능은 유저들이 게임을 중단하고 나중에 다시 이어서 플레이할 수 있도록 해줍니다. 이는 게임 플레이 경험을 끊김 없이 유지할 수 있으며, 유저들이 게임을 더욱 쉽게 접근할 수 있도록 도와줍니다. 또한 게임을 재미있게 플레이하는 유저들의 만족도를 높이는 데에도 중요한 역할을 합니다. 게임 저장 시스템을 구현하고 이를 정리하였습니다.
📺 미리보기
💬 서론
- 본 기능은 게임을 저장하고 불러오는 기능으로 저장할 데이터들은 게임의 스타일 및 제작자의 의도에 따라 다를 수 있습니다.
- 본 글에서는 게임 저장 시스템을 어떤 의도로 구현하였는지 요약하여 정리하였습니다.
- 모든 내용이 들어가 있지 않으며, 게임 저장 및 불러오기에 대한 대략적인 아이디어를 정리하였습니다.
📖 구현 내용
- 게임을 저장하고 불러옵니다.
- 플레이어의 위치, 바라보는 방향, 체력, 스탯 등 플레이 진척 사항을 데이터로 저장합니다.
- 저장한 데이터를 불러오고 저장 직전의 상태와 동일하게 불러옵니다.
- 필수적인 요소가 아닌 외적 요소 또한 유동적으로 저장 기능에 추가하거나 제거할 수 있습니다.
✅ 구현
- 이번 글에서는 구현한 모든 저장 및 불러오기 기능들을 한 번에 제어하는 매니저를 구현하였습니다.
- 본 내용에서는 다루지 않은 많은 저장 및 불러오기 기능이 포함되어 있습니다. 이전 글에서 그중 하나의 예시를 들었으며, 대부분의 기능이 비슷하게 구성되어 있습니다.
· Script
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.IO;
using UnityEngine.SceneManagement;
using System.Linq;
namespace GameSave
{
public class GameDataSaveLoadManager : Singleton<GameDataSaveLoadManager>
{
[Header("게임을 저장하는 역할별 매니저들")]
[SerializeField] private PlayerGameDataManager mPlayerGameDataManager;
[SerializeField] private QuestGameDataManager mQuestGameDataManager;
[SerializeField] private SceneGameDataManager mSceneGameDataManager;
[SerializeField] private NpcGameDataManager mNpcGameDataManager;
[SerializeField] private ShopGameDataManager mShopGameDataManager;
private static bool mIsSaveButtonLocked = false;
/// <summary>
/// 현재 저장 버튼이 비활성화 되어있는가?
/// </summary>
public static bool IsSaveButtonLocked
{
get
{
return mIsSaveButtonLocked;
}
}
private static int mCurrentSlotId = -1;
/// <summary>
/// 현재 선택한 슬롯의 ID
/// </summary>
public static int CurrentSlotId
{
get
{
return mCurrentSlotId;
}
}
private static readonly string mFileName = "GameData.data"; // 파일 이름
public static string _FILE_PATH
{
get
{
return $"{Application.persistentDataPath}/{mFileName}";
}
}
private void Awake()
{
if(FindObjectsOfType<GameDataSaveLoadManager>().Length != 1)
{
Destroy(gameObject);
return;
}
transform.SetParent(null);
DontDestroyOnLoad(gameObject);
}
/// <summary>
/// 인덱스 번호에 해당하는 세이브 파일이 있는가?
/// </summary>
public bool FileExists(int index)
{
return File.Exists(_FILE_PATH + index);
}
public void SaveGameData(int slotId)
{
GameDataCore gameDataCore = new GameDataCore();
// Universal Data
{
gameDataCore.gameBaseInfo.dateTime = System.DateTime.Now.ToString();
gameDataCore.gameBaseInfo.sceneName = SceneManager.GetActiveScene().name;
gameDataCore.playerGameData = mPlayerGameDataManager.GetData();
gameDataCore.questGameData = mQuestGameDataManager.GetData();
}
// Scene Individual Data
{
List<SceneIndividualData> sceneIndividualDatas = new List<SceneIndividualData>();
// 기존 씬 파일 데이터가 있다면 파일 읽기
if (FileExists(slotId) == true)
{
string fromJson = File.ReadAllText(_FILE_PATH + slotId);
sceneIndividualDatas = JsonUtility.FromJson<GameDataCore>(fromJson).sceneIndividualDatas.ToList();
}
// 현재 씬의 데이터를 가져오기
SceneIndividualData sceneIndividualData = new SceneIndividualData();
sceneIndividualData.sceneName = SceneManager.GetActiveScene().name;
sceneIndividualData.npcGameData = mNpcGameDataManager.GetData();
sceneIndividualData.sceneGameData = mSceneGameDataManager.GetData();
sceneIndividualData.shopGameData = mShopGameDataManager.GetData();
// 이미 저장된 파일에서 현재 씬을 덮어쓰기가 가능하면 덮어쓰기
for (int i = 0; i < sceneIndividualDatas.Count; ++i)
if (sceneIndividualDatas[i].sceneName == sceneIndividualData.sceneName)
{
sceneIndividualDatas[i] = sceneIndividualData;
goto Overrided;
}
// 덮어쓰기를 하지 못했다면? (씬을 처음 저장하는 형태)
sceneIndividualDatas.Add(sceneIndividualData);
Overrided:;
// 배열로 변환하여 저장
gameDataCore.sceneIndividualDatas = sceneIndividualDatas.ToArray();
}
// 파일 저장
string ToJsonData = JsonUtility.ToJson(gameDataCore);
File.WriteAllText(_FILE_PATH + slotId, ToJsonData);
}
public void LoadGameData(int slotId, string? forceSceneName = null)
{
if (FileExists(slotId) == false)
return;
// 현재 슬롯 Id를 등록
mCurrentSlotId = slotId;
// 파일 읽기
string fromJson = File.ReadAllText(_FILE_PATH + slotId);
GameDataCore gameDataCore = JsonUtility.FromJson<GameDataCore>(fromJson);
// Universal Data
{
mPlayerGameDataManager.ApplyGameData(gameDataCore.playerGameData);
mQuestGameDataManager.ApplyGameData(gameDataCore.questGameData);
}
// Scene Individual Data
{
foreach(SceneIndividualData individualData in gameDataCore.sceneIndividualDatas)
if(individualData.sceneName == (forceSceneName is null ? gameDataCore.gameBaseInfo.sceneName : forceSceneName))
{
mSceneGameDataManager.ApplyGameData(individualData.sceneGameData);
mNpcGameDataManager.ApplyGameData(individualData.npcGameData);
mShopGameDataManager.ApplyGameData(individualData.shopGameData);
}
}
}
public void RemoveGameData(int slotId)
{
if (FileExists(slotId) == false)
return;
File.Delete(_FILE_PATH + slotId);
}
public void TryLockSaveButtons(bool isEnable)
{
mIsSaveButtonLocked = isEnable;
}
}
}
[Header("게임을 저장하는 역할별 매니저들")]
[SerializeField] private PlayerGameDataManager mPlayerGameDataManager;
[SerializeField] private QuestGameDataManager mQuestGameDataManager;
[SerializeField] private SceneGameDataManager mSceneGameDataManager;
[SerializeField] private NpcGameDataManager mNpcGameDataManager;
[SerializeField] private ShopGameDataManager mShopGameDataManager;
- 구현한 역할별 매니저들을 레퍼런스 하기 위해 사용합니다.
- 이 매니저들에게 저장 및 불러오기를 요청하기 위해 사용합니다.
private static bool mIsSaveButtonLocked = false;
/// <summary>
/// 현재 저장 버튼이 비활성화 되어있는가?
/// </summary>
public static bool IsSaveButtonLocked
{
get
{
return mIsSaveButtonLocked;
}
}
- 저장 버튼을 비활성화되어 있는지 확인하는 변수입니다.
- 특정 조건에 저장 기능을 비활성화할 수 있습니다.
- 예시로 보스와 전투 중인 경우는 전투 전에는 저장이 가능하나, 전투 도중에는 저장이 불가능하게 설정할 수 있습니다.
private static int mCurrentSlotId = -1;
/// <summary>
/// 현재 선택한 슬롯의 ID
/// </summary>
public static int CurrentSlotId
{
get
{
return mCurrentSlotId;
}
}
- 게임을 불러올 때 현재 선택한 슬롯의 번호를 저장합니다.
- 슬롯의 번호를 이용하여 적절한 게임 저장 데이터를 불러올 수 있도록 연결해 줍니다.
private static readonly string mFileName = "GameData.data"; // 파일 이름
public static string _FILE_PATH
{
get
{
return $"{Application.persistentDataPath}/{mFileName}";
}
}
- 파일을 저장하고, 불러오기 위한 이름을 약속합니다.
private void Awake()
{
if(FindObjectsOfType<GameDataSaveLoadManager>().Length != 1)
{
Destroy(gameObject);
return;
}
transform.SetParent(null);
DontDestroyOnLoad(gameObject);
}
- 이 매니저는 싱글톤으로 디자인되어 있으며, 씬 내에 두 개 이상 존재해서는 안됩니다.
- 타이틀 화면(Main Title) 씬에서 해당 매니저가 존재하며, 게임을 시작하여 다시 돌아올 때 이 매니저 오브젝트가 두 개가 될 수 있습니다.
- 두 개가 되는것을 방지하기위해 개수를 세어 두개 이상이면 Awake를 호출한(나중에 생긴) 오브젝트를 제거하도록 합니다.
/// <summary>
/// 인덱스 번호에 해당하는 세이브 파일이 있는가?
/// </summary>
public bool FileExists(int index)
{
return File.Exists(_FILE_PATH + index);
}
- 인덱스 번호에 맞는 세이브 파일이 존재하는지 확인합니다.
- 여기서 인덱스 번호란, 저장 및 불러오기 슬롯의 번호에 해당합니다.
public void SaveGameData(int slotId)
{
GameDataCore gameDataCore = new GameDataCore();
// Universal Data
{
gameDataCore.gameBaseInfo.dateTime = System.DateTime.Now.ToString();
gameDataCore.gameBaseInfo.sceneName = SceneManager.GetActiveScene().name;
gameDataCore.playerGameData = mPlayerGameDataManager.GetData();
gameDataCore.questGameData = mQuestGameDataManager.GetData();
}
// Scene Individual Data
{
List<SceneIndividualData> sceneIndividualDatas = new List<SceneIndividualData>();
// 기존 씬 파일 데이터가 있다면 파일 읽기
if (FileExists(slotId) == true)
{
string fromJson = File.ReadAllText(_FILE_PATH + slotId);
sceneIndividualDatas = JsonUtility.FromJson<GameDataCore>(fromJson).sceneIndividualDatas.ToList();
}
// 현재 씬의 데이터를 가져오기
SceneIndividualData sceneIndividualData = new SceneIndividualData();
sceneIndividualData.sceneName = SceneManager.GetActiveScene().name;
sceneIndividualData.npcGameData = mNpcGameDataManager.GetData();
sceneIndividualData.sceneGameData = mSceneGameDataManager.GetData();
sceneIndividualData.shopGameData = mShopGameDataManager.GetData();
// 이미 저장된 파일에서 현재 씬을 덮어쓰기가 가능하면 덮어쓰기
for (int i = 0; i < sceneIndividualDatas.Count; ++i)
if (sceneIndividualDatas[i].sceneName == sceneIndividualData.sceneName)
{
sceneIndividualDatas[i] = sceneIndividualData;
goto Overrided;
}
// 덮어쓰기를 하지 못했다면? (씬을 처음 저장하는 형태)
sceneIndividualDatas.Add(sceneIndividualData);
Overrided:;
// 배열로 변환하여 저장
gameDataCore.sceneIndividualDatas = sceneIndividualDatas.ToArray();
}
// 파일 저장
string ToJsonData = JsonUtility.ToJson(gameDataCore);
File.WriteAllText(_FILE_PATH + slotId, ToJsonData);
}
- 현재 씬을 저장하여 파일로 저장합니다.
- 모든 저장 매니저들을 호출하여 데이터를 가져옵니다.
- Universal Data와 Scene Individual Data를 구분하여 저장합니다.
· Universal
- 게임 내 씬에 관계없이 저장될 데이터입니다.
- 예를 들어 플레이어의 체력, 공격력 등 스탯 정보와 인벤토리 아이템의 정보, 퀘스트 정보가 대상이 됩니다.
· Scene Individual
- 게임 내 씬에 따라 별도로 저장이 필요한 대상입니다.
- 씬 내에 배치되어 있는 상자의 정보, 바닥에 버려져있는 아이템들, 상점의 정보들이 대상이 됩니다.
public void LoadGameData(int slotId, string? forceSceneName = null)
{
if (FileExists(slotId) == false)
return;
// 현재 슬롯 Id를 등록
mCurrentSlotId = slotId;
// 파일 읽기
string fromJson = File.ReadAllText(_FILE_PATH + slotId);
GameDataCore gameDataCore = JsonUtility.FromJson<GameDataCore>(fromJson);
// Universal Data
{
mPlayerGameDataManager.ApplyGameData(gameDataCore.playerGameData);
mQuestGameDataManager.ApplyGameData(gameDataCore.questGameData);
}
// Scene Individual Data
{
foreach(SceneIndividualData individualData in gameDataCore.sceneIndividualDatas)
if(individualData.sceneName == (forceSceneName is null ? gameDataCore.gameBaseInfo.sceneName : forceSceneName))
{
mSceneGameDataManager.ApplyGameData(individualData.sceneGameData);
mNpcGameDataManager.ApplyGameData(individualData.npcGameData);
mShopGameDataManager.ApplyGameData(individualData.shopGameData);
}
}
}
- 게임을 로드합니다.
- forceSceneName?을 이용하여 특정한 씬을 강제로 불러올 수 있습니다. 이 값이 null이라면 gameDataCore.gameBaseInfo.sceneName(저장 당시 현재 씬)을 불러옵니다.
- 각 매니저들에게 ApplyGameData를 호출하여 씬에 데이터를 불러와 적용하도록 합니다.
✅ 결과
- 위 이미지와 같이 지정한 변수들이 json 파일에 저장된 것을 볼 수 있습니다.
- 하지만 이 형태는 모든 변수들이 쉽게 수정이 가능해지며, 변조될 수 있기에 암호화를 하여 변조를 하기 어렵게 만들어야 합니다.
- 파일을 암호화하는 내용은 "[유니티] 파일 암호화 (json 암호화)"에서 확인 가능합니다.
'unity game modules' 카테고리의 다른 글
[유니티] 데미지 표시 (Damage Indicator) (0) | 2023.04.03 |
---|---|
[유니티] 로딩 화면 구현 (0) | 2023.04.02 |
[유니티] 게임 저장 시스템(2) - 저장 및 불러오기 예시 (0) | 2023.04.02 |
[유니티] 게임 저장 시스템(1) - 슬롯 구성 (0) | 2023.04.02 |
[유니티] 상자 시스템(2) - 상자 다이얼로그 (0) | 2023.04.02 |