i-school - PlayerPrefs 用ヘルパークラスの実装例
 gamebox 様のこちらの記事を参考に、PlayerPrefsHelper クラスを作成しています。
ありがとうございます。
gamebox 様
【unity】放置ゲーム/ソシャゲ系のオフライン報酬1【スクリプト】
https://www.unitygamebox.com/?p=716


 
 ゲーム内で取得した必要な情報群をセーブ用のクラスにまとめ、一括でセーブ・ロードするための機能を実装します。

 処理の実装にあたっては、セーブ・ロードのみの機能を備えたクラスを作成します。
こちらはUnity や C# の用意している3つの機能を活用することで実装を行うように設計しています。

 その後、次の手順において、作成したクラスに用意したメソッドを適宜なタイミングで実行して、セーブとロードを実行します。

 このように、プログラムにおいては、実装の前段階となる異なる機能の実装から作っていくケースが多くあります
処理の全体像を把握した上で下準備を行い、その後、下準備として完成している機能(今回であればセーブ・ロード機能)を使い、
実際に処理に取り入れて構築していきます

 いくつもの異なる処理がつながって、大きな処理をつくっていく(ロジックとして組み立てていく)というイメージを持つことが大切になります。



 新しい学習内容は、以下の通りです。

 ・static クラスと static メソッド
 ・PlayerPrefs クラスと各メソッド ーSet〜 メソッド、Save メソッド、HasKey メソッド、Get〜 メソッド、DeleteKey メソッド、DeleteAll メソッドー
 ・JsonUtility クラス
 ・型引数とジェネリック型



PlayerPrefsHelper スクリプトを作成する

1.設計・考え方


 セーブ・ロードの機能についてはオフライン(サーバーを利用しない)で、スマホの場合であれば端末内にセーブデータを保持するようにします。
この機能は Unity の用意している PlayerPrefs(プレイヤープレフス) クラスを利用することで実装を行います。

 ゲームにおいては様々な部分でセーブとロードを実行することが想定されますので、そういった場合に備えて
PlayerPrefs クラスの機能をさらに自分のゲーム用にカスタマイズして、ゲーム内容に即した1つの新しいクラスを作成しておく設計にします。

 このようにしておくことにより、ゲーム内のセーブ・ロードに関しては、このクラスを利用すればよい状態を作り出すことが出来ます。



 PlayerPrefs クラスでは、3つの型の情報をセーブすることが出来ます。int 型、float 型、string 型になります。
ただし、自作したクラスそのものであったり、あるいは時間のデータである DateTime(デイトタイム) 構造体などは上記のいずれの型にも当てはまりません。
そのため、今のままでは PlayerPrefs の用意している型と異なってしまって、セーブ・ロードが行えません。

 このようなときにはいくつかの方法が考えられますが、今回は、まず保存用のクラスを作成し、その中にセーブ・ロードして利用したい情報をまとめるようにします。
主にはユーザーの情報になりますが、その場合、スコア、お金などの int 型の情報、bool 型の情報、
そして自作クラス内の情報などであっても、保存用のクラスの中に一括でまとめるようにします。
そして、その保存用のクラスのデータの型の作りを PlayerPrefs でのセーブ・ロードが行える string 型に変更してから、セーブ・ロードを行うようにします。

<イメージ>
UserData.cs(セーブしたいクラス その1)
  public class UserData : MonoBehaviour {

      public int score; // 保存したい情報
      public int money; // 保存したい情報
      public bool isFirstMission; // 保存したい情報


   以下には、セーブしなくてもよい情報(変数やメソッド)があるとします。

  }


ItemInvenrty.cs(セーブしたいクラス その2)
  public class ItemInvenrty : MonoBehaviour {

      public class ItemInvenrtyData {
          public int itemId;
          public string itemName;
      }

      public List<ItemInventryData> itemInventryDataList = new();   // 保存したい情報

   以下には、セーブしなくてもよい情報(変数やメソッド)があるとします。

  }

 上記のような構造になっている2つのクラスの内容をセーブ・ロードしたい、と考えた場合には、各変数ごとに1つずつセーブ・ロードさせるのではなく、
