i-school - 2Dタップシューティングゲーム 発展6
 前回の手順で用意した Exp 表示用のゲームオブジェクトに、ゲーム内の値を反映して表示の更新制御を行う処理を実装します。
複数のスクリプトを利用する処理になっていますので、処理の流れを把握していくことが大切です。


<実装画像 エネミーを倒すと Exp を獲得し、合計値を画面に表示更新する>
動画ファイルへのリンク


 以下の内容で順番に実装を進めていきます。

発展6 −GameData の作成、Exp 獲得処理と Exp 表示更新処理の実装−
11.GameData スクリプトを作成し、Exp の値を更新するための変数とメソッドを用意する
12.UIManager スクリプト、EnemyGenerator スクリプト、EnemyController スクリプトを順番に修正して、エネミーが破壊された際に Exp の値を加算する処理と、Exp の表示を更新する一連の処理を追加する



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

 ・シングルトンデザインパターンによるクラスの作成と活用
 ・複数のスクリプトを経由して処理を実行していくロジックの考え方と設計方法



11.GameData スクリプトを作成し、Exp の値を更新するための変数とメソッドを用意する

1.設計


 ゲームを遊んでもらうにあたり、エネミーを倒した際のユーザーへのご褒美を用意することを考えます。
色々な方法が考えられますが、今回は Exp という値を獲得できるようにし、この値をたくさん獲得してもらうことを目的とした設計にします。

 こちらの Exp の値をどのようにゲーム内で利用するかは任意です。
単純にゲームクリア時のスコアの代わりとしてもいいですし、お金のようにたくさん貯めて使えるようにしてもいいでしょう。

 Exp という名称になっていますが、これをどのように使うかは設計者、つまり、自分が考えて使っていくことになります。
もしもお金のようにするのであれば、Exp ではなく Money としてもよいでしょう。

 発展編の後半ではこの Exp の値を利用した処理を用意していますので、そちらも参考にしてください。



 前回の手順で EnemyData クラスに Exp の値を作成し、エネミーごとに異なる値を設定してもらいましたので、
Exp が少ないエネミーとたくさん Exp が獲得できるエネミーがいることになります。
こういった差があることにより、ユーザーはどのエネミーは Exp が高い、といったことを学習していきますので、
Exp という値をエネミーを倒したくなる仕掛けとして利用することできます。



 この手順は、 Exp を獲得した際に管理するための変数を用意し、Exp の値を更新する処理も合わせて作成します。

 こちらにはシングルトンデザインパターンという方式で作成した GameData クラスを用意して、その中に上記の情報を作成します
シングルトンで作成した GameData クラスは、変数への代入処理を行うことなく外部のスクリプトで利用できるクラスになります。

 処理の流れは次のように設計しています。

<処理の流れ>
 1.エネミーを倒す
 2.GameData クラスに用意した totapExp 変数に、倒したエネミーに設定されている Exp を加算する
 3.外部のスクリプトで GameData クラスにある totalExp 変数を参照できるようにし、現在までに獲得している Exp の値として利用できるようにする 

 これが GameData クラスの役割になります。
各クラスごとの役割を明確に分担しておくことで設計を作りやすくし、管理もしやすくなりますので、是非心がけてください。

 どの処理によって Exp を加算していくかは次の手順で説明します。
 

2.<シングルトンデザインパターン>


 シングルトンとは、数多くあるデザインパターンの1つです。
そのクラスのインスタンスが必ず1つであることを保証するデザインパターンのことを言います。

 GameData クラスでは、このシングルトンを採用しています。
つまり、ゲーム中を通じて、この GameData クラスが1つしか存在できないようになります。
実装例は複数ありますが、一番読みやすい方式で記述しています。



<シングルトンデザインパターンのクラスの作成方法>
    public static GameData instance;

    private void Awake() {
        if (instance == null) {
            instance = this;
            DontDestroyOnLoad(gameObject);
        } else {
            Destroy(gameObject);
        }
    }



 ポイントは、自分自身の GameData 型を static 修飾子付きの instance 変数として宣言していることです。
この instance 変数が GameData クラス自身が代入された情報として利用することになります。

 Awake メソッドを利用して、instance 変数が null (空っぽ) である場合には、GameData クラス(this)を代入します。
次の DontDestroyOnLoad メソッドは Unity が用意しているメソッドで、引数に指定されたゲームオブジェクトはシーン遷移をしても破壊されてないゲームオブジェクトになります。
このメソッドはシングルトンデザインパターンにする際に一緒に用いられることが多いです。

 そして instance 変数が null ではない場合、つまり、2つ目以降の複数の GameData クラスが存在する場合には、その GameData クラスのゲームオブジェクトを Destroy します。
