オブジェクト指向プログラミングにおける継承と多態性

Tutorial

·

foundational

·

+10XP

·

20 mins

·

(6)

Unity Technologies

オブジェクト指向プログラミングにおける継承と多態性

本チュートリアルでは、OOP の柱として密接に関連する継承とポリモーフィズム (多態性) について学習します。

  • 親クラスと子クラスの間で機能を共有するために、どのように継承が使用されるかを説明する
  • 親クラスと子クラスの関係を定義し、子クラスが親クラスに対してできること、できないことを含める
  • コードの簡素化のために継承を使用する機会を認識する
  • 親クラスの機能を子クラスで変更するためにポリモーフィズムがどのように使用されるかを説明する
  • ポリモーフィズムをコンパイル時(メソッドオーバーロード)と実行時(メソッドオーバーライド)にどのように適用できるかを説明することができる
  • プロジェクトに応じたハイレベルなシステムアーキテクチャの推奨

Languages available:

1. 概要

オブジェクト指向プログラミングの次の 2 つの柱である継承とポリモーフィズムは、互いに深く関連しています。継承 (Inheritance) は、その名の通り異なるオブジェクト間の親子関係に着目したものです。 ポリモーフィズム (Polymorphism、多態性) は、継承の結果、子クラスが親クラスから受け継いだものを変更することを指します。継承とポリモーフィズムを併用することで、アプリケーションで書かなければならないコードの量を減らすことができるのです。

2. 継承とは?

継承とは、一次クラス(親クラスともいう)を作り、そこから他のクラス(子クラスともいう)を作成することです。子クラスは親クラスの機能をすべて自動的に継承します。アプリケーションでは、異なるクラスが同じような機能を共有することがよくあります。例えば、ビデオゲームに登場する敵のクラスにはさまざまな種類がありますが、自分の体力を管理したり、プレイヤーにダメージを与えたりといったコアな部分は共通していると思われます。継承を使えば、敵のクラスごとにその体力やダメージの機能を書く必要がなくなるので、各クラス独自の機能を書くことに集中できるようになります。

これまで Unity で書いてきたスクリプトでは、すでに継承を利用していますね。デフォルトでは、新しいクラスを作成すると、MonoBehaviour を継承します:

public class SomeClass : MonoBehaviour { }

MonoBehaviour は、Unity のすべてのコアなスクリプト機能を継承する基本クラスです。MonoBehaviour がなければ、OnTriggerEnter や GetComponent を呼び出すことはできませんし、Start や Update を使うことさえできません。

上の図では、継承するクラスを MonoBehaviour から Enemy に変更したため、すべての敵の子クラスが Unity の機能にアクセスできなくなったように見えるかもしれません。幸いなことに、Enemy クラスは Monobehaviour を継承しているので、Enemy クラスの子も MonoBehaviour の子とみなされます!

3. ポリモーフィズムとは?

親クラスからコア機能を継承することは有用ですが、子クラスが親クラスと全く同じ動作をすることを望まない状況も多くあります。ポリモーフィズムは、あるオブジェクトが親クラスから継承する機能を変更することができます。

public class Enemy : MonoBehaviour 
{ 
    public void DealDamage () 
    {
        Player.Health -= 10;
    }
}

上記の例では、Enemy クラスに DealDamage メソッドがあり、それが呼ばれるたびに Player の体力 (health) が 10 ポイント削られます。Enemy の子である Thief クラスは、クラス内で宣言しなくても、このメソッドを呼び出すことができます。

public class Thief : Enemy
{
    private void Update()
    {
        if (Player.isSeen)
        {
            DealDamage(); // method from parent class can be called
        }
    }
}

Thief が Enemy クラスとまったく同じ量のダメージを与えたい場合はこれでよいのですが、異なる値にしたい場合はどうすればよいでしょうか。このような変更は、メソッドのオーバーライドとして知られるプロセスによって実現されます。

親クラスでオーバーライドしたいメソッドは、まずオーバーライド用にマークしておく必要があります。これは、それを仮想メソッドにすることで実現します:

public class Enemy : MonoBehaviour { 

    public virtual void DealDamage () { // virtual keyword allows overriding

        Player.Health -= 10;
    }
}

メソッドを virtual (仮想) と指定することは、そのメソッドがオーバーライドされる可能性はあるが、される必要はないことを示します。というのも、Thief の子クラスは DealDamage メソッドを変更する必要があるかもしれませんが、Scoundrel クラスのような別の子クラスは変更しないかもしれないからです。

DealDamage を virtual に設定すると、Thief クラスは DealDamage のメソッドを独自に作成してオーバーライドすることができるようになります。ここでは virtual の代わりに override という記法を使います。これで、Thief クラス専用のメソッドに新しい機能を追加することができます:

public class Thief : Enemy
{
    public override void DealDamage() // can override virtual methods from parent class
    {
        Player.Health -= 2;
        CommitPettyTheft();
    }
    private void Update()
    {
        if (Player.isSeen)
        {
            DealDamage();
        }
    }
}

Thief クラスは、親である Enemy クラスよりも小さな量のダメージを与えるようになり、また Thief 固有のメソッドの 1 つを呼び出します。これで、Thief オブジェクトから Update で DealDamage が呼び出されると、親メソッドではなく、カスタマイズされた DealDamage メソッドが呼び出されるようになります。

4. 装置タイプの新規作成

プロジェクト概要の中で、構築するアイテムの 1 つに製造装置があることがわかります。この装置は、ユーザーがシーンで選択した任意の装置タイプの生産性を向上させる必要があります。 ユーザーはフォークリフトと同じように、左クリックで装置を選択し、右クリックで移動先の装置を選択することになります。装置の生産性は、製造装置がアクティブに作業している間だけ向上し、装置がリソースから離れたら通常の生産速度に戻るはずです。

ここでは、あなたのプロジェクトでそれがどのように見えるかを示します:

フォークリフトは TransporterUnit スクリプトによって管理され、それ自体は Unit クラスの子です。Unit クラスを見ると、移動に必要な機能はすべて備わっているので、製造装置が Unit の別の子クラスになることは理にかなっています。

1. Prefabs フォルダーで ProductivityUnit プレハブを見つけて、シーンに追加してください。これが、山積みのリソースの生産性を向上させる作業員です。

2. 新しい C# スクリプトを作成し、ProductivityUnit と名付けます。

3. ダブルクリックで Visual Studio で開きます。Start メソッドと Update メソッドを削除してください。

4. ProductivityUnit クラスを Unit から継承させるには、クラス宣言から MonoBehaviour を削除し、Unit に置き換えてください。

public class ProductivityUnit : Unit // replace MonoBehaviour with Unit
{

}

スクリプトに 'ProductivityUnit' は継承された抽象メンバ 'Unit.BuildingInRange()' を実装していないというエラーが表示されます。ご心配なく!今すぐ修正します。

5. Unit. cs を見てみると、次のようなコードがあります:

protected abstract void BuildingInRange();

オーバーライドが任意である仮想メソッドとは異なり、このメソッドはオーバーライドが必要であることを示す抽象記法を使用します。抽象メソッドは、すべての子クラスがある種の機能を必要とするが、その機能は各子クラスで個別にコード化されるべきであることを認識する場合に有用です。この場合、BuildingInRange は、装置が(building クラスの子である)山積みのリソースと相互作用するときに起こるすべてのことを管理することを意味しますが、何が起こるかは、メソッドを呼び出す子クラスに基づいて変更されることになります。

そこで、ProductivityUnit. cs に戻り、そのメソッドをオーバーライドするだけでよいのです:

protected override void BuildingInRange()
{
    
}

6. このエラーを解決した上で、Unit クラスを拡張することで自動的に得られる機能を探ってみましょう。ProductivityUnit スクリプトを保存し、Unity に戻ります。

7. ProductivityUnit スクリプトを Prefab に追加します。このクラスは Unit の子なので、速度を制御するための public float 変数が自動的に与えられていることに注意してください。