別の保存用のクラスを用意し、そちらにセーブ・ロードしたい情報を各クラスからピックアップしてまとめておき、それをセーブ・ロードさせるという形にします。

   ↓

SaveData.cs(上記の2つのクラス内から、セーブしたい情報をまとめたクラス)
  public class SaveData {

      public int score; // 保存したい情報
      public int money; // 保存したい情報
      public bool isFirstMission; // 保存したい情報

      public List<ItemInventryData> itemInventryDataList = new();   // 保存したい情報

   // 他にもあれば追加していく

  }

 このようにすることで、セーブ・ロードに必要な情報をまとめて管理出来ます。



 上記の場合、SaveData クラスは自作クラスになるため、型としては SaveData 型となり、そのままでは PlayerPrefs クラスではセーブ・ロードが行えません。
こちらを string 型の情報へと変換し、その後、PlayerPrefs の機能を利用して string 型でセーブ・ロードを行うようにします。

 Unity の用意している機能の1つに JsonUtility(ジェイソン・ユーティリティ) クラスがあります。
このユーティリティクラスを利用すると、指定した型の情報を Json 形式と呼ばれる種類の string 型の文字に変換することが出来ます。

 また JsonUtility クラスでは、string 型の情報を元のクラスの情報に戻す処理も実行することが出来ます。

 このように、情報を書き換えてあげることで、PlayerPrefs クラスのセーブ・ロードができる string 型になりますので、
セーブを行う際に string 型にして保存し、ロードを行う際にはこの string 型でロードして、その情報を元の型の情報に戻すことで、
PlayerPrefs クラスを利用してセーブとロードが行えるようにします。



 処理の流れをまとめます。

<セーブするとき>
 ・セーブしたい情報(クラス・型)が PlayerPrefs クラスに対応している型ではないため、そのままでは自作クラスや、時間の情報などをセーブできない。
        ↓
 ・セーブしたい情報がクラスの場合、JsonUtility クラスの ToJson メソッドを利用して、セーブしたい情報(クラス・型)を string 型(Json 形式)に変換する。
  
  セーブしたい情報が時間(DateTime 構造体)の場合、ToString メソッドを利用して、string 型に変換する。
        ↓
 ・この string 型の情報を PlayerPrefs クラスの SetString メソッドと Save メソッドを利用してセーブする。

<ロードするとき>
 ・string 型でセーブしてある情報を PlayerPrefs クラスの GetString メソッドを利用してロードをする。
        ↓
 ・ロードした情報(クラス・型)が string 型のため、そのままではセーブする前のクラスの情報として活用できない。
        ↓
 ・ロードした情報がクラスの場合、JsonUtility クラスの FromJson メソッドを利用して、string 型(Json 形式)をロードしたい情報(クラス・型)に変換して復元する。
  これは、セーブした際の情報の型とロードする際の情報の型が同じもの同士で処理が行える。

  ロードした情報が時間(DateTime 構造体)の場合、DateTime 構造体の持つメソッドを利用して変換して復元する。
        ↓
 ・セーブする際の情報(クラス)に復元されるので、この情報をまたゲームで利用する

 簡単な処理ではありませんので、しっかりと処理の流れを把握してください。

 JSON(JavaScript Object Notation)とは「JavaScriptのオブジェクト記法を用いたデータ交換フォーマット」です。
様々な言語でサポートされているため、この情報を活用することにより、他の言語間のデータの受け渡しを簡単にするための機能ですが、
今回の場合は、string 型の情報になり、その後、クラスとして復元できる部分に活用しています。


2.PlayerPrefsHelper スクリプトを作成する


 Unity の PlayerPrefs クラスと JsonUtility クラスを利用して、ゲームの内容に即したクラスを作成します。
このように既存のクラスやメソッドの利便性を高めるように作成するクラスを総称してユーティリティクラス(ヘルパークラス)といいます。

 ユーティリティクラス(ヘルパークラス)は static クラス・ static メソッドのみを持つ内容になります。
