見出し画像

【Unity】JsonUtilityを使ってデータを保存、読み出し、書き出ししてみた

JsonUtilitiyを使ってデータを読み出し書き出しした時の備忘録です。最後にUnityでデータを取り扱った時の所感をまとめています。

この記事を読めば、JsonUtilityを使ってデータの保存、読み出し、書き出しを行うための手順がわかり、これらが可能になります。

1.前置き

こちらに書いたプロジェクトをベースに、読み出し、書き出しを追加しています。

なお、ベースにしたプロジェクトから少し変更を行いまして、これまではSpaceキーを押すことでアイテムを生成していたのですが、タップすることでアイテムを生成できるように変更しました。
この変更後に次に示す2通りの方法で読み書きテストを行なってみました。

2.【例1】 JsonUtilityの読み書きテスト

こちらのサイトを模範して行いました。

2.1 SaveData.csの作成と変更

「Project」タブの「Assets」フォルダから「Scripts」フォルダにアクセスします。「Script」フォルダで右クリックし、「Create」-「C# Script」で新規csファイルを生成します。ファイル名は「FileManeger.cs」とします。

次のようなコードに書き換えます。

//************************************************************
//  1. 参照ライブラリ
//************************************************************
using System.Collections.Generic;
using UnityEngine;
using System;
using System.IO;
using System.Runtime.Serialization.Formatters.Binary;
//************************************************************
//  2. クラス
//************************************************************
[Serializable]
public class SaveData : ISerializationCallbackReceiver
{
//********************************************************
//  2.1 グローバル変数
//********************************************************
public static SaveData Instance
{
get
{
if (_instance == null)
{
Load();
}
return _instance;
}
}
//********************************************************
//  2.2 ローカル変数
//********************************************************
//シングルトンを実装するための実体、初アクセス時にLoadする。
private static SaveData _instance = null;

//SaveDataをJsonに変換したテキスト(リロード時に何度も読み込まなくていいように保持)
[SerializeField]
private static string _jsonText = "";

//--------------------------------------------------------
//保存されるデータ(public or SerializeFieldを付ける)
//--------------------------------------------------------
public int SampleInt = 10;
public string SampleString = "Sample";
public bool SampleBool = false;

public List<int> SampleIntList = new List<int>() { 2, 3, 5, 7, 11, 13, 17, 19 };

[SerializeField]
private string _sampleDictJson = "";
public Dictionary<string, int> SampleDict = new Dictionary<string, int>()
{
    {"Key1", 50},
    {"Key2", 150},
    {"Key3", 550}
};


//********************************************************
//  2.3 列挙体
//********************************************************


//********************************************************
//  2.4 構造体
//********************************************************

//********************************************************
//  2.5 コンストラクタ
//********************************************************

//********************************************************
//  2.6 静的関数
//********************************************************

//********************************************************
//  2.7 動的関数
//********************************************************
//--------------------------------------------------------
//  シリアライズ,デシリアライズ時のコールバック
//--------------------------------------------------------
/// <summary>
/// SaveData→Jsonに変換される前に実行される。
/// </summary>
public void OnBeforeSerialize()
{
    //Dictionaryはそのままで保存されないので、シリアライズしてテキストで保存。
    _sampleDictJson = Serialize(SampleDict);
}

/// <summary>
/// Json→SaveDataに変換された後に実行される。
/// </summary>
public void OnAfterDeserialize()
{
    //保存されているテキストがあれば、Dictionaryにデシリアライズする。
    if (!string.IsNullOrEmpty(_sampleDictJson))
    {
        SampleDict = Deserialize<Dictionary<string, int>>(_sampleDictJson);
    }
}



//引数のオブジェクトをシリアライズして返す
private static string Serialize<T>(T obj)
{
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    MemoryStream memoryStream = new MemoryStream();
    binaryFormatter.Serialize(memoryStream, obj);
    return Convert.ToBase64String(memoryStream.GetBuffer());
}

//引数のテキストを指定されたクラスにデシリアライズして返す
private static T Deserialize<T>(string str)
{
    BinaryFormatter binaryFormatter = new BinaryFormatter();
    MemoryStream memoryStream = new MemoryStream(Convert.FromBase64String(str));
    return (T)binaryFormatter.Deserialize(memoryStream);
}

//--------------------------------------------------------
//  取得
//--------------------------------------------------------
/// <summary>
/// データを再読み込みする。
/// </summary>
public void Reload()
{
    JsonUtility.FromJsonOverwrite(GetJson(), this);
}

//データを読み込む。
private static void Load()
{
    _instance = JsonUtility.FromJson<SaveData>(GetJson());
}

//保存しているJsonを取得する
private static string GetJson()
{
    //既にJsonを取得している場合はそれを返す。
    if (!string.IsNullOrEmpty(_jsonText))
    {
        return _jsonText;
    }

    //Jsonを保存している場所のパスを取得。
    string filePath = GetSaveFilePath();

    //Jsonが存在するか調べてから取得し変換する。存在しなければ新たなクラスを作成し、それをJsonに変換する。
    if (File.Exists(filePath))
    {
        _jsonText = File.ReadAllText(filePath);
    }
    else
    {
        _jsonText = JsonUtility.ToJson(new SaveData());
    }

    return _jsonText;
}


//--------------------------------------------------------
//  保存
//--------------------------------------------------------
/// <summary>
/// データをJsonにして保存する。
/// </summary>
public void Save()
{
    _jsonText = JsonUtility.ToJson(this);
    File.WriteAllText(GetSaveFilePath(), _jsonText);
}

//--------------------------------------------------------
//  削除
//--------------------------------------------------------
/// <summary>
/// データを全て削除し、初期化する。
/// </summary>
public void Delete()
{
    _jsonText = JsonUtility.ToJson(new SaveData());
    Reload();
}

//--------------------------------------------------------
//  保存先のパス
//--------------------------------------------------------
//保存する場所のパスを取得。
private static string GetSaveFilePath()
{

    string filePath = "SaveData";

    //確認しやすいようにエディタではAssetsと同じ階層に保存し、それ以外ではApplication.persistentDataPath以下に保存するように。
    #if UNITY_EDITOR
            filePath += ".json";
    #else
        filePath = Application.persistentDataPath + "/" + filePath;
    #endif

    return filePath;
}
}

