Unity セーブ・ロードの仕組み

ゲームをする側でいる時、前回プレイした時に獲得したアイテムがまだ手元に残っているのを当たり前のように思っていましたが、いざ自分自身がゲームを作る側になると、どういう仕組みでそれが成立しているのか不思議なります。今回はアイテムなどのオブジェクトのセーブ/ロードに関しての話です。（今回話すのはローカルセーブについてです。）

## 実現したいこと
- セーブ / ロード機能の実装

## 現状
- セーブ/ロードがどういう仕組みか分からない
- セーブ/ロード機能の実装方法が分からない

## 仕組みの大まかな説明
セーブするとはどのような仕組みによって成立しているのでしょうか？

ざっくり言うとセーブ対象となるオブジェクトの状態(クラスのフィールドと思って大丈夫かと)を保存することです。オブジェクトの状態を保存をする際にシリアル化と言う重要なプロセスがあります。

シリアル化についての説明は、[Microsoft Document](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/concepts/serialization/)によると
> シリアル化は、オブジェクトを格納するか、メモリ、データベース、またはファイルに転送するためにバイト ストリームに変換するプロセスです。 その主な目的は、必要なときに再作成できるように、オブジェクトの状態を保存しておくことです。 逆のプロセスは、逆シリアル化と呼ばれます。

引用したドキュメントにはわかりやすい図もあったので載せておきます
![](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/concepts/serialization/media/index/serialization-process.gif)

Unityではセーブ機能を実装するための様々な方法があります。

- PlayerPrefs
- ScriptableObjects
- json / xml
- custom binary file


それぞれ長所、短所があり一概にどれが一番良いと言うことはできませんし、自分にあった方法を見つけていただければと思います。

以下の表にざっくりとまとめました。

