Unityに関連する記事です

 今回の設計では、クラスの継承多態性という、オブジェクト指向プログラミングにおける重要な概念(原則)を利用した設計の学習を行います。
アイテムという種類のゲームオブジェクトであれば、すべて共通する処理によって動くように設計し、実装を行います。

 このような場合、プレイヤーがアイテムを取得する、取得したアイテムが消える、という部分については
いずれのアイテムのゲームオブジェクトのクラスでも同様の処理を記述することが想定されますので、
こういった共通する処理をまとめて1つのクラスに記述し、そのクラスを継承する形で処理を共通化して実装するようにしていきます。



手順


 以下の手順に沿って実装を進めていきます。

 1.アイテム用クラスの親クラスを設計して実装する
 2.親クラスを継承した回復アイテム用の子クラスを設計して実装する
 3.プレイヤーが回復アイテムを取得できるようにし、HPが回復する処理の修正を行う
 4.アイテムの生成方法を修正する

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

<学習内容>
 ・クラスの継承
 ・protected(プロテクテッド) キーワード
 ・virtual(バーチャル) キーワード
 ・override(オーバーライド) キーワード
 ・base(ベース) キーワード
 ・クラスの継承時に利用できる、仮想(抽象)メソッドのオーバーライド処理 ー多態性(ポリモーフィズム)ー
 ・親クラスを利用したゲームオブジェクトの生成


1.アイテム用クラスの親クラスを設計して実装する


 親となるクラスはMonobehaviourを継承したクラスになります。これがすべてのアイテムの親となるクラスになります。
親クラスは通常通り、新しいC#スクリプトを作成していく手順で作っていきます。

 子クラスでのみ実装が必要な情報と、親クラスに実装して子クラスで共通化して利用したい情報とがありますので
まずはその切り分けを行って、共通化できるもののみを親クラスに用意します

 ・アイテムを取得する => 重複して取得することを防ぐ
 ・取得したアイテムが破壊される
 ・アイテムの効果を適用する
 ・エフェクトを生成する

 この辺りは、共通化できる処理になります。

 ですが、

 ・アイテムの効果を適用する
 ・エフェクトを生成する

 この2つについては、それぞれのアイテムによって内容が異なるため、
親クラスに共通化して処理を作った上で、子クラスでそれぞれのアイテムの内容に沿う内容に修正する必要があります

 クラスの継承では、この機能を実装できます。
なお、エフェクトについては、次以降の手順で取り扱います。



 親クラスに記述する内容は、通常のクラスと同じように変数の宣言とメソッドの作成になりますが、記述する書式がいくつか変わります。

 変数やメソッドの宣言において private 修飾子を利用する部分には、代わりに protected 修飾子を使用します。
これは継承したクラス間でのみ使用できることを許可する修飾子です。
外部で利用したい変数やメソッドの場合には、通常通り public 修飾子を使用します。

 またメソッドの場合には、宣言時に virtual キーワードを記述します。こうすることで子クラスが上書き可能な、親クラスのメソッドとして成立します。

 親クラスで設定されたアクセス修飾子の情報は子クラスでも引き継がれます
例えば親クラスで public float x を作成していれば、それは子クラスでも public float x として扱われます
メソッドも同様です。


ItemBase.cs

<= +ボタンを押すと開きます。親クラスとして共有化する情報はプロジェクトにより異なります。自分のプロジェクトに合ったものにアレンジしましょう。



<protected(プロテクテッド) キーワード>


 protected キーワードはアクセス修飾子の1つです。private や public の仲間です。

 protected キーワードを宣言した変数やメソッドは、クラス内部、あるいは派生(子)クラスからのみアクセスすることが出来ます。

 そのため、親クラスとなるクラスにおいて、親子間で利用したい変数やメソッドには protected 修飾子を宣言します
これは親クラスにおいて private 修飾子で宣言している場合、親クラス内部では利用できますが、派生クラスでは利用できなくなってしまうためです。