8. Play を押します。新しい作業員を左クリックし、山積みのリソースの 1 つを右クリックします。あなたの作業員は自動的に山積みのリソースに移動します。シーン内の他の場所で右クリックすると、そこへも移動します。これらはすべて、ProductivityUnit クラスに一行のコードも追加することなく実現されており、現在の機能はすべて Unit から継承されています。

5. BuildingInRange メソッドのオーバーライド

製造装置の主な機能は、現在割り当てられている山積みのリソースの生産率を上げることです。その機能を構築してみましょう。

1. BuildingInRange メソッドを完成させるには、ユーザーが選択した山積みのリソースを追跡する変数が必要です。クラスの先頭で、m_CurrentPile という名前の ResourcePile 変数を作成し、それを null に設定します。また、リソースの生産性をどれだけ上げるかを定義する float も必要です:

public class ProductivityUnit : Unit
{
    // new variables
    private ResourcePile m_CurrentPile = null;
    public float ProductivityMultiplier = 2;

2. BuildingInRange メソッドに戻り、製造装置が山積みのリソースの範囲内にあるときに何が起こるかをコード化します。このコードは毎フレーム実行されます。製造装置が山積みのリソースである Building の範囲内に入ったフレームで、生産速度が上がるようにしたいと考えています。そして、このコードが次のフレームで実行されないようにしたいのです。そうしないと、生産速度がどんどん上がっていってしまうので。

protected override void BuildingInRange()
{
    // start of new code
    if (m_CurrentPile == null)
    {
        ResourcePile pile = m_Target as ResourcePile;

        if (pile != null)
        {
            m_CurrentPile = pile;
            m_CurrentPile.ProductionSpeed *=  ProductivityMultiplier;
        }
    }
    // end of new code
}

「as ResourcePile」表記は、m_Target が ResourcePile 型の場合のみ、pile 変数を m_Target に設定します。m_Target が Base の場合、これらの型はマッチせず、pile は null に設定されます。これは、m_Target が山積みのリソースであるかどうかをチェックする効率的な方法です。もしそうなら(pile != null)、m_CurrentPile にその山積みのリソースを設定し、その ProductionSpeed を 2 倍にします。

次のフレームでは、メソッドの先頭にある if 文により、m_CurrentPile に値(山積みのリソース)が設定されるため、このコードが再び実行されることはありません。

このコードでもう一つ興味深いのは、親である Unit クラスの「protected」変数である m_Target 変数にアクセスできたことです。保護された変数はプライベート変数のようなものですが、任意の子クラスからもアクセスできます。ProductivityUnit.cs は Unit.cs から派生しているのでアクセスできただけなのです。

3. スクリプトを保存して、Unity に戻ります。

4. Play を押して、自分の作業員を山積みのリソースがある場所へ移動させます。

5. 製造装置が到着する前に山積みのリソースを選択し、到着すると生産速度が 2 倍になることに注目してください。

6. オーバーロードの理解

製造装置を確定させるには、概要に書かれているように、装置が離れた時点で山積みのリソースの生産率が以前の値に戻るようにする必要があります。Unit クラスでは、これを GoTo メソッドで管理しています。ところが、クラスを見ると、この名前を持つメソッドが 1 つではなく、2 つあることに気がつきます:

public virtual void GoTo(Building target)
{
    m_Target = target;

    if (m_Target != null)
    {
        m_Agent.SetDestination(m_Target.transform.position);
        m_Agent.isStopped = false;
    }
}

public virtual void GoTo(Vector3 position)
{
    m_Target = null;
    m_Agent.SetDestination(position);
    m_Agent.isStopped = false;
}

メソッドは名前を共有できない - そうでしょうか?ほとんどの場合 これは真実ですが この特別な状況を除いては メソッドのオーバーロードと呼ばれるものです。両方の GoTo メソッドが異なるパラメーター型を持ち、異なる機能を持つことに注意してください。メソッドのオーバーロードは、一つのメソッドを多目的に使うことができるようにします。ユーザーがターゲットを選択すると、このオーバーロードのペアは、選択されたオブジェクトの種類に応じてナビゲーションを処理します。

最初の GoTo メソッドは、ユーザーが山積みのリソースや床の上で右クリックしたときに収集される Building クラスをパラメーターとして受け取ります。このパラメーターは、Unit スクリプトの SetTarget メソッドに渡されます。

2 つ目の GoTo メソッドは Vector3 パラメーターを受け取ります。これは、ユーザーが山積みのリソースではなく、倉庫内のランダムなポイントを選択する場合に使用します。

これらを別々のメソッドとして書くこともできますが、そうすると複数の呼び出しを覚えておかなければなりません。メソッドのオーバーロードを使えば、1 回だけ呼び出しを覚えればよく、渡されるデータ型によって実行するコードが決まります。

これは、Unity のビルトインメソッドで見たことがある別の機能です。Unity API からメソッドを呼び出し、使用するパラメーターに多くの選択肢がある場合は常に、メソッドのオーバーロードを利用していることになります。例えば、transform.Translate には 4 つのオーバーロードがあり「Create with Code」の最初のユニットで、車を道路に走らせるためにそのうちのいくつかを使用したことがあります:

public void Translate(Vector3 translation); 
// implemented as transform.Translate(Vector3.forward);

public void Translate(float x, float y, float z); 
// implemented as transform.Translate(0, 0, 1);

public void Translate(Vector3 translation, Transform relativeTo);
// implemented as transform.Translate(Vector3.forward, Space.Self);

public void Translate(float x, float y, float z, Transform relativeTo); 
// implemented as transform.Translate(0, 0, 1, Space.Self);

7. GoTo メソッドのオーバーライド

製造装置は、現在山積みのリソースで作業しているかどうかを確認し、作業している場合は、基本の GoTo メソッドで発生することを実行し、その山積みのリソースの生産高を元の値に戻してから離れるようにする必要があります。そのために GoTo メソッドをオーバーライドしてみましょう。

1. ProductivityUnit スクリプトで、ResetProductivity という新しいメソッドを作成します。GoTo メソッドはどちらも同じ機能を必要とするので、両方から呼び出せるように 1 つのメソッドを構築します。

2. 次に、m_CurrentPile 変数が null であるかどうかを確認します。もし null でなければ、m_currentPile.ProductionSpeed を ProductivityMultiplier で割って元の値に戻してから、null に設定します。

void ResetProductivity()
{
    if (m_CurrentPile != null)
    {
        m_CurrentPile.ProductionSpeed /= ProductivityMultiplier;
        m_CurrentPile = null;
    }
}

3. Building をターゲットパラメーターとする public override GoTo メソッドを新規に作成します:

public override void GoTo(Building target)
{

}

4. 作成した ResetProductivity メソッドと base.GoTo メソッドを呼び出します。

public override void GoTo(Building target)
{
    ResetProductivity(); // call your new method
    base.GoTo(target); // run method from base class
}

base のラベルは、このオーバーライドメソッドの新しいコードに加えて、元のメソッドを実行するようにスクリプトに指示します。

5. もう一方の GoTo についても、まったく同じ作業を繰り返します:

public override void GoTo(Vector3 position)
{
    ResetProductivity();
    base.GoTo(position);
}

これらのメソッドは、ユーザーが製造装置の新しい場所を選択するとすぐに実行されます。移動する前に、現在生産性の高い山積みを選択していた場合、現在の山積みの生産速度を元の速度に戻します。

6. スクリプトを保存して Unity に戻り、再度プレイテストを行います。

7. 自分の作業員を山積みのリソースに移動させて、作業員がたどり着く前に山積みを選択します。

8. 山積みのリソースの生産高の増加を観察し、別の山積みに作業員を移動させます。

9. 元の山積みを再選択し、その割合が減少した量に戻ったことに注目してください。

8. まとめ

継承とポリモーフィズムは、クラス間の相互関係を作り出し、最終的に書くべきコード量を減らすのに役立ちます。このチュートリアルでは、親クラスである Unit クラスの機能を拡張した独自の新しいクラスを作成しました。

Complete this tutorial