エディタで確認した時は次のようなかたちです。(一部抜粋)

2.2 GameController.csでのデータの読み出し書き出し処理追加

GameController.csのAwake関数部分に次の処理を追加します。

    //----------------------------------------------------
    // データの読み出しと書き出しテスト
    //----------------------------------------------------
    //データの定義
    //データの取得
    int value = SaveData.Instance.SampleInt;
    string text = SaveData.Instance.SampleString;

    //データの変更
    SaveData.Instance.SampleInt = 5;
    SaveData.Instance.SampleString = "テスト";

    //全データの保存
    SaveData.Instance.Save();

    //全データの再読み込み
    //SaveData.Instance.Reload();

    //全データの削除
    //SaveData.Instance.Delete();

エディタで確認した時は次のようなかたちです。(一部抜粋)

2.3 データの読み出し、書き出しテスト

プログラムを実行するとAssetsと同じ階層にSavaData.jsが追加されています。こちらはUnity上で確認したのではなく、MACにデフォルトで搭載されているFinderで直接ファイルの状態がどうなったか見にいっています。

プログラム実行前
プログラム実行後

こちらのコードの良い点は開発時は上記の通りAssetsフォルダと同じ階層にSaveData.jsが生成されますが、アプリリリース時にはApplication.persistentDataPath以下に保存されます。

この方法で実装するとファイルの中身は次のようになっておりパッと見では次のようになっているため、改ざんなどは難しくなっています。


3.【例2】JsonUtilityの読み書きテスト

準備として3つのC#ファイルを新規作成します。読み書き準備のサンプルはこちらを参考にしましたが、こちらは採用しませんでした。なぜならデータの一括書き込み機能が活用できできなかったからです。