|実装方法|概要|長所|短所|備考|
|:-----:|:-----|:-----|:-----|:-----:|
|PlayerPrefs|Player Preferences(略してPlayerPrefsかな)を保存、アクセスできる|UnityEngineが提供しているため手軽に扱える。|使えるデータ型の制限(Int, Float, Stringのみ)がある。<br>単純な記録（スイッチのオンオフを覚えておくとか）には向いているが、複雑な処理には向いていない|[Unity Document](https://docs.unity3d.com/ScriptReference/PlayerPrefs.html)|
|ScriptableObjects|クラスのインスタンスとは独立し、データを大量に格納できるコンテナ|アセットとして扱える。<br>メモリ消費量が小さい。|主に開発側のためのものであり、プレイヤーのプレイ中におけるセーブのための機能ではない(つまりプレイヤーの進捗とか保存できない)。|[Unity Documentation](https://docs.unity3d.com/ja/2019.2/Manual/class-ScriptableObject.html)|
|external files(json/xml)|よく使われるマークアップ言語|簡単に修正できる|簡単に修正できる||
|custom binary file|バイナリ形式でシリアライズ、デシリアライズを行う|自由度が高い。<br>バイナリ形式であるため安全性が高い。|コードを書く量が多い。||

## サンプル
[githubにサンプル](https://github.com/JunNishimura/Save-Load-Example)を上げていますので、興味があればご自由に見てください。
本サンプルでは、Custom Binary Fileでセーブ機能を実装しています。

### 実行環境
- macOS Catalina ver10.15.1
- Unity 2019.2.10f1

### 概要

シーン1: 新しくゲームを始めるか、登録済みのプレイヤーをロードするかの選択。（ロードを選択するとシーン3へ行きます）
<img width="400" alt="スクリーンショット 2019-12-08 19.28.00.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191216/e9d5769b-326b-907b-8c4f-677b8b811238.png">

シーン2: セーブするプレイヤーの情報を入力。
<img width="400" alt="スクリーンショット 2019-12-08 19.28.37.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191216/85d25325-efb1-ca06-68fb-6b0a56009324.png">

シーン3: ゲームシーン。セーブデータが正しく保存されているか確かめるためにセーブデータをロードしてプレイヤーに反映します。
<img width="400" alt="スクリーンショット 2019-12-08 19.28.46.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/191216/4f0e7a72-eec4-f7f9-2e92-3f366e556b76.png">

全部を紹介すると長くなるのでセーブ、ロードに関するところをピックアップして以下に紹介します。

### シーン2
シーン1はシーン遷移のみなので飛ばします。

まずセーブするプレイヤーのデータ

```C#:SavePlayerData.cs
[System.Serializable]
public class SavePlayerData
{
    public string name;
    public int age;
    public string color;
}
```
セーブするクラスにはSystem.Serializableと言う属性をつけることでシリアル化を可能にします。
Monobehaviourを継承しているとシリアル化できないの注意しましょう。

次にユーザーの入力をもとにセーブデータを作りシリアル化する箇所です。

```C#:SaveManager.cs
using System.Collections.Generic;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
using UnityEngine;
using UnityEngine.UI;

public class SaveManager : MonoBehaviour
{
    // ----- 一部抜粋 ----- //
    public void OnSaveNewPlayer()
    {
        // セーブデータ作成
        SavePlayerData player = CreateSavePlayerData();
        // バイナリ形式でシリアル化
        BinaryFormatter bf = new BinaryFormatter();
        // 指定したパスにファイルを作成
        FileStream file = File.Create(SaveFilePath);
        // Closeが確実に呼ばれるように例外処理を用いる
        try
        {
            // 指定したオブジェクトを上で作成したストリームにシリアル化する
            bf.Serialize(file, player);
        }
        finally
        {
            // ファイル操作には明示的な破棄が必要です。Closeを忘れないように。
            if (file != null) 
                file.Close();
        }
    }

    // 入力された情報をもとにセーブデータを作成
    private SavePlayerData CreateSavePlayerData() 
    {
        SavePlayerData player = new SavePlayerData();
        player.name = nameInput.text;
        player.age = int.Parse(ageDropdown.options[ageDropdown.value].text);
        player.color = colorDropdown.options[colorDropdown.value].text;
        return player;
    }
    // ----- 一部抜粋 ----- //
}
```

### シーン3
セーブしたファイルをロードする部分はこちらです。

```C#:LoacManager.cs
public class LoadManager : MonoBehaviour
{
    // ----- 一部抜粋 ----- //
    private void LoadPlayer()
    {
        if (File.Exists(SaveFilePath))
        {
            // バイナリ形式でデシリアライズ
            BinaryFormatter bf = new BinaryFormatter();
            // 指定したパスのファイルストリームを開く
            FileStream file = File.Open(SaveFilePath, FileMode.Open);
            try 
            {
                // 指定したファイルストリームをオブジェクトにデシリアライズ。
                SavePlayerData player = (SavePlayerData)bf.Deserialize(file);
                // 読み込んだデータを反映。
                var playerObject = Instantiate(playerPrefab) as GameObject;
                playerObject.GetComponent<PlayerController>().Init(player.name, player.age, player.color);
            }
            finally 
            {
                // ファイル操作には明示的な破棄が必要です。Closeを忘れないように。
                if (file != null) 
                    file.Close();
            }
        }
        else
        {
            Debug.Log("no load file");
        }
    }
    // ----- 一部抜粋 ----- //
}
```
それほど難しくないですね！

シリアライズなど使えるようになると、ランキングやら簡単なログイン機能の実装やら色々できることの幅が広がりそうですね！

## Next Step
- 他のPlayerPrefs/ScriptableObjects/JSON/XMLとの違いを試してみる
- クラウドセーブ
- セーブファイルと暗号化について
- オートセーブ機能について

## 参考文献
[Unite 2016 - Best Practices in Persisting Player Data on mobile](https://www.youtube.com/watch?v=_hAzWgQupms)
[Unite Europe 2017 - How Unity's Serialization system works](https://www.youtube.com/watch?v=N-HJvfVuKRw)
[Saving Game Data in Unity](https://www.youtube.com/playlist?list=PL5KbKbJ6Gf9_Rvhx1ye1iHxtLTsAaH7uD)
[How to Save and Load a Game in Unity](https://www.raywenderlich.com/418-how-to-save-and-load-a-game-in-unity)
[シリアル化](https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/concepts/serialization/)
[Script Serialization](https://docs.unity3d.com/Manual/script-Serialization.html)
[オブジェクトのシリアル化](http://www.kanazawa-net.ne.jp/~pmansato/net/net_tech_serialize.htm)

## コメント
以下の観点でコメントいただけると嬉しく思います！

- 間違い、不備
- 「こういう状況では、このセーブ/ロード方法が良いよ！」