そのような構造にしておくことで、各クラスにおいて共通して利用したい処理を、どのクラスからでも簡単にアクセスできるようにしています。
また汎用的に利用する関連性のある処理をまとめておくことも目的にしています。

 モノビヘイビアーの継承がないため、ゲームオブジェクトへのアタッチが不要です(正確に言うと出来ません)。
他のインスタンスメンバーと異なり、クラスごとに唯一の実体を持ち、すべてのインスタンスで共有化される情報になります。

 例を挙げると、各ゲームオブジェクトにはそれぞれ1つずつ TapPointDetail という名前のクラスがアタッチされているとします。
そのため、そのゲームオブジェクトがゲーム内に5つあるとするなら、TapPointDetail クラスも5つあり、
同名クラスではあるものの、それぞれが異なるインスタンスを持っている状態です。
同じ設計図(クラス)から、外見が同じ異なるもの(クラスのインスタンス)を5つ作っているイメージです。
よって、スクリプト内において TapPointDetail クラスの情報を利用したい場合には、対象となる TapPointDetail クラスの情報を取得して利用することになります。

 これに対して、static 修飾子のクラスは、複数存在することがありません。
PlayerPrefsHelper クラスはゲーム内通じて1つしか存在できないようになっています。
よって、対象となるクラスは常に1つだけですので、クラスの情報を取得する必要がありません。

 クラスを人に例えたとして、「TapPointDetail(佐藤) さん」とだけ声をかけると5人の TapPointDetail (佐藤)さんが振り向いてしまうので、
「このゲームオブジェクトにアタッチされている(出身地とか、下の名前とかのイメージ)」TapPointDetail (北海道、〜市在住の佐藤 〜)さんと、個別指定を声をかけるのに対し、
「PlayerPrefsHelper さん」と声をかけると、常に一人しかいないので、その人が必ず振り向いてくれる、というようなイメージです。総理大臣を呼ぶ感じでしょうか。

 static クラスはインスタンスを使って参照できませんので、(クラス)型名を使って参照して処理を行います。
これはどのクラスからでもメソッドの呼び出し命令が実行できます。例えば、【 PlayerPrefsHelper.メソッド名 】と記述すればメソッドを実行できます。
これが他のクラスとの大きな違いになります。

<通常のクラスの参照>
 // クラス(型)と変数の宣言
  TapPointDetail tapPointDetail;

 // 制御したいクラス(型)のインスタンスを取得して代入し、変数を介してインスタンスを参照できる状態にする
  tapPointDetail = GatComponent<TapPointDetail>();

 // 参照できるようになったので、変数を利用して処理を実行する
  tapPointDetail.実行したい public 修飾子のメソッド名や変数名;

<static クラスの参照>
  // インスタンス参照不要のため、クラス(型)をそのまま宣言して参照して処理を実行する
  PlayerPrefsHelper.実行したい public static 修飾子のメソッド名;

 参照して処理を実行していく際の方法が違うことがわかると思います。

参考サイト
MicroSoft
static 修飾子
https://docs.microsoft.com/ja-jp/dotnet/csharp/lan...
未確認飛行 C 様
静的メンバー
https://ufcpp.net/study/csharp/oo_static.html



 今回のヘルパークラスもこれと同様に、static クラスとして作成し、各メソッドも static メソッドになっています。
そのため、上記の使用例をみていただいてもわかる通り、static 修飾子のクラスとメソッドにして作成しておくことによって
いずれのクラスからでも自由に(変数への代入不要で)、呼び出し命令を行うことが出来るようになっています。

 このような設計になっていることによって、ヘルパークラスは利便性を確保しています。

 メインとなっているのは、クラスを string 型としてセーブを行い、ロードして元のクラスに復元する機能をメソッド化して用意しています。
他にも便利な機能して、セーブデータが存在するのかを確認するメソッドや、整数の情報をセーブ・ロードする機能、
デバッグを簡単にするためにセーブデータを削除する機能もメソッド化しています。

 今後も PlayerPrefs クラスを活用したい場合には新しくメソッドを作成して、機能ごとに用意をしておくとよいでしょう。