この手順により、GameData クラスがアタッチされているゲームオブジェクトが常にヒエラルキー上に1つしか存在しない状態を作り出しています

 このシングルトンによってインスタンスが1つか生成されないことが保証されますので、
この GameData クラスへの参照は、いずれのクラスからであっても変数を介さずに参照を行えるようになります。



 例えば、Enemy というクラスがあり、その Emeny クラスを持つゲームオブジェクトが5つあった場合、「どの」Enemy クラスであるかを確定できないと、対象となる Enemy クラスへは参照できません。
そのため、Enemy 型の変数を用意して、その変数へ参照したい Enemy クラスを代入することによって、はじめて Enemy クラスの情報を扱うことができるようになります。

 ですがシングルトンである GameData クラスの場合には、このインスタンスは常に1つしかないことが保証されていますので、「どの」という指定の部分が不要になります。
つまり変数への代入が不要になります。
GameData という指定はすなわち、自動的にただ1つの GameData クラスの参照が行われます。


3.GameData ゲームオブジェクトを作成する


 最初に、GameData スクリプトをアタッチするゲームオブジェクトを作成します。
このゲームオブジェクトは Canvas 内に表示される情報ではありません。
そのため、GameManager ゲームオブジェクトや、UIManager ゲームオブジェクトなどと同じで、ヒエラルキー内に存在していれば問題ありません

 ヒエラルキーの空いている場所で右クリックをしてメニューを開き、Create Empty を選択します。
新しいゲームオブジェクトが作成されますので、名前を GameData に変更してください。

 Transform コンポーネントを確認して、Position の値を (0, 0, 0) に戻してください。最初からその状態なら変更は不要です。


ヒエラルキー画像



GameData ゲームオブジェクト インスペクター画像



 以上でこのゲームオブジェクトは完成です。


4.GameData スクリプトを作成する


 設計とシングルトンデザインパターンの書式を参考して、GameData スクリプトを作成します。
Exp を管理するための変数を、EnemyData クラスの Exp と同じ型で宣言しておきます。
同じ型にすることで計算処理が可能になります。
SerializeField 属性付きで宣言しておくことによって、インスペクターより値の確認が可能になります。
これにより、一時的に Exp を変更したりする場合など、デバッグする際にも役立ちます。

 メソッドは2つ用意します。

 1つは、totalExp 変数に EnemyData クラスの Exp 変数の値を計算するためのメソッドです。
現在は加算用に考えていますが、メソッド本来は加算でも減算でも対応可能になっていることを念頭に置きましょう。
つまり、引数に届いた値が正の整数であれば加算、負の整数であれば減算になる、ということです。

 メソッドとはこのように、引数によって振る舞いが変わることが最大のメリットです。
1つの方向だけのイメージに捕らわれないように意識しましょう。 


 もう1つは戻り値を持つメソッドです。このメソッドを外部のスクリプトから参照することにより、
int 型の情報として totalExp の値を参照することができるようにします。いわゆる、ゲッターメソッドになります。


GameData.cs


 スクリプトを記述したらセーブします。


5.GameData ゲームオブジェクトに GameData スクリプトをアタッチする


 ヒエラルキーにある GameData ゲームオブジェクトに、作成した GameData スクリプトをドラッグアンドドロップしてアタッチします。
アタッチしたら、必ず対象のゲームオブジェクトのインスペクターを確認します。


GameData ゲームオブジェクト インスペクター画像



 以上で設定は完了です。


12.UIManager スクリプト、EnemyGenerator スクリプト、EnemyController スクリプトを順番に修正して、エネミーが破壊された際に Exp の値を加算する処理と、Exp の表示を更新する一連の処理を追加する

1.設計


 GameData クラスは完成しましたので、こちらを利用することによって、エネミーを倒した際に Exp を加算していく処理と、その値を外部のスクリプトで取得できるようになりました。
あとはこの処理をどのスクリプトに記述していくか、という部分を考えていく必要があります。

 ここでは2つの手順に分けて処理を考えてみましょう。

 1つは、エネミーを倒して、Exp を加算するまでの方法です。
もう1つは、画面に表示されている Exp の値を計算処理が行われるたびに更新を行う方法、以上の、この2つです。
一度にまとめて考えていくのは最初は難しいですので、処理の実装とおなじで、すべて1つずつ、順番に積み重ねて作っていくことが重要です。



 1つ目のエネミーを倒して、Exp を加算するまでの方法については、エネミーを倒した際にそのエネミーの情報として持っている Exp を獲得できるとも言えます。
色々な角度から処理の内容を見ていくことが大切です。特定の日本語の処理だけに捕らわれないようにします

 エネミーを倒した際、ですので、これは EnemyController スクリプトに記述することが最適であると考えられます。
