【Unity】ゲームにおける委譲と継承、その違いと使い分け【タワーディフェンス】

6 min読了の目安(約6000字TECH技術記事

はじめに

タワーディフェンスを作ろうの記事第4回目です。
今回は「委譲」をテーマに継承との違いや使い方、そして委譲と継承の両方を組み合わせて敵プレイヤーのAIを作るところまで説明したいと思います。

今までの記事はこちら。

https://zenn.dev/supple/articles/2012a0ebfe13181286af
https://zenn.dev/supple/articles/64c9f9aaf2e7ca6f7151
https://zenn.dev/supple/articles/e4a5a7bb57922c

委譲とは

委譲とは何でしょうか。
プログラミングの勉強していると大体ここら辺から継承やコンポジション、ポリモーフィズムといった難しそうな単語が沢山出てきて混乱してしまう人も多いのではないでしょうか。
委譲と一言で言ってもその意味は多岐に渡ります。

  • コンポジション(広い意味での委譲)
  • オブジェクト指向の手法としての委譲
  • C#のデリゲート

全部委譲で間違いではありません。
委譲の話をする際にはまずどの委譲かを知っておかないといけないので混乱するのも無理のない話です。
ではこれらの委譲を順番に見ていきましょう。

コンポジション(広い意味での委譲)

コンポジションは、ある機能を持ったクラスをメンバ変数として宣言し、それを使用することを指します。
再利用できる機能を分離してクラス化して使ったり、肥大化したクラスを機能ごとに分解してクラス化してそれを利用したりといった使い方です。

コンポジションの利用イメージ

コンポジションは「特定の処理を他の場所に任せる」という意味では広い意味での委譲になります。

オブジェクト指向の手法としての委譲

オブジェクト指向での委譲は、上記のコンポジションに加えて多態性(ポリモーフィズム)の概念が加わります。
ポリモーフィズムとは、委譲先を変えることで違った動きをさせることができる概念になります。
前回の記事「クラスを継承した特殊ユニットの作り方」でユニットとは別の挙動をする拠点ユニットを作りましたが、このUnitとFortUnitで挙動が違うというところがポリモーフィズムになります。

下図が委譲の利用イメージになります。
委譲の利用イメージ
今回の記事はこの委譲がテーマとなります。

C#のデリゲート

C#にはdelegateという機能があります。
これは、中身を差し替えることができるメソッドのようなもので、委譲の機能を有しています。
今回のテーマからは外れるので詳細は説明しませんが気になる方はこちらをどうぞ。

https://docs.microsoft.com/ja-jp/dotnet/csharp/programming-guide/delegates/

今回やること

今回は敵の拠点のAIを作りますが、かなり複雑な内容になります。
最終的にどうなるかの図を示しますので、何をやっているのか分からなくなったらこの図を見返してみてください。

ユニットのAIをコンポジションする

今回は拠点ユニットのAIを委譲で実装しますが、その前にUnitクラスにあるAIの機能を切り離してコンポジションしましょう。

委譲先クラスの作成

まずは委譲先となるユニットのAIクラスを作成します。
名前はUnitAIにしました。

必要なパラメーター

AIが頭脳だとするとUnitは体です。
このAIはどのユニットのAIなのかを知るためのパラメーターが必要です。

UnitAI.cs
public class UnitAI
{
    public Unit ownerUnit;      // この思考の持ち主
}

委譲される部分の作成

今までUnitクラスで行っていた思考部分を代わりに行うメソッドを追加します。
このとき、virtualキーワードを使ってオーバーライド可能にしておきます。
こうすることで継承先のクラスで違う挙動をさせるとが可能になります。

UnitAI.cs
public class UnitAI
{
    // ユニットを更新するメソッド。
    // virtualにすることで派生クラスで違う思考をさせることを可能にする
    public virtual void UpdateUnit(float deltaTime)
    {
        //TODO: Unit.csのUpdateUnitメソッドのAI部分をここに移植する
	//      長くなるので中身は割愛します。
    }
}

委譲する部分の作成

委譲先ができたらUnitクラスをUnitAIクラスを使用したコンポジションに書き換えます。

必要なパラメーター

作成したUnitAIクラスをパラメーターとして追加します。

Unit.cs
    protected UnitAI unitAi = null;     // ユニットのAI

AIの作成

AIの中身を作成するメソッドを作ります。名前はCreateAIにしました。
CreateAIメソッドをStartメソッドで呼び出し、オブジェクト生成後すぐにAIが作成されるようにします。
CreateAIメソッドはvirtualキーワードを使ってオーバーライド可能にしておきます。
こうすることで継承先のクラスで違うAIを設定できるようにすることが可能になります。

Unit.cs
    // AIを作成するメソッド
    // virtualにすることで派生クラスで違うAIを割り当てる事ができるようにする
    protected virtual void CreateAI()
    {
        // AIを生成し、思考の持ち主を自分に設定する
        unitAi = new UnitAI();
        unitAi.ownerUnit = this;
    }
    
    void Start()
    {
        // AIを作成する
        CreateAI();
    }

AIの委譲

AI部分のコードはUnitAIクラスに移されたので削除します。
代わりに、unitAiのメソッドを呼ぶことで処理を委譲します。

Unit.cs
    // ユニットを更新するメソッド
    public virtual void UpdateUnit(float deltaTime)
    {
        // AIの更新をunitAIクラスに委譲する
        unitAi.UpdateUnit(deltaTime);
	
	// TODO:AIはUnitAIクラスに委譲されたので
        //      AI制御部分のコードはUnitAIクラスに移動させます。
	//      長くなるので中身は割愛します。
    }

敵の拠点ユニットを作る

ユニットからAIを切り離す事ができたので敵の拠点ユニットの作成に入ります。

UnitAIを継承してEnemyFortUnitAIを作る

まずはUnitAIを継承した拠点用のAIクラスを作ります。
名前はEnemyFortUnitAIとしました。
中身は後で記述します。

EnemyFortUnitAI.cs
// 敵拠点のAIクラス
public class EnemyFortUnitAI : UnitAI
{
    // TODO:ここにAIの中身を書く
}

FortUnitを継承してEnemyFortUnitを作る

FortUnitを継承した敵の拠点用のユニットクラスを作ります。
名前はEnemyFortUnitとしました。

EnemyFortUnit.cs
// 敵の拠点ユニットクラス
public class EnemyFortUnit : UnitAI
{
    public Unit productionUnit = null; // 生産するユニット
    
    protected override void CreateAI()
    {
        // AIを生成し、思考の持ち主を自分に設定する
        unitAi = new EnemyFortUnitAI();
        unitAi.ownerUnit = this;
    }
}

CreateAIメソッドをオーバーライドし、作成するAIをEnemyFortUnitAIに挿げ替えます。
これで通常のユニットはUnitAIを使い、敵の拠点ユニットはEnemyFortUnitAIを使うようになります。
また、この拠点で生産するユニットのパラメーターも追加しています。

敵の拠点ユニットのAIを作る

AIを使う枠組みはできたので、次はAIの中身を作っていきましょう。

UnitAIのUpdateUnitをオーバーライドする

UpdateUnitメソッドをオーバーライドして拠点としての行動を追記できるようにします。

EnemyFortUnitAI.cs
    public override void UpdateUnit(float deltaTime)
    {
        // 基底クラスのメソッドを呼ぶ
        base.UpdateUnit(deltaTime);
	
	// TODO:ここに拠点としての行動を追記する
    }

資源が貯まったら出撃するAIを記述する

UpdateUnitメソッドをオーバーライドしたら、その中に拠点としての行動を追記します。
とりあえず生産できる資源が溜まったら即出しする単純なAIを作ってみましょう。

ownerUnitの中身がEnemyFortUnitであることを確認する

ここで一つ注意点があります。
EnemyFortUnitAIは敵拠点ユニット用のAIなので、思考の持ち主は敵拠点ユニットである必要があります。
もしこの思考をEnemyFortUnit以外が持っているなら正常に動作しないのでエラーとしてしまいましょう。

public override void UpdateUnit(float deltaTime){
        if (!(ownerUnit is EnemyFortUnit))
        {   // このAIはEnemyFortUnitクラスでしか動かない
            Debug.LogError("EnemyFortUnitAIの思考の持ち主はEnemyFortUnitである必要があります");
            return;
        }

EnemyFortUnitを操作する

思考の持ち主がEnemyFortUnitであることを確認出来たら思考ルーチンを記述します。
EnemyFortUnitクラスにアクセスするためにキャストし、生産に必要な資源が溜まっていたらユニットを出撃するよう命令を出します。

public override void UpdateUnit(float deltaTime){
        // 思考の持ち主はEnemyFortUnitクラスなのでキャストする
        EnemyFortUnit unit = ownerUnit as EnemyFortUnit;

        if (unit.resourcePoint > unit.productionUnit.cost)
        {   // 生産に必要な資源が溜まっていたら
            // 出撃する
            unit.SortieUnit(unit.productionUnit);
        }
    }

委譲と継承の使い分け

今回委譲と継承の両方を使用しています。
今回新しく作られたクラスでUnitAIは委譲、EnemyFortUnitAI、EnemyFortUnitは継承を使用しています。
それはなぜでしょうか。

継承はクラスの性質が変わらない場合にだけ使用する

それぞれのクラスの性質を見てみましょう
Unitクラスの性質は「駒」
FortUnitクラスの性質は「駒」
EnemyFortUnitクラスの性質は「駒」
UnitAIの性質は「思考」
EnemyFortUnitAIクラスの資質は「思考」
となります。
EnemyFortUnitは「駒」というFortUnitと同じ性質を持つから継承を使い、
UnitAIは「思考」というUnitとは違う性質を持つから委譲を使った、という事になります。

最後に

今回の記事を書くにあたり自分でも復習してみたのですが、案外正確じゃない覚え方をしてたりして自分の勉強にもなりました。
ぶっちゃけ分かりやすいかと言われたら微妙な気がしますが一応図を入れたりしてできるだけ分かりやすくしたつもりです。
今回ゲームの内容としてはあまり進んでいませんがご容赦ください。
そろそろ今作ってるものがどう動くのかの動画も出せればなと思っています。