게임에서 저장 및 불러오기 기능은 유저들이 게임을 중단하고 나중에 다시 이어서 플레이할 수 있도록 해줍니다. 이는 게임 플레이 경험을 끊김없이 유지할 수 있으며, 유저들이 게임을 더욱 쉽게 접근할 수 있도록 도와줍니다. 또한 게임을 재미있게 플레이하는 유저들의 만족도를 높이는 데에도 중요한 역할을 합니다. 게임 저장 시스템을 구현하고 이를 정리하였습니다.

 

📺 미리보기

 

💬 서론

  • 본 기능은 게임을 저장하고 불러오는 기능으로 저장할 데이터들은 게임의 스타일 및 제작자의 의도에 따라 다를 수 있습니다.
  • 본 글에서는 게임 저장 시스템을 어떤 의도로 구현하였는지 요약하여 정리하였습니다.
  • 모든 내용이 들어가있지 않으며, 게임 저장 및 불러오기에대한 대략적인 아이디어를 정리하였습니다.

 

📖 구현 내용

  • 게임을 저장하고 불러옵니다.
  • 플레이어의 위치, 바라보는 방향, 체력, 스탯 등 플레이 진척 사항을 데이터로 저장합니다.
  • 저장한 데이터를 불러오고 저장 직전의 상태와 동일하게 불러옵니다.
  • 필수적인 요소가 아닌 외적 요소 또한 유동적으로 저장 기능에 추가하거나 제거할 수 있습니다.

 

✅ 구현

  • 이번 글에서는 저장 및 불러오기 기능을 어떻게 구현하였는지 그 예시를 간단하게 다룹니다.
  • 전체적인 내용을 다루기에는 게임별로 디자인이 다르기에 모든 내용을 다루는것은 불필요하다고 판단됩니다.
  • 예시로 씬에 배치된 아이템을 저장하고, 불러오는 기능을 다뤄보겠습니다.

· 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를 최소화합니다.
bonnate