【UnityJsonUtility】スクリプタブルオブジェクトのデータを保存する方法

2023年1月27日

スクリプタブルオブジェクトもJson形式に変換することができます
Json形式は文字列型なので、そのままPlayerPrefsで保存、読み出しをすることができます

サンプル

サンプルでは、次を管理するケースを考えてみます

  • ゲームのプレイヤーの名前とライフ(Hp)
  • どのシーンまで進んだか(どのシーンがクリアできたか)
    シーンと名前とクリア済みのフラグのペアで管理

シーンの構成(最終)

空のゲームオブジェクトを作成、SaveLoadCheckSampleスクリプトをアタッチしています
PlayerDataという名前で作成したスクリプタブルオブジェクトはあらかじめ作成しておきます。作成方法についてはページ下部の参考リンクで確認しましょう

PlayerDataスクリプタブルオブジェクト(最終)

スクリプタブルオブジェクトを作るには、先にPlayerDataScriptableスクリプトを作成しておく必要があります
Projectウィンドウを右クリックしてCreateから作成します。名前を変更できますので、今回はPlayerDataとしておきます
プレイヤー情報(名前とhp)とシーン情報(3シーン)をあらかじめインスペクターウィンドウから入力しておきます

クラス作成の考え方

シーンの管理をSceneParamクラスを型パラメータとしたList型で管理するのがポイントです
ここは、シーン名をキーとしてクリア状況を値で取得できるDictionary型を使う方がいいと思う人もいるでしょう
その通りなのですが、スクリプタブルオブジェクトの便利さとPlayerPrefsの簡便さを享受するためにこのようにしています
理由は、Dictionary型がUnityではシリアライズ対象にならないためです(保存できる状態にできない)

シンプルなクラス

public string playerName;
public int hp;

public List<SceneParam> sceneParams;

public class SceneParam
{
    public string sceneName;
    public bool isClear;
}

スクリプタブルオブジェクトとして作成

では、スクリプタブルオブジェクトとしてコーディングしていきましょう
なお、スクリプタブルオブジェクトの詳細についてはページ下部の参照リンクで確認しましょう

「シンプルなクラス」をそのままで作成

using System.Collections.Generic;
using UnityEngine;
using System;

[CreateAssetMenu(menuName = "PlayerData")]
public class PlayerDataScriptable : ScriptableObject
{
    public string playerName;
    public int hp;

    public List<SceneParam> sceneParams;

    [Serializable]
    public class SceneParam
    {
        public string sceneName;
        public bool isClear;
    }
}

今回は、クラスの中にクラスがあるインナークラスを使っています
特定のクラスでのみ使うことを意識づけしたい場合、インナークラス(内部に持つ)といいです
このシーンの情報クラスを他でも使うなど、独立して使う意図があれば外部クラスにして別ファイル(別スクリプト)にするといいでしょう

保存、読み出しメソッドの実装

次にデータを保存、読み出しができるメソッドを考えます
作成後、上記クラスにメンバーとして追記します

  • Jsonに変換後に文字列として保存するSaveメソッド
  • Jsonを読み出した後、スクリプタブルオブジェクトに逆変換するLoadメソッド
重要

JsonUtility.FromJsonOverwrite
スクリプタブルオブジェクトに戻す場合、上書き用のメソッドを使います
詳細は、ページ下部のリンクを参照してください

public void Save()
{
    var data = JsonUtility.ToJson(this, true);

    Debug.Log(data);

    PlayerPrefs.SetString("PlayerData", data);
}

public void Load()
{
    var data = PlayerPrefs.GetString("PlayerData");

    Debug.Log(data);

    JsonUtility.FromJsonOverwrite(data, this);
}

なぜthis?
このクラスから作られたインスタンス自身を対象にしているので、thisを使ってインスタンスをJsonに渡すようにしています

テスト用のコードを作成

コードの処理がうまくいっているかをテストしていきます
保存、読み出しをそれぞれキー入力でテストができるシンプルなのにします

準備

サンプルシーンのイメージはページ最初のシーンの構成を参考にしてください

シーンを構成を参考に次のようにシーンを作りましょう
テスト用のコードを新しく作ったGameObjectにアタッチし、playerStateフィールド(シリアライズ化:SerializeField)に作成済みにスクリプタブルオブジェクトをドラッグ&ドロップしておきます

テスト用コード

using UnityEngine;

public class SaveLoadCheckSample : MonoBehaviour
{
    [SerializeField]
    PlayerDataScriptable playerState;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            playerState.hp++;

            // フラグを反転させています
            playerState.sceneParams[1].isClear = !playerState.sceneParams[1].isClear;

            playerState.Save();
        }

        if (Input.GetKeyDown(KeyCode.L))
        {
            playerState.Load();
        }
    }
}

テスト手順

Sキー押下

  • hpがインクリメント
  • インデックス番号1のシーンのクリアフラグをトグル(キーを押すたびにオン・オフが切り替わる)
  • 全部の値を保存

Lキー押下

  • 保存された全部の値を読み出して、スクリプタブルオブジェクトに反映

実行結果

