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

 

📺 미리보기

 

💬 서론

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

 

📖 구현 내용

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

 

✅ 구현

  • 이번 글에서는 저장 및 불러오기 기능을 제공할 슬롯에 대한 내용을 정리하였습니다.

· Script

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using TMPro;
using UnityEngine.UI;
using UnityEngine.SceneManagement;
using System.IO;
using DialogBox;

namespace GameSave
{
    public class GameDataSaveLoadSlot : MonoBehaviour
    {
        [field: Header("이 슬롯의 고유한 번호")]
        [field: SerializeField] public int SlotId { private set; get; } = -1;

        [Space(30)]
        [Header("슬롯의 버튼 (저장, 불러오기, 삭제)")]
        [SerializeField] private Button mSaveButton;
        [SerializeField] private Button mLoadButton;
        [SerializeField] private Button mRemoveButton;

        [Space(30)]
        [Header("슬롯의 메인, 서브 라벨")]
        [SerializeField] private TextMeshProUGUI mMainLabel;
        [SerializeField] private TextMeshProUGUI mSubLabel;

        private void Awake()
        {
            UpdateSlot();
        }

        private void OnEnable()
        {
            UpdateSlot();

            // 현재 세이브로드 슬롯을 사용하는 씬이 타이틀이라면?
            if (SceneManager.GetActiveScene().name == "Main Title")
                mSaveButton.interactable = false;// 세이브 버튼 비활성화
        }

        public void UpdateSlot()
        {
            // 슬롯 번호에 해당하는 세이브 파일이 있으면?
            if (GameDataSaveLoadManager.Instance.FileExists(SlotId))
            {
                // 코어 파일 읽기
                {
                    string fromJson = File.ReadAllText(GameDataSaveLoadManager._FILE_PATH + SlotId);
                    GameDataCore dataCore = JsonUtility.FromJson<GameDataCore>(fromJson);

                    mMainLabel.text = dataCore.gameBaseInfo.dateTime;
                    mSubLabel.text = $"위치:{dataCore.gameBaseInfo.sceneName} / 레벨:{dataCore.playerGameData.statData.level}";
                }

                // 로드, 제거 버튼 활성화
                mLoadButton.interactable = true;
                mRemoveButton.interactable = true;
            }
            else
            {
                // 로드, 제거 버튼 비활성화
                mLoadButton.interactable = false;
                mRemoveButton.interactable = false;

                mMainLabel.text = "저장된 파일이 없습니다.";
                mSubLabel.text = "";
            }
        }
        
        public void BTN_Save()
        {
            if (GameDataSaveLoadManager.IsSaveButtonLocked)
            {
                DialogBoxController dialogBox = DialogBoxGenerator.Instance.CreateSimpleDialogBox("알림", $"지금은 게임을 저장할 수 없습니다.", "확인", DialogBoxController.RESERVED_EVENT_CLOSE, null, 220);
                dialogBox.ModifyBottomLayoutPadding(70, 70);
                return;
            }

            // 이미 파일이 있으면? > 덮어쓰기 위험성 알리기
            if (GameDataSaveLoadManager.Instance.FileExists(SlotId))
            {
                DialogBoxController dialogBox = DialogBoxGenerator.Instance.CreateSimpleDialogBox(
                    "알림", $"{SlotId + 1}번째 슬롯에 게임을 저장합니까?", "예", "YES",
                    (controller, eventArg) =>
                    {
                        switch (eventArg)
                        {
                            case "YES":
                                {
                                    GameDataSaveLoadManager.Instance.SaveGameData(SlotId);
                                    UpdateSlot();
                                    break;
                                }
                            case "NO":
                                {
                                    break;
                                }
                        }

                        controller.DestroyBox();
                    },
                    220, 130, 30, 30);

                dialogBox.AddButton(null, true, "아니오", "NO");
                dialogBox.AddBorder(null, true, 10, true);
                dialogBox.AddText(null, true, "기존의 파일을 덮어쓰기합니다.", 20, TextAlignmentOptions.Center);
                dialogBox.ModifyBottomLayoutPadding(32, 32, 5, 5, 15);
            }
            else
            {
                GameDataSaveLoadManager.Instance.SaveGameData(SlotId);
                UpdateSlot();
            }
        }

        public void BTN_Load()
        {
            DialogBoxController dialogBox = DialogBoxGenerator.Instance.CreateSimpleDialogBox(
                "게임 불러오기", $"{SlotId + 1}번째 게임을 불러옵니까?", "예", "YES",
                (controller, eventArg) =>
                {
                    switch (eventArg)
                    {
                        case "YES":
                            {
                                string fromJson = File.ReadAllText(GameDataSaveLoadManager._FILE_PATH + SlotId);
                                GameDataCore dataCore = JsonUtility.FromJson<GameDataCore>(fromJson);

                                LoadingSceneController.Instance.LoadScene(dataCore.gameBaseInfo.sceneName, () =>
                                {
                                    GameDataSaveLoadManager.Instance.LoadGameData(SlotId);
                                });
                                break;
                            }
                        case "NO":
                            {
                                break;
                            }
                    }

                    controller.DestroyBox();
                },
                220, 130, 30, 30);

            dialogBox.AddButton(null, true, "아니오", "NO");
            dialogBox.ModifyBottomLayoutPadding(32, 32, 5, 5, 15);
        }

