i-school - 2D放置ゲーム 手順11
 ゲームのベースとなるお使いのサイクルが完成しました。
この手順より、放置ゲームとして必要となる機能を順番に実装を行っていきます。

 gamebox 様のこちらの記事を参考に、PlayerPrefsJsonUtil クラスと OfflineTimeManager クラスを本プロジェクト用に作成しています。
ありがとうございます。
gamebox 様
【unity】放置ゲーム/ソシャゲ系のオフライン報酬1【スクリプト】
https://www.unitygamebox.com/?p=716


 
 時間の経過を再現するために、現実の時間の情報をゲーム内で活用する必要がありますので、
まずは最初に、ゲーム内で取得した必要な情報を、情報ごとにセーブ・ロードするための機能を実装します。

 処理の実装にあたっては、Unity や C# の用意している3つの機能を活用することで実装を行うように設計しています。


手順11 −ヘルパークラスの作成−
20.PlayerPrefsHelper スクリプトを作成する



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

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



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

1.設計


 現実の時間をゲーム内に反映するために、お使いを開始した時間を記録しておいて、その後の時間と比べることで差分値を算出できるようにします。
その機能を実装するためには、まずは時間をセーブとロードをするための機能が必要になります。

 現実の時間については Unity の用意している DateTime(デイトタイム) 構造体を利用することで、現在の時間を取得することが出来ます。

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

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

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



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

 このようなときにはいくつかの方法が考えられます。(他にもあります)

<型が異なる際のセーブの方法>
 1.セーブ・ロードが行える型の書式になるように、保存するデータ自体の型の変更をする
 2.保存したいデータの型の作りをセーブ・ロードが行える型に変更してから、セーブ・ロードを行う

 今回は【2】の方法を採用します。

 時間の情報をセーブ・ロードする際にはDateTime 構造体の情報やクラスの情報を、セーブ・ロードできる状態の 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つあり、それぞれが異なるインスタンスを持っている状態です。
よって、スクリプト内において 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...



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



 以上でこの手順は終了です。
 
 次は 手順12 −ゲーム開始時間と差分値の取得処理の実装− です。