i-school - 3D宝石集めアクションゲーム 手順13
 プレイヤーが宝石のゲームオブジェクトに侵入した際に、宝石を獲得したことによるスコア加算機能を追加します。
この手順ではあくまでも内部的(プログラムとして)、スコアの加算を行います。ゲーム画面に表示されるための処理は次以降の手順で作成します。


<実装動画>
動画ファイルへのリンク



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

手順13 ースコアの加算処理ー

<新しく学習する内容>
 ・TryGetComponent()メソッドとoutキーワード宣言
 ・メンバ変数に用意していない型への処理を実行していく方法



1.設計


 Gem スクリプトの OnTriggerEnter メソッド内の処理が無事に動作しましたので、このメソッド内に処理を追加していきます。

 設計としては、次のようなロジックを考えています。

 ・新しく ScoreManager スクリプトを作成する。これはプレイヤーのゲームオブジェクトにアタッチして利用する
   => 役割は、宝石を獲得した際の合計得点を管理するため

 そのためには、以下のような内容で ScoreManager スクリプトを記述します。

 ・合計得点の管理用の int 型の totalPoint 変数を用意する
 ・得点の加算用の AddScore メソッドを public 修飾子で用意する
   => AddScore メソッドには int 型の引数を用意し、受け取った値を totalPoint 変数に対して加算処理を行う仕組み
     引数を int 型にしているのは totalPoint 変数と同じ型にすることで容易に加算処理できるようにするため

 ここまでが、新しく作成する ScoreManager スクリプトの処理になります。



 続いて、宝石用の Gem スクリプトについて、処理を修正します。

 ・得点の情報として int 型の point 変数を追加する。SerializeField 属性のついた private 修飾子で宣言することでインスペクターから設定ができるようにしておく

 ・OnTriggerEnter メソッド内の処理を修正して、タグではなく、プレイヤーにアタッチされている ScoreManager の情報を取得して判定する条件式に変更する
   => 取得した ScoreManager の情報を使って、ScoreManager に用意した AddScore メソッドを実行し、得点の加算処理を行う
     この加算処理を ScoreManager の外部のクラスから実行するために、AddScore メソッドは public 修飾子にしてある

 ・AddScore メソッドを実行する際には int 型の引数情報が必要になるので、 Gem スクリプトに用意した int 型の point 変数の値を引数として渡す
   => 引数を利用することで、Gem スクリプトの point 変数の値の情報を ScoreManager 側に届くようにできる

 こちらが Gem スクリプトの内容になります。



 双方の処理を作ることにより

 ・ScoreManager では Gem スクリプト側から AddScore メソッドが実行され、引数として point 変数の値が届いている
   totalPoint 変数に、Gem スクリプトから渡ってきた point 変数の値を加算していく

 このようなロジックを持つ処理の流れになります。
 一連の処理のイメージが沸きますでしょうか。

 この手順を参考にしながらコメントで書いてみて、ロジック化してプログラムを書いてみてください。


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


 int 型の totalPoint 変数を宣言します。private 修飾子で問題ありません。この変数を得点の管理用に利用します。

 新しく AddScore メソッドを作成し、引数を int 型で用意しておきます。こうすることで、このメソッドを実行する際には int 型の値を渡すことが必要になります。
AddScore メソッド内では引数で受け取った値を totalPoint 変数に加算していく処理を実装します。

 totalPoint 変数は private 修飾子ですので、処理が実行されても加算された値がわかりません。
そのため、加算後に Debug.Log メソッドを用意して、Console ビューに totalPoint 変数の値を表示するようにします。
これで、処理が実行されている確認と、現在の totalPoint 変数の値の、両方の確認が行えるようになります。

 Debug.Log メソッドを有効に活用しましょう。
あるいは、private 修飾子の totalPoint 変数に SerializeField 属性を付与して、インスペクターから確認が出来るようにしてもよいです。



ScoreManager.cs

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


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


3.プレイヤーのゲームオブジェクトに ScoreManager スクリプトをアタッチする


 ヒエラルキーにあるプレイヤーのゲームオブジェクトを選択し、ScoreManager スクリプトをドラッグアンドドロップしてアタッチします。

 プレイヤーのゲームオブジェクトのインスペクターを確認し、ScoreManager スクリプトがアタッチされていることを確認します。
新しく宣言した変数は private 修飾子であるので表示されません。


プレイヤーのゲームオブジェクト インスペクター画像




 ただし、private 修飾子の変数に SerializeField 属性を付与している場合に限り、各変数がインスペクターに表示されます。