こちらのサンプルはインターフェースISaveableクラスを継承したクラスでデータを管理することでカプセル化して、各種データをクラスで塊にして、さらにそれを配列で複数持てるようなカタチとなっています。ISaveableにはPopulateSaveData関数とLoadFromSaveData関数が定義されており、データごとにこの関数が呼ばれるので、任意の読み込み、書き込み時の処理を書くことができます。

こちらのサンプルでデータの一括書き込みをするときはSaveDataManegerクラスのSaveJsonData関数を利用するのですが、これを使うと肝心のデータが初期値()となります。

SaveDataManager.SaveJsonData(_readData1);

そこで、データの塊ごとにデータを次のようにPopulateSaveData関数で書き込無事で、書き込みはできるのですが、少し気になったので、こちらのサンプルは活用しませんでした。

_readData1[0].PopulateSaveData(originData1); 

なお、読み込み方法は2通りあり、データを塊ごとに読みだす方法と、全データ読み出す方法です。
■全データ読み出す方法

    SaveDataManager.LoadJsonData(forDebugReadArray);  

■データを塊ごとに読みだす方法

    forDebugReadArray[0].LoadFromSaveData(forDebugDataReference);

このサンプルの良いところは、JsonUtilityを利用する時に多くの人が行う実装であろうデータの保存場所の定義設定を行わなくて良い点です。本サンプルでは書き込みの時にデータの保存場所からJsonファイルの生成とそこへの書き込みを行なっています。

今回のサンプル


3.1 FileManeger.csの作成と変更

4つのファイルを作成していきます。はじめに「Project」タブの「Assets」フォルダから「Scripts」フォルダにアクセスします。「Script」フォルダで右クリックし、「Create」-「C# Script」で新規csファイルを生成します。ファイル名は「FileManeger.cs」とします。

FileManegerを開き、次のコードに書き換えます。

//************************************************************
//  1. 参照ライブラリ
//************************************************************
using System;
using System.IO;
using UnityEngine;
//************************************************************
//  2. クラス
//************************************************************
public static class FileManager
{
//********************************************************
//  2.1 グローバル変数
//********************************************************
//********************************************************
//  2.2 ローカル変数
//********************************************************

//********************************************************
//  2.3 列挙体
//********************************************************

//********************************************************
//  2.4 構造体
//********************************************************

//********************************************************
//  2.5 コンストラクタ
//********************************************************


//********************************************************
//  2.6 静的関数
//********************************************************


public static bool WriteToFile(string a_FileName, string a_FileContents)
{
    var fullPath = Path.Combine(Application.persistentDataPath, a_FileName);

    try
    {
        File.WriteAllText(fullPath, a_FileContents);
        return true;
    }
    catch (Exception e)
    {
        Debug.LogError($"Failed to write to {fullPath} with exception {e}");
        return false;
    }
}

public static bool LoadFromFile(string a_FileName, out string result)
{
    var fullPath = Path.Combine(Application.persistentDataPath, a_FileName);

    try
    {
        result = File.ReadAllText(fullPath);
        return true;
    }
    catch (Exception e)
    {
        Debug.LogError($"Failed to read from {fullPath} with exception {e}");
        result = "";
        return false;
    }
}

//********************************************************
//  2.7 動的関数
//********************************************************
}

エディタで確認した時は次のようなかたちです。(一部抜粋)


3.2 SaveData.csの作成と変更

同様の手順でSaveData.csを作成します。