現在エネミーはバレットに接触すると Hp が減算されて、0 以下になったときに破壊されます。
以上のことから、エネミーが破壊される前に、Exp を加算する処理を記述するイメージを持てばよさそうです。



 処理の記述ですが、シングルトンクラスの場合、変数を用意したり、GetComponent メソッドによる取得処理は不要です
GameData クラス、そして、その情報が代入されている instance 変数を記述すれば、自動的に、GameData クラスへの参照情報となります。
GameData クラスに用意してあるメソッドはどちらも public 修飾子で宣言してありますので、外部のスクリプトから実行命令を出すことが可能です。


<シングルトンクラスの参照方法>
  GameData.instance.UpdateTotalExp("int 型の加算したい値");

  int totalExp = GameData.instance.GetTotalExp();

 シングルトンクラスは上記のように、「シングルトンクラス名.シングルトンクラスが代入されている変数」という記述で書くことが出来ます。
今回であれば、GameData.instance と記述すれば、GameData クラスにある public 修飾子の情報をそのあとに記述することで命令や参照が行えます



 2つ目の画面に表示されている Exp の値を計算処理が行われるたびに更新を行う方法については、いくつかの方法が考えられます。
まずは、画面に Exp 表示更新する処理を考えます。何故ならば、これがこの処理のゴール地点(目的)となる処理になるからです。

 Exp の表示は画面上の固定情報になります。つまり、UI ということになりますので、この UI 情報の制御は UIManager スクリプトに記述をするべきである、と考えることが出来ます。
前回の手順で、画面上に Exp の表示用のゲームオブジェクトは製作済ですので、このゲームオブジェクトの持つ Textコンポーネント の情報を取得して変数に代入し、
その情報を利用して、Exp の値を表示更新していく処理を作成すればよいと考えられます。これはメソッドで用意することが適切です。

 何故ならば、エネミーを倒すたびに Exp が加算され、その都度、画面上の Exp の表示更新も処理される、何度も繰り返し発生する処理になるためです。
繰り返して利用する処理についてはメソッド化して、その中に表示更新の処理を記述することで、同じ処理を何回も書かなくて済むように出来ます。

 外部のスクリプトより呼び出される前提のメソッドになりますので、public 修飾子で宣言するメソッドとして用意します。



 目標となる、Exp の表示更新の処理については、どのスクリプトに記述するか目星がつきました。
続いては、この表示更新の処理を行うメソッドを、どのスクリプトから、どのタイミングで呼び出すようにするかを考えます。

 ここが非常に難しいので、混乱しないようにします。

 ゴール地点となるメソッドを用意したら、次は、スタート地点、つまり、処理が始まるスクリプトを考えて、そこからゴールまでの道のりを考えます
ゴールまでの道のりとは、どのスクリプトを経由して、UIManager スクリプトのメソッドを実行するようにするか、ということになります。

 現在までに決まっている処理の流れです。

<ロジックの流れ>
 1.Exp を獲得   ← スタート地点
 2.【ここを考える】 
 3.UIManager スクリプトに用意した、Exp の表示更新を行うメソッドを実行する ← ゴール地点
 
 つまり、【2】の部分がゴールまでの道のりになります。このようにイメージを作り、考えていきましょう。


 スタート地点は「Expを獲得」ということは、「エネミーが破壊される前」の部分に相当しそうです。
この処理は1つ目の方法で提示したように、EnemyController スクリプトにおいて処理される部分です。
ということは、スタート地点とは、EnemyController スクリプトになります。
EnemyController スクリプトから処理を実行し、外部のスクリプトを経由して、UIManager スクリプトの処理を実行できるようにする
これが【2】の部分になります。

 EnemyController スクリプトが変数として管理しているスクリプトの情報は、現在は EnemyGenerator スクリプトのみです。
逆に言えば、EnemyGenerator スクリプトに何かメソッドがあれば、それは EnemyController スクリプトからでも実行できることを意味します。
そしてもう1つ、EnemyGenerator スクリプトで管理している情報に、GameManager スクリプトがあります。
これを前提に考えます。



 現在 UIManager スクリプトの情報を変数として管理しているのは GameManager スクリプトのみです。
ただし、GameManager スクリプトの UIManager スクリプトの情報は public 修飾子で宣言されている変数ですので、
GameManager スクリプトの情報を利用して、UIManager スクリプトの public 修飾子のメソッドを実行することが可能です。

 この手法は以前にも学習している方法です。復習の意味も込めて再度学習を行います。

 GameManager スクリプトの情報があれば、UIManager スクリプトの public 修飾子のメソッドが実行できるということは、
GameManager スクリプトの情報を変数として管理している、EnemyGenerator スクリプトからでも UIManager スクリプトのメソッドを実行することが可能になります。

 先ほども書きましたが、EnemyGenerator スクリプトで管理している情報に、GameManager スクリプトがありますね。