プレイヤーのゲームオブジェクト インスペクター画像(SerializeField 属性を付与した場合)



 いずれかの状態になっていることを確認してください。

 インスペクターに表示されている場合には、この値は触らないように 0 のままにしておいてください。


4.Gem スクリプトを修正する(タグによる判定をやめる)


 private 修飾子の int 型の point 変数を宣言し、この値を「宝石の持つ得点」という情報として役割を持たせます
そのため、その役割に沿った使い方をスクリプト内で記述していく必要があります。

 private 修飾子のままですとスクリプト内でしか値の編集が出来ないため、値を変えてデバッグしたい場合には不便です。
こういったケースでは SerializeField 属性を付けて変数を宣言しておくことで、インスペクターから任意の得点の設定を可能にしておくと便利です。

 int 型を利用しているのは、得点を管理している ScoreManager スクリプトにある totalPoint 変数の型が int 型であるためです。
同じ型同士であれば簡単に加算処理が作れます。

 新しい情報を用意する場合には、その変数がどの部分で利用されるのかを考えて、必要な形で設計をします



 重要な変更点として、OnTriggerEnter メソッド内の処理を修正し、ゲームオブジェクトがコライダーに侵入したら、そのゲームオブジェクトのタグによって判定を行う処理から、
侵入したゲームオブジェクトにアタッチされている指定されたスクリプト(ここでは ScoreManager)を取得できるかを判定する処理に変更します。

 得点の情報を管理しているのは、プレイヤーのゲームオブジェクトではなく、そこにアタッチされている ScoreManager が管理を行っています。
そのため、例えばタグによってゲームオブジェクトの判別が出来たとしても、その後に再度、そのタグによって判別されたゲームオブジェクトに対して、
指定されたスクリプト(ScoreManager)を取得することができるかを判定する必要があります。

 つまり、タグによる処理の場合、そのあとの処理につなげていく際に、再度、スクリプトの有無の判定の処理を行うことになるため、
同じゲームオブジェクトに対して重複判定をすることになります。タグの判定後に、さらに GetComponent メソッドの処理が必要になるためです。

 それは無駄な処理になってしまうため、今回はタグの処理を止め、代わりに、スクリプトを取得できるかを判定し、
スクリプトを取得できた場合にのみ、次の処理へとつながるようなロジックを作って処理を組み上げています。
これには新しい TryGetComponent メソッドを活用しています。

 この新しい処理に変更することにより、スクリプトのアタッチの有無の確認をしながらゲームオブジェクトの判定を行い、
かつ、必要なスクリプトの取得の処理を1回の処理で行うことができるため、処理の正確性を高め効率化も測れます。

 結果として、現在用意しているタグによる判定処理の代わりにもなりますので、タグの判定部分を削除することが出来ます。
 


 ScoreManager が取得出来た場合には変数に代入して、ScoreManager スクリプトを参照したり、命令を出せるようにします。
そして、ScoreManager スクリプト内の public 修飾子で宣言している AddScore メソッドを実行(呼び出)します。

 AddScore メソッドを実行するには int 型の引数を用意する必要がありますので、今回宣言を追加した int 型の point 変数を渡します。
この処理によって、スコアの加算処理とともに、どの値を加算するのか、という情報も一緒に ScoreManager 側に届くようになります。


Gem.cs

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


 スクリプトの修正が終了したら、セーブします。

 つづけて、宝石のゲームオブジェクトのインスペクターを確認します。
新しく SerializeField 属性をつけた private 修飾子で宣言した point 変数が表示されていれば問題ありません。


Gem ゲームオブジェクト インスペクター画像(初期値時)




 こちらの point 変数の初期値は 0 ですので、任意の値を設定してください。
この値がプレイヤーが宝石に侵入した際に獲得できる得点になります。


Gem ゲームオブジェクト インスペクター画像(値設定時)



5.<TryGetComponent()メソッドとoutキーワード宣言>


 Unity2019.2以降に追加されたメソッドです。処理結果として bool 型で戻り値を返してくれます
このときの処理結果というのは、指定したコンポーネントの型の取得を行い、それが取得できれば true、取得できなければ false が戻ります

 またoutキーワードによる宣言がありますので、trueの場合には必ず、このoutの後に宣言した変数内に型が代入されます。
 outキーワード宣言を行うと、outを付けた引数で指定した変数はメソッド内で必ず結果が入ることが保証されるものです

 この処理を活用することにより、タグを利用した if 文による評価処理の代替処理が作成可能になります。

  // col.gameObject(つまり、プレイヤーのゲームオブジェクト)に対して、TryGetComponent メソッドを実行し、ScoreManager クラスの情報を取得できるか判定する
 if (col.gameObject.TryGetComponent(out ScoreManager scoreManager)) {

      // この中の処理は、ScoreManager クラスが取得できた場合のみ実行される。
      // その場合、scoreManager 変数を利用することで ScoreManager クラスを参照できる
  }

 今回はこのような処理として利用しています。