//************************************************************
//  1. 参照ライブラリ
//************************************************************
using System.Collections.Generic;
using UnityEngine;
//************************************************************
//  2. クラス
//************************************************************
[System.Serializable]
public class SaveData
{
//********************************************************
//  2.1 グローバル変数
//********************************************************
public int m_Score;
public List<EnemyData> m_EnemyData = new List<EnemyData>();
//********************************************************
//  2.2 ローカル変数
//********************************************************

//********************************************************
//  2.3 列挙体
//********************************************************
[System.Serializable]
public struct EnemyData
{
    public string m_Uuid;
    public int m_Health;
}

//********************************************************
//  2.4 構造体
//********************************************************

//********************************************************
//  2.5 コンストラクタ
//********************************************************

//********************************************************
//  2.6 静的関数
//********************************************************

//********************************************************
//  2.7 動的関数
//********************************************************
public string ToJson()
{
    return JsonUtility.ToJson(this);
}

public void LoadFromJson(string a_Json)
{
    JsonUtility.FromJsonOverwrite(a_Json, this);
}
}
public interface ISaveable
{
void PopulateSaveData(SaveData a_SaveData);
void LoadFromSaveData(SaveData a_SaveData);
}
public class  CharacterSaveable1 : ISaveable
{
public void PopulateSaveData(SaveData a_SaveData)
{
FileManager.WriteToFile("SaveData01.dat", a_SaveData.ToJson());
}
public void LoadFromSaveData(SaveData a_SaveData)
{
    if (FileManager.LoadFromFile("SaveData01.dat", out var json))
    {
        a_SaveData.LoadFromJson(json);
    }
}
}

エディタで確認した時は次のようなかたちです。(一部抜粋)

3.3 SaveDatamanager.csの作成と変更

同様の手順でSaveDataManager.csを作成します。

//************************************************************
//  1. 参照ライブラリ
//************************************************************
using System.Collections.Generic;
using UnityEngine;
//************************************************************
//  2. クラス
//************************************************************
public class SaveDataManager
{
//********************************************************
//  2.1 グローバル変数
//********************************************************
//********************************************************
//  2.2 ローカル変数
//********************************************************

//********************************************************
//  2.3 列挙体
//********************************************************

//********************************************************
//  2.4 構造体
//********************************************************

//********************************************************
//  2.5 コンストラクタ
//********************************************************

//********************************************************
//  2.6 静的関数
//********************************************************

//********************************************************
//  2.7 動的関数
//********************************************************
public static void SaveJsonData(IEnumerable<ISaveable> a_Saveables)
{
    SaveData sd = new SaveData();
    foreach (var saveable in a_Saveables)
    {
        saveable.PopulateSaveData(sd);
    }

    if (FileManager.WriteToFile("SaveData01.dat", sd.ToJson()))
    {
        Debug.Log("Save successful");
    }
}

public static void LoadJsonData(IEnumerable<ISaveable> a_Saveables)
{
    if (FileManager.LoadFromFile("SaveData01.dat", out var json))
    {
        SaveData sd = new SaveData();
        sd.LoadFromJson(json);

        foreach (var saveable in a_Saveables)
        {
            saveable.LoadFromSaveData(sd);
        }

        Debug.Log("Load complete");
    }
}
}

エディタで確認した時は次のようなかたちです。(一部抜粋)

3.4 GameController.csでのデータの読み出しと書き出し処理追加

Awake部分でデータの読み出し、書き出しを行います。次のコードを追加します。SaveDataクラスのm_Scoreの値を0から4に変更してみます。

   //----------------------------------------------------
    // データの読み出しと書き出しテスト
    //----------------------------------------------------
    //データの定義
    SaveData originData1 = new SaveData();
    originData1.m_Score = 4;
    CharacterSaveable1 _someObject1 = new CharacterSaveable1();
    CharacterSaveable1[] _readData1 = new[] { _someObject1 };

    //セーブデータ
    _readData1[0].PopulateSaveData(originData1);
    //SaveDataManager.SaveJsonData(_readData1);
    

    //ロードデータ 1
    CharacterSaveable1 forDebugRead = new CharacterSaveable1();
    var forDebugReadArray = new[] { forDebugRead };
    SaveData forDebugDataReference = new SaveData();
    forDebugReadArray[0].LoadFromSaveData(forDebugDataReference);//forDebugDataReferenceに参照渡しされる
    print("ロードデータ確認用 1:" + forDebugDataReference.m_Score.ToString());

    //ロードデータ 2
    CharacterSaveable1 forDebugRead2 = new CharacterSaveable1();
    var forDebugReadArray2 = new[] { forDebugRead2 };
    SaveData forDebugDataReference2 = new SaveData();
    SaveDataManager.LoadJsonData(forDebugReadArray2);  //forDebugReadArray2に参照渡しされる
    forDebugReadArray2[0].LoadFromSaveData(forDebugDataReference2);//forDebugDataReference2に参照渡しされる
    print("ロードデータ確認用 2:" + forDebugDataReference2.m_Score.ToString());