<親クラスにある protected 修飾子を持つ変数やメソッド>

  // 変数での利用
    protected int itemNo;


  // メソッドでの利用
  protected virtual void EndItem(float duration = 1.5f) {

        Destroy(gameObject, duration);
    }



<参考サイト>
MicroSoft
protected (C# リファレンス)
https://docs.microsoft.com/ja-jp/dotnet/csharp/lan...
MicroSoft
アクセス修飾子
https://docs.microsoft.com/ja-jp/dotnet/csharp/pro...


<virtual(バーチャル) キーワード>


 親クラスにおいて定義したメソッドは、virtual (仮想) キーワードを一緒に宣言することで、派生(子)クラスにおいてオーバーライド処理をして利用することが許可されます。
この機能を有しているメソッドを仮想メソッドといいます。仮想メソッドにより、メソッドには多態性が生まれます。


<親クラスにある仮想メソッド>
    public virtual void TriggerItem() {
        // 各アイテムの効果を記述する

    }


 virtual キーワードのないメソッド(通常のメソッド)は仮想メソッドではないため、処理をオーバーライドすることは出来ません


<各キーワードの関係性>
 virtual => override できる

  abstract => override できる

MicroSoft
virtual (C# リファレンス)
https://docs.microsoft.com/ja-jp/dotnet/csharp/lan...


2.親クラスを継承した回復アイテム用の子クラスを設計して実装する


 前回作成した Item_RecoveryLife クラスに、MonoBehaviour の代わりに ItemBase を継承させます
これによって親クラス ItemBase、子クラス Item_RecoveryLife という関連性が生まれます。

 なお、ゲームオブジェクトにアタッチするのは子クラス(ここでは Item_RecoveryLife)のみで大丈夫です。
継承元であるクラスを別途アタッチする必要はありません

 Unity において利用する機会の多い MonoBehaviour クラスを継承しているスクリプト(新しくスクリプトを作成すると自動的に継承しているクラス)も、
MonoBehaviour というクラス自体はアタッチしていませんが、MonoBehaviour クラスの持つ Start メソッドなどが有効に動くのは、このクラスの継承によるものです。
 


 クラスを継承した場合、継承している親クラス(今回であれば ItemBase クラス)の継承しているクラスも引き続きます
そのため、これから修正する Item_RecoveryLife クラスは MonoBehaivour クラスと ItemBase クラスの2つのクラスを継承しているクラスになります。

 MonoBehaivour クラスが継承されていますので、Start メソッドや OnTrigger 〜 メソッドも利用できますし、
ゲームオブジェクトにアタッチして利用することも出来ます。

 以下は Item_RecoveryLife を修正したスクリプトになります。


Item_RecoveryLife.cs

<= +ボタンを押すと開きますので、自分なりの実装を行った上で確認をしてみましょう。


 子クラスの処理は以上です。他の必要な処理はすべて親クラスである ItemBase 側に記載されていますので、そちらで処理を行ってくれます。
そのため子クラスには、親クラスに足りない情報や、親クラスのメソッド内容に対して修正したい処理だけを記述することで、親クラスの処理も含めて実行されます。

 子クラスにおいて親クラスのメソッドに変更を加えた場合、親クラスの処理は無視されます。これをメソッドのオーバーライドといいます。
オーバーライドできるメソッドは、親クラスで virtual キーワードを付けて宣言しているメソッド(仮想メソッド)のみになります。

 メソッドのオーバーライドを行うと、親クラスに記述されているメソッド内の処理はすべて上書きされて無視されます
そのため、親クラスのメソッド内の処理に加えて、子クラスの処理を追加したい場合には、オーバーライドしたメソッド内に base.オーバーライドしたメソッド名(); を記述します。
詳しい使い方については長くなってしまうため別のページで行いますが、まずはご自分で継承について調べてみましょう。

 以上のことより、オーバーライドを行わないメソッドは、子クラスで親クラスの処理を上書きする必要がないメソッドになりますので、子クラスでの記述は不要になります
Item_RecoveryLife クラスには親クラスにある EndItem メソッドの記述がありませんので、この EndItem メソッドは、Item_RecoveryLife クラスでは、親クラスの処理をそのまま利用することになります。


 スクリプトを修正したら、プレファブになっている回復用ゲームオブジェクト Item_RecoveryLife のプレファブを見て、修正した Item_RecoveryLife クラスを確認します。
インスペクター上には継承している親の情報も表示されます。忘れずに設定を行いましょう。




<override(オーバーライド) キーワード>


 abstract キーワードによる抽象実装や、virtual キーワードによる仮想実装に対して実行できる機能です。
上記の2つのキーワードを持つメソッドに対して、上記部分を override に変更することで、処理を拡張したり、書き換えたりすることが出来ます。

 このとき、メソッドのアクセス修飾子を変更することは出来ません
例えば、protected virtual メソッドであれば、protected override メソッドのように、あくまでも同じアクセス修飾子での実装になります

<親クラスにある仮想メソッド>
    public virtual void TriggerItem() {

        // 各アイテムの効果を記述する

    }

 ↓ 

<派生(子)クラスでのオーバーライド実装>
    public override void TriggerItem(CharaController player) {

        // 親クラスの TriggerItem メソッドに記述されている処理をすべて実行する
        // 今回の実装であれば、コライダーをオフにする処理を実行する
        base.TriggerItem(charaController);

        // HP 回復
        RecoverLife(player);

    // TODO 回復エフェクト生成と破棄


        // 親クラスの EndItemメソッドに記述されている処理をすべて実行する
        // 今回の実装であれば、アイテムの破棄を実行する
        base.EndItem();
    } 

 オーバーライドという単語の持つ意味の通りで、この処理を行った親クラス側に記述されていた仮想メソッド内の処理は上書きます
もしも親クラスに実装されている仮想メソッド内の処理も利用した上でオーバーライドしたい場合、base キーワードを利用します。こちらは次に説明します。

 親クラスにある virtual キーワード、あるいは abstract キーワードと override キーワードとは、1対1でつながっているイメージです。


<参考サイト>
MicroSoft
override (C# リファレンス)
https://docs.microsoft.com/ja-jp/dotnet/csharp/lan...


<base(ベース) キーワード>


 override キーワードを利用して仮想メソッドを変更する場合、親クラスでの実装内容を利用して拡張したい場合と、まるごと書き換えたい場合とがあります。
メソッドのオーバーライド処理は基本的に上書きですので、丸ごと書き換わります。

 親クラスにある仮想メソッドの処理を残した上で、拡張する形でメソッドのオーバーライドを行いたい場合には、base キーワードを使って実装します

 記述する場合には、base. に続けて親クラスにある仮想メソッドのメソッド名と引数を宣言します。
処理を書く順番にも注意してください。プログラムは上から順番に実行されますので、拡張した処理に対してどのタイミングで親クラスの処理を実行するかを考える必要があります。

    public override void TriggerItem(CharaController player) {

        // 親クラスの TriggerItem メソッドに記述されている処理をすべて実行する
        // 今回の実装であれば、コライダーをオフにする処理を実行する
        base.TriggerItem(charaController);

        // HP 回復
        RecoverLife(player);

    // TODO 回復エフェクト生成と破棄


        // 親クラスの EndItemメソッドに記述されている処理をすべて実行する
        // 今回の実装であれば、アイテムの破棄を実行する
        base.EndItem();
    } 

 上記の実装の場合、親クラスの処理に加えて、子クラスの処理が実装されますので、実際には次のような内容になります。

<base キーワードを利用した場合の処理>
    public override void TriggerItem(CharaController player) {


////*  base.TriggerItem で実装される親クラスの処理  *////


        // 親クラスの TriggerItem メソッドに記述されている処理をすべて実行する
        // 今回の実装であれば、コライダーをオフにする処理を実行する
        base.TriggerItem(charaController);

      => これはつまり、

        // 重複獲得を防止
        col.enabled = false;


////*  この処理が行われている  *////


        // HP 回復
        RecoverLife(player);

    // TODO 回復エフェクト生成と破棄



////*  base.EndItem で実装される親クラスの処理  *////


        // 親クラスの EndItem メソッドに記述されている処理をすべて実行する
        // 今回の実装であれば、アイテムの破棄を実行する
        base.EndItem();

      => これはつまり、

        Destroy(gameObject, duration);


////*  この処理が行われている  *////


    } 

 base を記述すれば、その処理の内容としては親クラスの処理がそのまま適用されますので、非常に便利です。
特に親クラスでの仮想メソッド内の処理が多いほど、1行の base キーワードによって実装される処理も多くなりますので、恩恵は大きくなります。

 逆に考えると、親クラスにおけるメソッドの挙動をしっかりと理解していないと、クラスの継承を利用したプログラムを組み込めない、ということになります。



 オーバーライドしたメソッドでは、base キーワードがない限り、親クラスでの処理の内容は反映されない(上書きされる)ことになりますので、
例えば、以下のように base キーワードがない場合には、子クラスでオーバーライドされた処理のみが実装されます。


<base キーワードがない場合の処理>
    public override void TriggerItem(CharaController player) {


////*  base.TriggerItem で実装される親クラスの処理がないので  *////


    // コライダーをオフにする処理


////*  この処理が行われない  *////


        // HP 回復
        RecoverLife(player);

    // TODO 回復エフェクト生成と破棄



////*  base.EndItem で実装される親クラスの処理がないと  *////


   // アイテムを破壊する処理
 

////*  この処理が行われない  *////


    } 


<参考サイト>
MicroSoft
base (C# リファレンス)
https://docs.microsoft.com/ja-jp/dotnet/csharp/lan...
++C++ // 未確認飛行 C 様
継承
https://ufcpp.net/study/csharp/oo_inherit.html


3.プレイヤーが回復アイテムを取得できるようにし、HPが回復する処理の修正を行う


 プレイヤー側の OnTriggerEnter を修正することで、回復アイテムを取得できるようにしていきます。

 タグによる評価を削除し、代わりに if 文と TryGetComponent メソッドを利用します。
この分岐処理により、侵入したコライダーを持つゲームオブジェクトにアタッチされているクラスを評価して、
アイテムのゲームオブジェクトか、そうではないゲームオブジェクトなのかを判別しています。

 下記に CharaController(PlayerControler) を補記したスクリプトを提示します。修正している部分のみ記載しています。


CharaController.cs

<= +ボタンを押すと開きますので、自分なりの実装を行った上で確認をしてみましょう。


 さて、いままでの処理とは、どの部分が異なっており、どこがポイントかわかりますでしょうか。

 タグの場合には、タグによる分岐後、さらにアイテムの名前を特定してあげないと、どのアイテムを取得したのかを判別できず、処理を実行できませんでした。

 今回の実装の場合、TryGetComponent メソッドでは、ItemBase クラスがアタッチされているゲームオブジェクトかどうか、を評価しています。
ItemBase クラスとは、各アイテムにアタッチされているクラスの親クラスです。

 つまり、子クラスで個別に判定を行うのではなく、親クラスである ItemBase が継承されているかどうか、を判定する評価式になっています。
これは、各アイテムが ItemBase クラスを継承することが前提で実装可能な設計になります。

 分岐の処理はこれだけです。個別に分ける必要なありません。

 あとは、ItemBase クラスの取得が出来た場合には、同時にアイテムであると判断されるので、TriggerItem メソッドを実行しています。
このメソッドも親クラスである ItemBase クラスに用意されているメソッドです。つまり、子クラスにも必ず存在するメソッドとなりますので、
アイテムの取得処理は、この処理だけで、あとは、各子クラスでオーバーライドした TriggerItem メソッドが実行されることになります。


<クラスの継承時に利用できる、仮想(抽象)メソッドのオーバーライド処理 ー多態性(ポリモーフィズム)ー>


 多態性(ポリモーフィズム)とは、 同じメソッドの呼び出し命令に対して、異なるオブジェクトが異なる動作をする(振る舞いを変える)ことを言います
オブジェクト指向プログラミングのプログラミングにおける、重要な考え方・概念になります。
++C++; // 未確認飛行 C 様
C# によるプログラミング入門 多態性
https://ufcpp.net/study/csharp/oo_polymorphism.htm...



 今回の実装のケースであれば、ItemBase クラスに用意されている TriggerItem メソッドを実行する際に、
回復アイテムであれば回復アイテムとしての処理を行い、速度アップのアイテムであれば速度アップとしての処理を行うように、
同じメソッドであるにもかかわらず実行される処理が異なるー振る舞いを変えるーことを指しています。

 仮にクラスの継承がなく、メソッドのオーバーライド処理もない場合と、今回実装している処理とを比較してみます。
ゲームオブジェクトを判定するにあたり、タグを利用したケースで考えてみます。


<タグを利用して複数のゲームオブジェクトを判定する場合>

  if(hit.collider.gameObject.tag == "Enemy_A" || hit.collider.gameObject.tag == "Enemy_B"){

     // 敵のクラスを取得
     EnemyController enemy = hit.collider.gameObject.GetComponent<EnemyController>();

     // 敵のクラスにあるメソッドを実行
     enemy.Damage();

  } 
  else if(hit.collider.gameObject.tag == "Enemy_C"){

     // 敵のクラスを取得
     EnemyController_Boss boss = hit.collider.gameObject.GetComponent<EnemyController_Boss>();

     // 敵のクラスにあるメソッドを実行
     boss.BossDamage();
  }
  else if(hit.collider.gameObject.tag == "PowerUpItem"){

     // パワーアップアイテムのクラスを取得
     PowerUpItem powerUpItem = hit.collider.gameObject.GetComponent<PowerUpItem>();

     // 敵のクラスにあるメソッドを実行
     powerUpItem.PowerUp();
  }

 このように各ゲームオブジェクトが異なるクラスによって管理されている場合には、各ゲームオブジェクトごとに分岐を作っています。
そして、1つの新しい情報が増えるたびに分岐やタグを増やしていく必要があります
各ゲームオブジェクトにアタッチされているクラスの情報も異なるため、GetComponent メソッドで取得する型の指定や、その後のメソッドも書き替える必要があります。
数が少ないうちはいいですが、処理は煩雑になりがちですし、管理も修正も大変です。


 共通のクラスを継承している場合には、分岐は必要ありません
同じクラスを取得し、同じメソッドを実行すれば、親クラス、あるいは子クラス内でオーバーライドされている処理が振る舞いを変えて、自動的に実行されるためです。

     // ゲームオブジェクトにアタッチされている親クラスを取得できるか判定
        if (other.TryGetComponent(out ItemBase item)) {

      // 取得した親クラスにある抽象(仮想)メソッドを実行する => 子クラスで実装しているメソッドの振る舞いになる
            item.TriggerItem(this);
	}

 非常に簡潔に処理を記述できる上に、ゲームオブジェクトの種類が増えてクラスが増えても、そのクラスが ItemBase クラスを継承していればここに処理を書き足す必要もありません
これがクラスの継承とメソッドのオーバーライド処理によって振る舞いを変えることで実装できる大きな利点です。

 この記述により、今後アイテムの種類が増えても、CharaCotroller 側のアイテムの取得処理には追加の記述は不要になりました

 クラスの継承を理解するのは難しいですが、それだけの機能や恩恵が受けられることを考えると、是非習得していただきたい技術になります。
また、こういった処理を理解していくことでオブジェクト指向プログラミングにも慣れていくことが出来ます。

 例えば、同じクラスの継承による機能を敵側にも用意することで、異なる敵からの攻撃判定を、別々の攻撃処理として振る舞いを変えることで実装が可能です。

 繰り返し処理を考えて記述し、自分でもクラスを継承したクラスを自作してみてください。


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


 プレファブになっている回復アイテムを1つヒエラルキーにドラッグアンドドロップしてください。

 ゲームを実行して、プレイヤーを回復アイテムに侵入させて、いままでと同じように回復の処理が実行されるかを確認してみます。

 問題がなければ、敵が生成する回復アイテムの処理も修正を行います。


4.アイテムの生成方法を修正する


 EnemyController スクリプトにある、アイテムの生成処理について修正を加えていきます。

 まず、いままで回復アイテム用プレファブを登録していた宣言の型を ItemBase の型に変更します。
親クラスを型として宣言しておくことにより、ItemBase を継承しているアイテムであれば、どのアイテムであっても登録が可能になります
いままでのように、Item_RecoveryLife 型で宣言している場合には、この型と同じアイテムしか登録できませんので、
この部分でもクラスの継承によって、拡張性と汎用性の高い処理が実装可能になっています。

 ここに ItemBase を親クラスとして持つ各アイテムのプレファブをインスペクター上で登録しておきます。
今までと同じように回復アイテムのプレファブをドラッグアンドドロップしてアサインしてください。

 回復アイテムにアタッチされてるスクリプト自体が Item_RecoveryLife クラスであっても、
Item_RecoveryLife クラスの親クラスが ItemBase クラスなので、問題なくアサインできます。

 また、アイテムを生成する処理の部分も修正します。

 
EnemyController.cs

<= クリックすると開きます。宣言フィールドと修正したメソッドのみを記載してあります。


 生成する際にも ItemBase クラスを指定しているので、親クラスに対して生成を行う処理になりますが
実勢に生成されるのは、ItemBase クラスを継承している ItemRecoveryLife アイテムになります。

 よって、プレファブとしてアサインするゲームオブジェクトが回復アイテム以外のアイテムになった場合でも、
この生成処理も各アイテムごとに分けて作る必要はなく、ItemBase クラスを継承していれば、この処理だけで、すべてのアイテムを生成することが出来ます


<親クラスを利用したゲームオブジェクトの生成>


 まずアイテム生成時の手順が少し変わりました。以前は回復アイテム用のプレファブ(Item_RecoveryLife クラス)を指定して、そのまま回復アイテムを生成していました。
ですがこの処理ですと、他のアイテムを生成するためには、アイテムごとに生成処理を変えなくてはならず、同じような生成処理であるのに冗長的な処理を増やしてしまいます

 そこで今回は ItemBase クラスを指定して、アイテムを生成しています。
生成されるゲームオブジェクトは ItemBase クラス型がアタッチされているか、あるいは ItemBase クラスが継承されているスクリプトがアタッチされたプレファブのゲームオブジェクトになっています。

 そのため、ItemBase を指定して生成を行うのですが、実際には、その ItemBase を継承している子クラスが生成されるゲームオブジェクトとなるため
異なるゲームオブジェクトを生成する処理にもかかわわず、同じ処理を書く必要がないうえ、生成されるアイテムを自動的に変えることが出来ます

 この部分にも、プログラムの持つ多態性・振る舞いを変えるという概念が活きています。

コメントをかく


「http://」を含む投稿は禁止されています。

利用規約をご確認のうえご記入下さい

Menu



技術/知識(実装例)

2Dおはじきゲーム(発展編)

2D強制横スクロールアクション(発展編)

3Dダイビングアクション(発展編)

2Dタップシューティング(拡張編)

レースゲーム(抜粋)

2D放置ゲーム(発展編)

3Dレールガンシューティング(応用編)

3D脱出ゲーム(抜粋)

2Dリアルタイムストラテジー

2Dトップビューアドベンチャー(宴アセット使用)

3Dタップアクション(NavMeshAgent 使用)

2Dトップビューアクション(カエルの為に〜、ボコスカウォーズ風)

VideoPlayer イベント連動の実装例

VideoPlayer リスト内からムービー再生の実装例(発展)

AR 画像付きオブジェクト生成の実装例

AR リスト内から生成の実装例(発展)

private



このサイト内の作品はユニティちゃんライセンス条項の元に提供されています。

管理人/副管理人のみ編集できます