게임에서 저장 및 불러오기 기능은 유저들이 게임을 중단하고 나중에 다시 이어서 플레이할 수 있도록 해줍니다. 이는 게임 플레이 경험을 끊김없이 유지할 수 있으며, 유저들이 게임을 더욱 쉽게 접근할 수 있도록 도와줍니다. 또한 게임을 재미있게 플레이하는 유저들의 만족도를 높이는 데에도 중요한 역할을 합니다. 게임 저장 시스템을 구현하고 이를 정리하였습니다.
📺 미리보기
💬 서론
- 본 기능은 게임을 저장하고 불러오는 기능으로 저장할 데이터들은 게임의 스타일 및 제작자의 의도에 따라 다를 수 있습니다.
- 본 글에서는 게임 저장 시스템을 어떤 의도로 구현하였는지 요약하여 정리하였습니다.
- 모든 내용이 들어가있지 않으며, 게임 저장 및 불러오기에대한 대략적인 아이디어를 정리하였습니다.
📖 구현 내용
- 게임을 저장하고 불러옵니다.
- 플레이어의 위치, 바라보는 방향, 체력, 스탯 등 플레이 진척 사항을 데이터로 저장합니다.
- 저장한 데이터를 불러오고 저장 직전의 상태와 동일하게 불러옵니다.
- 필수적인 요소가 아닌 외적 요소 또한 유동적으로 저장 기능에 추가하거나 제거할 수 있습니다.
✅ 구현
- 이번 글에서는 저장 및 불러오기 기능을 어떻게 구현하였는지 그 예시를 간단하게 다룹니다.
- 전체적인 내용을 다루기에는 게임별로 디자인이 다르기에 모든 내용을 다루는것은 불필요하다고 판단됩니다.
- 예시로 씬에 배치된 아이템을 저장하고, 불러오는 기능을 다뤄보겠습니다.
· Script
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEngine;
using UnityEngine.SceneManagement;
using ChestSystem;
namespace GameSave
{
/// <summary>
/// 씬별로 저장되어야 할 파일
/// </summary>
public class SceneGameDataManager : MonoBehaviour
{
public void ApplyGameData(SceneGameData sceneGameData)
{
#region 씬에 배치된 아이템 로드
{
// SaveItemInfo를 가져옴 (가독성을 위한 변수이름 참조)
SceneItemInfo[] savedItems = sceneGameData.sceneItems;
// 씬에 미리 배치되어 인스턴스 프리팹으로 사용할 수 있는 레퍼런스 딕셔너리
Dictionary<EntityCode, GameObject> refItemGos = new Dictionary<EntityCode, GameObject>();
// 씬에 미리 배치되어 있는 모든 아이템을 획득
List<ItemPickUp> sceneItems = new List<ItemPickUp>();
foreach (ItemPickUp item in Object.FindObjectsOfType<ItemPickUp>(true))
{
sceneItems.Add(item);
refItemGos.TryAdd(item.Item.ID, item.gameObject);
}
// Item 데이터와 비교하여 조건이 맞으면 데이터를 Scene의 아이템으로 사용
for (int i = 0; i < savedItems.Length; ++i)
{
for (int j = sceneItems.Count - 1; j >= 0; --j)
{
if (savedItems[i].entityCode == EntityCode.NULL) continue; // 이미 아이템 정보를 사용했다면?
if (savedItems[i].entityCode != sceneItems[j].Item.ID) continue; // 아이템 코드가 다르면?
// 여기까지 온 조건이면 씬에 아이템이 이미 있고, 그 정보또한 받아온 상태
// 아이템의 정보를 세이브 데이터의 정보로 바꾸기
sceneItems[j].transform.position = savedItems[i].position;
sceneItems[j].transform.eulerAngles = savedItems[i].eulerAngles;
sceneItems[j].gameObject.SetActive(savedItems[i].isActiveSelf);
sceneItems.RemoveAt(j); // 씬에 있는 i번째의 아이템은 할당되었으므로 리스트에서 제거
savedItems[i].entityCode = EntityCode.NULL; // 세이브 데이터의 아이템 타입을 NULL로 설정
break;
}
}
// 세이브 데이터를 확인한 후 씬에 있는 아이템이 남는경우 해당 아이템들은 모두 씬에서 제거
for (int i = sceneItems.Count - 1; i >= 0; --i)
{
if (sceneItems[i].Item.ID >= EntityCode.NPC_100001_CAMP_CAPTAIN) continue;
// Debug.Log($"{sceneItems[i].gameObject.name}을 씬에서 제거");
GameObject.Destroy(sceneItems[i].gameObject);
}
// 세이브 데이터를 확인하여 씬에 있는 아이템으로 해결하지 못한경우 인스턴스
for (int i = 0; i < savedItems.Length; ++i)
{
if (savedItems[i].entityCode == EntityCode.NULL) continue; // 이미 아이템 정보를 사용했다면?
// 여기까지 온 조건이면 씬에 더 이상 남은 아이템이 없는 상태
GameObject? itemGo = null;
refItemGos.TryGetValue(savedItems[i].entityCode, out itemGo);
if (itemGo is null) // 딕셔너리 레퍼런스를 실패한경우 리소스 로드
{
itemGo = Resources.Load<GameObject>($"Entity/{savedItems[i].entityCode.ToString()}"); // 리소스에서 로드
refItemGos.TryAdd(savedItems[i].entityCode, itemGo); // 딕셔너리에 추가
// Debug.Log($"{itemGo.name}을 리소스에서 로드함");
}
Instantiate(itemGo, savedItems[i].position, Quaternion.Euler(savedItems[i].eulerAngles));
itemGo.SetActive(savedItems[i].isActiveSelf);
}
}
#endregion
...
}
// 게임 저장하기
public SceneGameData GetData()
{
SceneGameData sceneGameData = new SceneGameData();
#region 씬에 배치된 아이템 저장
{
List<SceneItemInfo> itemInfoList = new List<SceneItemInfo>();
SceneItemInfo[] sceneItemInfos = Object.FindObjectsOfType<ItemPickUp>(true)
.Where(itemPickup => itemPickup.Item.ID < EntityCode.NPC_100001_CAMP_CAPTAIN)
.Select(itemPickup =>
new SceneItemInfo
{
position = itemPickup.transform.position,
eulerAngles = itemPickup.transform.eulerAngles,
isActiveSelf = itemPickup.gameObject.activeSelf,
entityCode = itemPickup.Item.ID
})
.ToArray();
sceneGameData.sceneItems = sceneItemInfos;
}
#endregion
...
return sceneGameData;
}
}
}
- 본 스크립트는 많은 저장 기능들 중 하나(씬에 있는 아이템들)를 다룹니다.
· 저장하기
public SceneGameData GetData()
- 씬에 배치된 아이템을 저장하기위해 데이터를 가져옵니다.
#region 씬에 배치된 아이템 저장
{
List<SceneItemInfo> itemInfoList = new List<SceneItemInfo>();
SceneItemInfo[] sceneItemInfos = Object.FindObjectsOfType<ItemPickUp>(true)
.Where(itemPickup => itemPickup.Item.ID < EntityCode.NPC_100001_CAMP_CAPTAIN)
.Select(itemPickup =>
new SceneItemInfo
{
position = itemPickup.transform.position,
eulerAngles = itemPickup.transform.eulerAngles,
isActiveSelf = itemPickup.gameObject.activeSelf,
entityCode = itemPickup.Item.ID
})
.ToArray();
sceneGameData.sceneItems = sceneItemInfos;
}
#endregion
- 씬에 있는 모든 아이템을 가져옵니다.
- Object.FindObjectsOfType<ItemPickUp>(true) 함수를 이용하여 현재 씬에 배치되어있는 모든 아이템을 가져오고 LINQ문을 이용하여 특정 조건에 맞게 필터링합니다.
- EntityCode(id)가 EntityCode.NPC_100001_CAMP_CAPTAIN(NPC)보다 작으면 아이템으로 사용하고 있으며, 이 값들만 배열로 저장하여 사용하도록 합니다.
· 불러오기
public void ApplyGameData(SceneGameData sceneGameData)
- 게임을 로드한경우 로컬로부터 읽은 게임 데이터를 씬에 적용합니다.
// 씬에 미리 배치되어 있는 모든 아이템을 획득
List<ItemPickUp> sceneItems = new List<ItemPickUp>();
foreach (ItemPickUp item in Object.FindObjectsOfType<ItemPickUp>(true))
{
sceneItems.Add(item);
refItemGos.TryAdd(item.Item.ID, item.gameObject);
}
- 씬 내의 모든 아이템 오브젝트를 가져옵니다.
- 해당 아이템 오브젝트가 저장 데이터에 있을경우, 해당 데이터로 위치나, 회전값이 대치됩니다.
- 또한 refItemGos에 삽입되어 로드 중 씬에서 아이템 오브젝트의 개수가 부족할경우 이 오브젝트를 인스턴스하여 더 배치할 수 있습니다.
// Item 데이터와 비교하여 조건이 맞으면 데이터를 Scene의 아이템으로 사용
for (int i = 0; i < savedItems.Length; ++i)
{
for (int j = sceneItems.Count - 1; j >= 0; --j)
{
if (savedItems[i].entityCode == EntityCode.NULL) continue; // 이미 아이템 정보를 사용했다면?
if (savedItems[i].entityCode != sceneItems[j].Item.ID) continue; // 아이템 코드가 다르면?
// 여기까지 온 조건이면 씬에 아이템이 이미 있고, 그 정보또한 받아온 상태
// 아이템의 정보를 세이브 데이터의 정보로 바꾸기
sceneItems[j].transform.position = savedItems[i].position;
sceneItems[j].transform.eulerAngles = savedItems[i].eulerAngles;
sceneItems[j].gameObject.SetActive(savedItems[i].isActiveSelf);
sceneItems.RemoveAt(j); // 씬에 있는 i번째의 아이템은 할당되었으므로 리스트에서 제거
savedItems[i].entityCode = EntityCode.NULL; // 세이브 데이터의 아이템 타입을 NULL로 설정
break;
}
}
- 씬에 배치된 모든 아이템 오브젝트와 로드한 데이터를 비교하여 EntityCode가 동일하다면 해당 아이템의 상태로 대치합니다.
// 세이브 데이터를 확인한 후 씬에 있는 아이템이 남는경우 해당 아이템들은 모두 씬에서 제거
for (int i = sceneItems.Count - 1; i >= 0; --i)
{
if (sceneItems[i].Item.ID >= EntityCode.NPC_100001_CAMP_CAPTAIN) continue;
// Debug.Log($"{sceneItems[i].gameObject.name}을 씬에서 제거");
GameObject.Destroy(sceneItems[i].gameObject);
}
- 만약에 해당 아이템이 로드한 데이터에 없어서 남는다면, 해당 오브젝트를 제거합니다.
// 세이브 데이터를 확인하여 씬에 있는 아이템으로 해결하지 못한경우 인스턴스
for (int i = 0; i < savedItems.Length; ++i)
{
if (savedItems[i].entityCode == EntityCode.NULL) continue; // 이미 아이템 정보를 사용했다면?
// 여기까지 온 조건이면 씬에 더 이상 남은 아이템이 없는 상태
GameObject? itemGo = null;
refItemGos.TryGetValue(savedItems[i].entityCode, out itemGo);
if (itemGo is null) // 딕셔너리 레퍼런스를 실패한경우 리소스 로드
{
itemGo = Resources.Load<GameObject>($"Entity/{savedItems[i].entityCode.ToString()}"); // 리소스에서 로드
refItemGos.TryAdd(savedItems[i].entityCode, itemGo); // 딕셔너리에 추가
// Debug.Log($"{itemGo.name}을 리소스에서 로드함");
}
Instantiate(itemGo, savedItems[i].position, Quaternion.Euler(savedItems[i].eulerAngles));
itemGo.SetActive(savedItems[i].isActiveSelf);
}
- 만약에 세이브 데이터를 모두 확인하였는데도 씬에서 그 데이터를 찾을 수 없는경우 Resources.Load를 이용하여 해당 아이템을 로컬에서 불러와 인스턴스를 합니다.
- 같은 아이템인경우 이미 불러온 인스턴스를 레퍼런스하여 추가로 인스턴스하여 Resources.Load를 최소화합니다.
'unity game modules' 카테고리의 다른 글
[유니티] 로딩 화면 구현 (0) | 2023.04.02 |
---|---|
[유니티] 게임 저장 시스템(3) - 매니저 (0) | 2023.04.02 |
[유니티] 게임 저장 시스템(1) - 슬롯 구성 (0) | 2023.04.02 |
[유니티] 상자 시스템(2) - 상자 다이얼로그 (0) | 2023.04.02 |
[유니티] 상자 시스템(1) - 상자 데이터 (0) | 2023.04.02 |