PlayerPrefsHelper.cs

 <= クリックすると開きます


 スクリプトを作成したらセーブします。

 このスクリプトはゲームオブジェクトにアタッチすることが出来ませんが、
static クラスとして作成しておくことで、ゲームを実行した時点で自動的にインスタンス化されますので、自由に利用できる状態になります。


3.<PlayerPrefs クラスと各メソッド ーSet〜 メソッド、Save メソッド、HasKey メソッド、Get〜 メソッド、DeleteKey メソッド、DeleteAll メソッドー>


 Unity ではデータのセーブ・ロードを行うための PlayerPrefs クラスが用意されています。
PlayerPrefs クラスにはセーブ・ロード用のメソッドや、データの確認メソッド、削除用のメソッドなどが用意されていますので、
こちらを実装することで指定したデータをセーブしたり、ロードしたりすることが可能になっています。

 データの保存先は、Unity Editor や PC ゲームの場合には PC 本体のハードディスク、Web の場合はブラウザ内、スマホ端末の場合にはアプリ内のデータの場所内です。


<PlayerPrefs クラス>
https://docs.unity3d.com/ja/current/ScriptReferenc...


 2つのメソッドを利用してセーブを行いますので、順番に説明します。


1.Set〜 メソッド

 Key という文字列を保存する際の識別子として指定し、その名前を用いて指定された型の情報をセーブするための準備を行います。
Key とはいわばセーブ用のラベルであり、名前を付けて保存のことです。この Key の情報をロードする際にも利用します。

 PlayerPrefs クラスにはセーブの方法が3種類用意されており、Set 〜 で始まる命名規則になっています。今回利用している SetString メソッドもその1つです。
SetString という名前の通り、文字列をセーブしておくことが出来ます。残る2つは SetInt メソッド、SetFloat メソッドであり、これらもメソッド名と同名の型の情報をセーブします。

 今回セーブしたい一番の情報はお使いのデータを管理するクラスと時間の情報になりますが、クラスの情報や構造体はそのままではセーブできません。クラス内には複数の型も混在しています。
こういったケースの場合、クラス内のそれぞれの型を個別にセーブしていては非常に効率が悪く、またロードを行う際にも不便です。また、クラスの情報がある場合、セーブが行えません。

 以上のことをふまえて、セーブしたい時間のクラスの情報を1つの文字列として作成し、その状態でセーブをします。
string 型であれば、SetString メソッドを活用することでセーブを行えるためです。

 このとき、クラスを復元することも考えて、JsonUtility クラスを利用して、Json 形式と呼ばれる形式に則って string 型を作成します。
DateTime 構造体については ToString メソッドを利用して、フォーマット形式を指定して string 型に変換します。

 // オブジェクトのデータを Json 形式に変換
  string json = JsonUtility.ToJson(obj);

  // セーブするための準備・セット
  PlayerPrefs.SetString(key, json);

 SetString メソッドの第1引数が Key になります。メソッドの引数で届いている key の情報をそのまま利用しています。

 SetString メソッドの第2引数がセーブされる文字例になります。
ここに Json 形式に変換されたクラスの情報の string 型を指定しています。

 この方法でセーブしておくことで、クラスの情報を1つにパッケージしてセーブしています。

 Set 〜 で始まる3つのメソッドは、セーブを行う対象を設定しています。そのためセーブするための情報をセットして準備を行うメソッドです。
実際には次に解説する Save メソッドを実行することでデータがセーブされます。


参考サイト
Unity公式スクリプトリファレンス
PlayerPrefs.SetString
https://docs.unity3d.com/ja/current/ScriptReferenc...


2.Save メソッド

 Set 〜 メソッドによって準備された情報をセーブするメソッドです。
この処理が実行されて始めてデータがセーブされます。

 複数の Set 〜 メソッドが実行されていた場合、このメソッドはそれらすべてのセーブを行います。

 // オブジェクトのデータを Json 形式に変換
  string json = JsonUtility.ToJson(obj);

  // セーブするための準備・セット
  PlayerPrefs.SetString(key, json);

  // セーブ
  PlayerPrefs.Save();

 今回の場合には、Json 形式になっている string 型の文字列の情報をセーブしています。