エディタで確認した時は次のようなかたちです。(一部抜粋)

3.5 データの読み出しと書き出しテスト

実行した時のログの様子です。m_Scoreの値を0から4に変更できたことがわかります。

4.Unityでデータを扱う実装をした所感

「Unityでデータの保存、読み出し、書き出しを扱いたい!どうしよう?」と思った時に真っ先に行ったことは、その手段の検索でした。それはこちらの記事にまとめています。


4.1 実用的なのは「JsonUtility」、「EditorJsonUtility」「JSON.NET」

その中で使えると思えたのはUnityのデフォルトのライブラリ「JsonUtility」、「EditorJsonUtility」と無料のUnityのアセットである「JSON.NET」でした。ライブラリ「JsonUtility」、「EditorJsonUtility」はUnityをインストールした時点で搭載されているデフォルトのライブラリですが、「JSON.NET」はお使いの環境にアセットをインポートしてくる必要があります。


4.2 「JSON.NET」は処理速度が100倍遅い

無料のUnityのアセットであるJSON.NETは一見楽そうですが、致命的な落とし穴があります。JSON.NETはJObject.Parseがベースとなっています。そのためか、デフォルトのライブラリであるJsonUtility、EditorJsonUtilityに比べJSON.NETはデータの読み込み速度が100倍遅いです。


4.3 「JsonUtility」は並列処理ができる!

こうしてUnityのデフォルトのライブラリ「JsonUtility」、「EditorJsonUtility」のどちらを利用するか?というところで悩むわけですが、何かの処理中にデータの保存処理を並行して行える、いわゆるバックグランドスレッド処理できるという点でJsonUtilityを利用しようという結論に至りました。


4.4 「JsonUtility」でのファイルマネージャが欲しい

しかしJsonUtilityを利用しようと、データの保存、読み出し、書き出しを行いたいと思って実装してみたりしたのですが、メンテナンス性が悪いというところが課題でした。なにせJsonUtilityはデータの保存、読み出し、書き出しといった単機能は持っていますが、これらを「同じデータの入れ物の中で」要望にはこたえる仕組みにはなっていないのです。そのため、ファイルマネージャなる何かしたデータを管理するための仕組みであるファイルマネージャが必要だなぁと気づき始めます。これをUnityでは「シリアライズ」という機能でユーザに便利な仕組みを提供しています。

4.5 「JsonUtility」におけるファイルマネージャ実現手段(シリアライズ)

Unityでは2種類の方法「SerializeReference」「ISerializationCallbackReceiver」を提供しています。

他のファイルマネージャ実現方法は、世の中のサンプルを利用することです。今回利用したファイルマネージャのサンプルはこちらです。

他にも次のようなサンプルもあります。

JSON を使用したテキスト ベースのシリアル化を使用し、必要に応じてそのデータを暗号化したサンプルはこちらです。Json.Netを利用しています。

こちらはサンプルのチュートリアルが動画になったものです。

4.6 【おまけ】SerializeReferenceとISerializationCallbackReceiverの例

SerializeReferenceはインターフェースや抽象クラスの参照がシリアライズするための機能です。これによりUnityのエディターでインターフェースを編集できるようになります。

ISerializationCallbackReceiverはシリアル化および逆シリアル化時にコールバックをうけることができます。

4.7 データの暗号化について

色々調べた中でJsonUtilityの方法であると、こちらのクラスを使用すると、攻撃者がシステムを乗っ取る可能性などがあり、暗号化などの対策が必要となります。

暗号化の対策について参考になった記事はこちらです。

csにおける文字列の暗号化方法


5.デザインパターン(ソフトウェアアーキテクチャ、設計パターン)

ここから先は

2,004字

¥ 300

期間限定 PayPay支払いすると抽選でお得に!

この記事が気に入ったらサポートをしてみませんか?