コンソール画面を確認します。青い部分だけ見るとhpまでしか表示されていないように見えますが、詳細エリアを拡張(境界線をドラッグしながら上に持ち上げる)するとちゃんとシーンのステータスも出てくるのがわかります

機能追加(オプション)

シーン名をキーにしてシーンクリア状況を取得、変更したい

UnityJsonでは、Dictionary型を扱えません
しかし、インデックス番号でのアクセスよりシーン名でアクセスしたいこともあると思います
List型のメンバーを使って、キーと値のペアと考えることでDictionary型のような振る舞いをさせることができます

確認コードを実現するためにインデクサを使う

スクリプタブルオブジェクトの方を確認コード実現のために変更する必要があります
インデクサについては、ページ下部の参照リンクで勉強しましょう

確認用コード

文字列型のシーン名をキーにして、リストメンバーを取得できるコードになります
コードでは、.isClearを続けていますので、シーンクリア情報を取得していることになります

using UnityEngine;

public class SaveLoadCheckSample : MonoBehaviour
{
    [SerializeField]
    PlayerDataScriptable playerState;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            playerState["シーン1"].isClear = true;

            playerState.Save();
        }

        if (Input.GetKeyDown(KeyCode.L))
        {
            playerState.Load();
        }
    }
}

違いを見てみましょう

最初に基本としていたクラスと比較します

インデクサを使わない場合

インスタンスをビリオド(. )でチェーンしていくアクセス方法になります
また、インデックス番号でアクセスするため、人にはわかりにくいですね

playerState.sceneParams[1]

インデクサを使った場合

Dictionaryのようなアクセスができます
シーン名(文字列)をインデックスにできます
短くなって、さらにわかりやすくなりました

playerState["シーン2"]

使っているLINQ説明

FirstOrDefaultメソッドはLINQになります。Listの中から一致ものを探して最初に見つかったものを戻します
ない場合は、デフォルト値(stringの場合は、nullになります)を戻します
よく似たメソッドにFirstがありますが、これは、一致するものがない場合、例外エラーになりエラー処理を作っておかないとアプリが落ちてしまいます
インデックスで渡された文字列とリストの中のsceneNameをFirstOrDefaultメソッドで順番に比べて、最初に見つかったシーン情報のインスタンスで返すコードになります(getのブロックがその役目です)

なお、比較して戻すメソッドの部分はラムダ式も使っています

public SceneParam this[string sceneName]
{
    get
    {
        return sceneParams. FirstOrDefault(scene => scene.sceneName == sceneName);
    }
}

総合のテスト

最初のサンプルから追加した機能

  • Listのラッパー機能(必要なので仕方なく)
  • データの保存、読み出しメソッド
  • シーン名をキーとしてシーン情報を取得する機能

シーン構成

今回は、クラスの保存とリストの保存を試すコードにしています

追加機能をまとめたコード

using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

[CreateAssetMenu(menuName = "PlayerData")]
public class PlayerDataScriptable : ScriptableObject
{
    public string playerName;
    public int hp;

    public List<SceneParam> sceneParams;

    [Serializable]
    public class SceneParam
    {
        public string sceneName;
        public bool isClear;

        public SceneParam(string sceneName, bool isClear)
        {
            this.sceneName = sceneName;
            this.isClear = isClear;
        }
    }

    public SceneParam this[string sceneName]
    {
        get
        {
            return sceneParams. FirstOrDefault(scene => scene.sceneName == sceneName);
        }
    }

    public void Save()
    {
        var data = JsonUtility.ToJson(this, true);

        PlayerPrefs.SetString("PlayerData", data);
    }

    public void Load()
    {
        var data = PlayerPrefs.GetString("PlayerData");

        JsonUtility.FromJsonOverwrite(data, this);
    }
}

テストコード

using UnityEngine;
using UnityEngine.UI;

public class SaveLoadCheckSample : MonoBehaviour
{
    [SerializeField]
    PlayerDataScriptable playerState;

    [SerializeField]
    Text uIText;
    [SerializeField]
    Text uIListText;

    int count;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.S))
        {
            playerState.sceneParams.Add(new PlayerDataScriptable.SceneParam("次郎", true));
            playerState.playerName = $"{count++}太郎";
            uIText.text = playerState.playerName;

            uIListText.text = "";

            foreach (var item in playerState.sceneParams)
            {
                uIListText.text += $"{item.sceneName}\n";
            }

            playerState.Save();

        }

        if (Input.GetKeyDown(KeyCode.L))
        {
            playerState.Load();
            uIText.text = playerState.playerName;

            uIListText.text = "";

            foreach (var item in playerState.sceneParams)
            {
                uIListText.text += $"{item.sceneName}\n";
            }
        }
    }
}

参考

UnityJson

JSON シリアライザ API は、通常の構造体やクラス同様、MonoBehaviour や ScriptableObject サブクラスもサポートしています。ただし、JSON をサブクラスのMonoBehaviour や ScriptableObjectにデシリアライズする場合は、必ず FromJsonOverwrite を使用します。FromJson はサポートされていないので、例外が発生します。

ScriptableObject(スクリプタブルオブジェクト)

インデクサ