参考サイト
Unity公式スクリプトリファレンス
PlayerPrefs.Save
https://docs.unity3d.com/ja/current/ScriptReferenc...



 続いて、セーブされている Key の情報があるかどうかを確認するためのメソッドと、ロードをする際のメソッドを説明します。
3.HasKey メソッド

 引数に指定した文字列が PlayerPrefs に保存されている Key の情報としてあるかを調べて、Key が存在している場合には true、存在していない場合には false を戻します。

  public static bool ExistsData(string key) {

    // 指定したキーのデータが存在しているか確認して、存在している場合は true 、存在していない場合には false を戻す
        return PlayerPrefs.HasKey(key);
    }

 今回のケースでは、このメソッドを別のメソッド内に記述して、その結果を戻り値として戻す設計になっています。
引数で届いている Key の確認を行い、その存在がない場合(false)と存在する場合(true)とで異なる値を戻すようになっています。

参考サイト
Unity公式スクリプトリファレンス
PlayerPrefs.HasKey
https://docs.unity3d.com/ja/current/ScriptReferenc...


4.Get〜 メソッド

 PlayerPrefs 内にセーブされている Key が存在している場合、その情報を string 型で取得するメソッドです。
第2引数を設定することで、もしも Key が存在しなかった場合には Default 値として設定を行うことも出来ます。
 
 // セーブされているデータをロード
  string json = PlayerPrefs.GetString(key);

 取得した情報は文字列ですので、この情報をさらに変換して、DateTime 構造体、あるいはお使いのデータのクラスに変換して利用できる状態に復元します。

参考サイト
Unity公式スクリプトリファレンス
PlayerPrefs.GetString
https://docs.unity3d.com/ja/current/ScriptReferenc...



 最後に削除用のメソッドを2つ説明します。

5.DeleteKey〜 メソッド

 PlayerPrefs 内にセーブされている情報に Key (引数で指定した文字列)が存在している場合、その情報を削除するメソッドです。
 
 // 指定されたキーのデータを削除
  PlayerPrefs.DeleteKey(key);

 今回のケースでは、メソッドの引数に届いている Key の情報を削除する命令を実行しています。


参考サイト
Unity公式スクリプトリファレンス
PlayerPrefs.DeleteKey
https://docs.unity3d.com/ja/current/ScriptReferenc...


6.DeleteAll〜 メソッド

 PlayerPrefs 内にセーブされているすべての情報をまとめて削除するメソッドです。
この操作は Unity エディターから実行することも可能です。主にデータをリセットする際に利用しますので、
ゲームのデータを初期化してデバッグを行う際にも利用できます。
 
 // すべてのセーブデータを削除
  PlayerPrefs.DeleteAll();

参考サイト
Unity公式スクリプトリファレンス
PlayerPrefs.DeleteAll
https://docs.unity3d.com/ja/current/ScriptReferenc...


4.<JsonUtility クラス>


 Unity が用意している Json 形式のデータを扱うためのクラスです。
メソッドは全部で3つ用意されています。

JsonUtility クラス
https://docs.unity3d.com/ja/current/ScriptReferenc...

 今回は ToJson メソッドと FromJson メソッドを利用しています。

 ToJson メソッドを実行することで、指定したクラス(型)の情報を string 型の Json 形式の文字列に変換します。

 FromJson メソッドを実行することで、Json 形式の string 型の情報をオブジェクトとして作成し、指定したクラス(型)の情報に変換(復元)します。
戻り値のあるメソッドにしてあるため、この情報を別のクラスに提供する(ゲッターメソッドとして利用する)ことが出来ます。

<ToJson メソッド>
    /// <summary>
    /// 指定されたオブジェクトのデータをセーブ
    /// </summary>
    /// <param name="key">データを識別するためのキー</param>
    /// <param name="isSave"></param>
    public static void SaveSetObjectData<T>(T obj, string key) {

        // オブジェクトのデータを Json 形式に変換
        string json = JsonUtility.ToJson(obj);   // <= この処理

        // セット
        PlayerPrefs.SetString(key, json);

        // セットした Key と json をセーブ
        PlayerPrefs.Save();
    }