        public void BTN_Remove()
        {
            DialogBoxController dialogBox = DialogBoxGenerator.Instance.CreateSimpleDialogBox(
                            "알림", $"{SlotId + 1}번째 슬롯의 데이터를 삭제합니까?", "예", "YES",
                            (controller, eventArg) =>
                            {
                                switch (eventArg)
                                {
                                    case "YES":
                                        {
                                            GameDataSaveLoadManager.Instance.RemoveGameData(SlotId);
                                            UpdateSlot();
                                            break;
                                        }
                                    case "NO":
                                        {
                                            break;
                                        }
                                }

                                controller.DestroyBox();
                            },
                            220, 130, 30, 30);

            dialogBox.AddButton(null, true, "아니오", "NO");
            dialogBox.ModifyBottomLayoutPadding(32, 32, 5, 5, 15);
        }
    }
}

 

[field: Header("이 슬롯의 고유한 번호")]
[field: SerializeField] public int SlotId { private set; get; } = -1;
  • 이 슬롯이 몇번째인지 확인하기위한 변수입니다.
  • 슬롯 번호에 따라 게임을 저장하고, 파일을 구분하기 위해 사용합니다.

 

[Space(30)]
[Header("슬롯의 버튼 (저장, 불러오기, 삭제)")]
[SerializeField] private Button mSaveButton;
[SerializeField] private Button mLoadButton;
[SerializeField] private Button mRemoveButton;
  • 버튼들을 가져옵니다.
  • 조건에 맞게 특정 버튼의 사용 가능 여부를 유연하게 설정하기위해 가져옵니다.

 

[Space(30)]
[Header("슬롯의 메인, 서브 라벨")]
[SerializeField] private TextMeshProUGUI mMainLabel;
[SerializeField] private TextMeshProUGUI mSubLabel;
  • 세이브 슬롯의 현재 정보를 표시하기위한 라벨들입니다.

 

private void OnEnable()
{
    UpdateSlot();

    // 현재 세이브로드 슬롯을 사용하는 씬이 타이틀이라면?
    if (SceneManager.GetActiveScene().name == "Main Title")
        mSaveButton.interactable = false;// 세이브 버튼 비활성화
}
  • 슬롯이 활성화 될 때 호출되며 갱신합니다.
  • 만약 현재 씬이 메인 타이틀이라면, 타이틀은 게임을 하는 씬이 아니기에 저장기능을 비활성화합니다.

 

public void UpdateSlot()
{
    // 슬롯 번호에 해당하는 세이브 파일이 있으면?
    if (GameDataSaveLoadManager.Instance.FileExists(SlotId))
    {
        // 코어 파일 읽기
        {
            string fromJson = File.ReadAllText(GameDataSaveLoadManager._FILE_PATH + SlotId);
            GameDataCore dataCore = JsonUtility.FromJson<GameDataCore>(fromJson);

            mMainLabel.text = dataCore.gameBaseInfo.dateTime;
            mSubLabel.text = $"위치:{dataCore.gameBaseInfo.sceneName} / 레벨:{dataCore.playerGameData.statData.level}";
        }

        // 로드, 제거 버튼 활성화
        mLoadButton.interactable = true;
        mRemoveButton.interactable = true;
    }
    else
    {
        // 로드, 제거 버튼 비활성화
        mLoadButton.interactable = false;
        mRemoveButton.interactable = false;

        mMainLabel.text = "저장된 파일이 없습니다.";
        mSubLabel.text = "";
    }
}
  • 슬롯의 현 상태를 업데이트합니다.
  • 만약 슬롯 번호에 해당하는 세이브파일이 있으면 로드가 가능하기에 파일을 읽어 요약 정보를 라벨에 표시해줍니다.
  • 만약에 세이브 파일이 없으면, 삭제 및 불러오기가 불가능하기에 해당 버튼들을 비활성화하고 라벨 또한 세이브 파일이 없다고 표시해줍니다.

 

public void BTN_Save() { ... }
  • 해당 슬롯에 게임을 저장합니다.
  • 만약에 특정 조건에 의해 세이브가 막혀있다면, 세이브가 불가능하다는 다이얼로그 창을 띄웁니다.
  • 막혀있지 않다면 저장을 시도합니다.
  • 만약 이미 파일이 있다면, 덮어쓰기 경고 다이얼로그 창을 띄워줍니다.
  • GameDataSaveLoadManager를 호출하여 현재 씬의 정보를 읽어 텍스트파일로 저장합니다.

 

public void BTN_Load() { ... }
  • 해당 슬롯의 게임 데이터를 불러옵니다.
  • 게임 데이터에 맞는 씬을 읽어 해당 씬을 로드하며 로드 후 데이터를 적용하도록 합니다.

 

public void BTN_Remove() { ... }
  • 해당 슬롯의 파일을 제거합니다.

 

· UI

  • 전체적인 디자인 구성입니다.

 

  • 각 슬롯에서 사용하는 구성요소입니다.
bonnate