Collider 情報から gameObject(つまりプレイヤーです)へとアクセスし、その gameObject に対して TryGetComponent メソッドを実行しています

 out キーワードの後には ScoreManager 型と scoreManager という変数を用意しておきます。

 もしもこの TryGetComponent の結果が true であるならば、out として用意した scoreManager 変数に
ScoreManager スクリプトの情報が代入された上で、if文内の処理に入ります
また if 文内の間はこの scoreManager 変数が使用できることになります。(scoreManager 変数のスコープが if 文ブロック内であるためです)

 TryGetComponent メソッドの結果が false の場合には ScoreManager スクリプトの取得ができなかったため false が結果として戻り、
この if 文は処理されないままで終了します

 なおTryGetComponentメソッドには複数の書式があります。こちらは下記のリファレンスを参照してください。


参考サイト
Unity公式スクリプトリファレンス
Component.TryGetComponent
Zenn fuqunaga 様
TryGetComponent() と比べたら GetComponent() を使う理由がなくなった件


6.<メンバ変数に用意していない型への処理を実行していく方法>


 スクリプトでは、外部のスクリプトの情報を取得し、変数に代入することで、そのスクリプトへの参照や、メソッドを実行することが出来るようになります。
外部のスクリプトの情報は、メンバ変数で用意しておく以外にも、スクリプト内で動的(ゲームが始まってから、特定のタイミングで)取得し、利用していくことも可能になっています。

 外部のスクリプトの情報を取得した場合、そのスクリプトの扱っている public 修飾子の情報が扱えるので、
public 修飾子の変数の情報を参照して利用したり、public 修飾子のメソッドを呼び出すことが可能になります


 if (col.gameObject.TryGetComponent(out ScoreManager scoreManager)) {

   // ScoreManager クラスが取得出来ている場合、scoreManager 変数を通じて ScoreManager クラスに記述されている public 修飾子の AddScore メソッドを呼び出す命令をする
    // 引数には point 変数の値を渡す
    scoreManager.AddScore(point);
  }

 if 文において col.gameObeject 変数(侵入してきたコライダーのアタッチしてされているゲームオブジェクト)に対して
TryGetComponent メソッドを実行し、引数において指定した型(ScoreManager 型)の取得を行います。

 TryGetComponent メソッドの結果、ScoreManager 型が取得できている場合には true が戻り値として戻り、if 文の評価が成立します。
かつ、out キーワードがありますので、scoreManager 変数に、今回取得した ScoreManager 型の情報が代入されます。
そして、if 文内においてのみ、scoreManager 変数を利用して、ScoreManager に対して命令できるようになります。(scoreManager 変数のスコープは、if 文内のみです)

 このように、GameObject.Find メソッドを利用することなく、命令を出したいゲームオブジェクトの情報を OnTriggerEnter メソッドの引数から取得し、
加えて TryGetComponent メソッドを利用して、命令したい ScoreManager スクリプトの情報を取得するように処理をつなげることで
Gem スクリプトから ScoreManager スクリプトに対して命令を出し、ScoreManager スクリプト内の public 修飾子で定義されている AddScoreメソッドを実行することが出来ます。



 Unity では色々なゲームオブジェクトや、それにアタッチされているコンポーネントに対して命令を出すことで、ゲーム内に動きをつけています。
それを実現する方法が色々と用意されていますので、一からすべての処理を自作するのではなく、Unity の機能を覚えて、それを活用していくことで
処理のロジックを構築することが出来ます。

 プログラムは暗記ではなく、どうしてその処理が動いているのか、という部分をしっかりと理解していく必要があります
その仕組みやロジックが理解できてくることで、自分で処理を作っていく力を養うことが出来ます。


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


 修正が完了しましたので、ゲームを実行して宝石にプレイヤーを侵入させてみます。
Debug.Log メソッドが実行されて、Console ビューに totalPoint 変数の値が更新されて表示されれば、スコアの加算処理の制御成功です。


Console ビュー画像



<実行動画>
動画ファイルへのリンク




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

 => 次は 手順14 ーCanvas内にUI部品を作成する(スコア表示) ー です。