EnemyGenerator スクリプトであれば、スタート地点である、EnemyController スクリプトから命令が実行できます。
ここでやっと、処理がつながり、道のりが完成しました。

 まとめましょう。

<ロジックの流れ>
 1.EnemyController スクリプトにおいて、Exp を獲得   ← スタート地点
 2.EnemyController スクリプトにおいて、EnemyGenerator スクリプトに用意したメソッドを実行し、加算後の Exp の値を渡す
 3.EnemyGenerator スクリプトにおいて、GameManager スクリプトにある UIManager スクリプトの情報を利用して、Exp の表示更新を行うメソッドを実行する  ← ゴール地点

 いかがでしょうか。EnemyController スクリプトから処理が始まって、外部のスクリプトである UIManager スクリプトのメソッドを実行する処理が実行できました。
このようにしてロジックを考えていってください。



 以前にもお伝えしているように、ロジックの流れと処理の実装の順番は同じである必要はなく、逆算して実装していった方が問題は少なく済みます。
今回も同じです。最初にロジックで実行する EnemyController スクリプトから EnemyGenerator スクリプトへの処理は、
当然ながら、EnemyGenerator スクリプト側に処理が用意されていなければ実行できません。

 そういった部分を考えると、やはり、ゴール地点より処理を実装していく方がよいでしょう。

<実装の順番>
 1.UIManager スクリプトを修正して、Exp の値の表示更新を行うメソッドを追加する。
    このメソッドを EnemyGenerator スクリプト側から呼び出し、Exp を引数として受け取る。
 2.EnemyGenerator スクリプトを修正して、 UIManager スクリプトの Exp の値の表示更新を行うメソッドを呼び出す処理を追加する。
    このメソッドを EnemyController スクリプト側から呼び出し、Exp を引数として受け取る。
 3.EnemyController スクリプトを修正して、EnemyGenerator スクリプト側に追加した【2】のメソッドを呼び出す。引数としてエネミーの情報の中にある Exp を渡す

 イメージをしっかりと作って、処理を実装していきましょう。
 

2.UIManager スクリプトを修正する


 設計に基づいて、Textコンポーネント を扱うための変数を SerializeField 属性で宣言し、インスペクターより取得しておきます。
また、EnemyGenerator スクリプト側から呼び出すための、Exp 表示更新を行うメソッドを新しく作成し、引数を設定します。
引数に Exp の現在値が届くことによって、その値を利用して表示更新を行います。


UIManager.cs

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


 スクリプトを作成したらセーブします。
UIManager ゲームオブジェクトのインスペクターを確認し、新しく SerializeField 属性で宣言した変数が表示されていることを確認しておきます。


UIManager ゲームオブジェクト インスペクター画像




3.UIManager ゲームオブジェクトの設定を行う


 UIManager ゲームオブジェクトのインスペクターを確認すると、UIManager スクリプトに txtTotalExp 変数が表示されていますので、
こちらに対象となるコンポーネントがアタッチされているゲームオブジェクトをドラッグアンドドロップしてアサインしてください。


UIManager ゲームオブジェクト インスペクター画像
[


 以上で設定は完了です。


4.EnemyGenerator スクリプトを修正する


 設計に基づいて、EnemyController スクリプト側から呼び出すためのメソッドを新しく作成します。
メソッドには引数を用意し、EnemyController スクリプトから Exp の現在値を受け取ります。

 このメソッド内に UIManager スクリプトに用意した、Exp の表示更新を行うメソッドを呼び出します。
UIManager スクリプトのメソッドにも引数がありますので、EnemyController スクリプトから届いている Exp の現在値を渡します。


EnemyGenerator.cs

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


 スクリプトを修正したらセーブします。


5.EnemyController スクリプトを修正する


 設計に基づいて、エネミーの破壊処理の前に、GameData スクリプト側に用意したExp の値を加算するメソッドと、
EnemyGenerator スクリプト側に用意したメソッドを呼び出す処理を順番に追加します。処理の順番は、Exp を加算してから Exp の表示更新になります。

 これで、EnemyController スクリプトから始まった Exp の加算処理と、Exp の表示更新の処理の一連の流れが完成します。


EnemyController.cs


 スクリプトを修正したらセーブします。


6.ゲームを実行して動作を確認する


 すべての実装が完成しましたので、ゲームを実行してエネミーを破壊してください。
エネミーごとに設定されている Exp の分だけ GameData スクリプトによういした totalExp 変数が加算されて、
その値を参照してゲーム画面の Exp の表示がその都度更新されれば制御成功です。


<実行画像 エネミーを倒すと Exp を獲得し、合計値を画面に表示更新する>
動画ファイルへのリンク


 記述した処理の量は多くありませんが、その処理をどのスクリプトの、どの部分に追加していくか、難しいと思います。
これを考えていくことが非常に重要な設計スキルになります。


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

 次を 発展7 −GameData の活用− です。