ToJson メソッド
https://docs.unity3d.com/ja/current/ScriptReferenc...



<FromJson メソッド>
    /// <summary>
    /// 指定されたオブジェクトのデータをロード
    /// </summary>
    /// <typeparam name="T"></typeparam>
    /// <param name="key"></param>
    /// <returns></returns>
    public static T LoadGetObjectData<T>(string key) {

        // セーブされているデータをロード
        string json = PlayerPrefs.GetString(key);

        // 読み込む型を指定して変換して取得
        return JsonUtility.FromJson<T>(json);   // <= この処理
    }

FromJson メソッド
https://docs.unity3d.com/ja/current/ScriptReferenc...


5.<型引数とジェネリック型>


 C# にはジェネリックという機能があります。
これは事前に型を指定するのではなく、メソッドを実行する際に任意の型を指定して実行できるようにしている機能です。

 日頃利用しているものとしては GetComponent メソッドや、List などがあります。

GetComponent メソッド
https://docs.unity3d.com/ja/current/ScriptReferenc...



 ジェネリックの型を示す用語として T が利用されます。ここにはどのような型でも指定していいが、実行時には型を指定する、という意味合いです。

    /// 指定されたオブジェクトのデータをセーブ
    /// </summary>
    /// <param name="key">データを識別するためのキー</param>
    /// <param name="isSave"></param>
    public static void SaveSetObjectData<T>(T obj, string key) {

        // オブジェクトのデータを Json 形式に変換
        string json = JsonUtility.ToJson(obj);

    }

 メソッド名と引数の間に <> があります。この部分は【型引数】と呼ばれる部分です。
GetComponent メソッドの <> と同じで、ここに、型の情報を指定します。GetComponent<Rigidbody>() のように、毎回型を指定して書いていると思いますが、それと同じです。

 このメソッドがジェネリックとして設計されている理由は、セーブを行う際に、どのようなクラスであっても Json 形式に変換できるようにするためです。
例えば、型を指定している場合、その型でしかメソッドを実行することが出来ません。

<型を指定している場合>
  public static void SaveSetObjectData(TapPointDetail obj, string key) {

 上記の例であれば、TapPointDetail クラスを常に引数として指定していますので、このクラスであれば引数として受け入れることが出来ますが、
他のクラスには対応できないことになります。つまり、汎用性がない、とも言えます。

 今回はクラスの情報を Json 形式の string 型に変換してセーブ・ロードを行うことが目的です。
このとき、変換できる型の指定があるかないかによって、メソッドの利便性が大きく変化します。
固定されて指定された型しか変換できないメソッドと、どのような型でも変換できるメソッド、の違いとも言えます。
また、いずれのクラスからでも実行できるように static なメソッドとクラスになっています。

 運用を行う観点から考えると、TapPointDetail クラスを固定してメソッドを作成してしまっている場合、
このクラスを変換してセーブすることはできますが、他のクラスには対応できません。
そうなると、各クラスごとに引数だけを変えたメソッドを複数個用意しておく必要が生まれます

 これは非常に不便であり、メソッドの多態性を考えた場合にも理にかなっているとは言えない設計です。

 そのため今回は、セーブとロードを行うメソッドはジェネリック型とし、処理を実行する際に型を指定してもらうことで
その型に対応して柔軟に処理を実行できるような設計になっています。


参考サイト
Ararami Studio様
C#のジェネリックを使おう
https://araramistudio.jimdo.com/2017/12/26/c-%E3%8...
超初心者向けプログラミング入門 様
ジェネリック
https://programming.pc-note.net/csharp/generic.htm...



 以上で完成です。
この機能を利用することでクラスの情報を JsonUtility を利用して セーブとロードが行えるようになりましたので、
次の手順ではこの PlayerPrefsHelper クラスを利用して、実際にセーブとロードの処理を組み立てていきます。



 以上でこの手順は終了です。