【Unity 6・C#入門】継承って何? MobStatusとEnemyStatusの関係を理解しよう

広告

Unityでゲームを作っていると、ほかのスクリプトに書かれている変数やメソッドを、そのまま使っているコードを見かけることがあります。

例えば、『Unity本格入門』に登場する MobStatus と EnemyStatus です。

この2つのクラスには、継承(けいしょう)というC#の仕組みが使われています。

継承は、最初は少し難しく感じるかもしれません。でも、考え方はそれほど複雑ではありません。

今回は、敵キャラクターのHPや攻撃力を管理する仕組みを例に、継承がどのように働いているのかを理解していきましょう。

1. EnemyStatusにHPが書かれていない?

まず、こんなコードを見てください。

public class EnemyStatus : MobStatus
{
    public int exp = 50;
}

EnemyStatus は、敵キャラクターのステータスを管理するクラスです。

このコードには、敵を倒したときに得られる経験値 exp が書かれています。

でも、よく見てください。HPや攻撃力が書かれていません。

敵キャラクターなのに、HPがなくても大丈夫なのでしょうか?

実は、大丈夫なのです。その理由が、次の部分にあります。

public class EnemyStatus : MobStatus

この : MobStatus が、今回のテーマである継承を表しています。

2. 継承は、ほかのクラスの機能を引き継ぐ仕組み

MobStatus には、HPや攻撃力など、キャラクターに共通する情報がまとめられているとします。例えば、次のようなコードです。

using UnityEngine;

public abstract class MobStatus : MonoBehaviour
{
    [SerializeField] private int hp = 100;
    [SerializeField] private int attack = 10;

    public int Hp => hp;
    public int Attack => attack;

    public void TakeDamage(int damage)
    {
        hp -= damage;
        if (hp < 0)
        {
            hp = 0;
        }
    }
}

abstract は、「このクラス単体では使わず、必ず継承して使う」という印です。くわしくは後で学ぶので、今は「親クラス専用の目印」と覚えておけば大丈夫です。

このクラスには、次の機能があります。

  • HPを記憶する
  • 攻撃力を記憶する
  • 現在のHPや攻撃力を取得する
  • ダメージを受けたらHPを減らす

では、先ほどの EnemyStatus をもう一度見てみましょう。

public class EnemyStatus : MobStatus
{
    public int exp = 50;
}

このコードは、次のような意味になります。

「EnemyStatusは、MobStatusの機能を引き継いで作られたクラスです」

つまり、EnemyStatus には直接HPを書いていなくても、MobStatus に用意されたHPの情報やダメージ処理を利用できます。

この関係を、親子関係にたとえて説明することがあります。

  • MobStatus は親クラス(基本クラス)
  • EnemyStatus は子クラス(派生クラス)

子クラスは、親クラスの利用可能な機能を引き継ぎます。

ただし、親クラスの private フィールドには、子クラスから直接アクセスできません。上の例では、Hp という public プロパティを通してHPを読み取ります。

3. どうして継承が必要なの?

「EnemyStatusにHPや攻撃力を直接書けばいいのでは?」そう思った人もいるかもしれません。

もちろん、それでもゲームを作ることはできます。例えば、スライム用のクラスを作ってみましょう。

public class SlimeStatus : MonoBehaviour
{
    public int hp = 30;
    public int attack = 5;
}

次に、ゴブリン用のクラスを作ります。

public class GoblinStatus : MonoBehaviour
{
    public int hp = 80;
    public int attack = 15;
}

どちらのクラスにも、hp と attack が書かれていますね。敵が2種類なら、これでもそれほど困りません。

では、敵が20種類になったらどうでしょうか。20個のクラスに同じようなコードを書くことになります。

さらに、すべての敵にダメージ処理を追加したくなった場合、それぞれのクラスを修正する必要があります。これは大変ですね。

そこで、どのキャラクターにも共通する情報や処理を、1つのクラスにまとめることを考えます。それが MobStatus です。

HPや攻撃力などの共通部分を MobStatus に用意しておけば、それを継承した EnemyStatus で再利用できます。同じコードを何度も書かずに済むわけです。

4. EnemyStatusには敵だけの情報を追加する

継承の便利なところは、親クラスの機能を引き継ぐだけではありません。子クラスに新しい機能を追加することもできます。

例えば、敵キャラクターには、倒したときにプレイヤーが得られる経験値が必要かもしれません。そこで、EnemyStatus に経験値を追加してみましょう。

using UnityEngine;

public class EnemyStatus : MobStatus
{
    [SerializeField] private int exp = 50;

    public int Exp => exp;

    public void ShowEnemyInfo()
    {
        Debug.Log($"HP: {Hp}");
        Debug.Log($"攻撃力: {Attack}");
        Debug.Log($"経験値: {Exp}");
    }
}

このコードでは、親クラスの機能に加えて、次の2つを定義しています。

  • Exp は、敵を倒したときにもらえる経験値を取得するプロパティです。
  • ShowEnemyInfo() は、敵の情報をConsoleに表示するメソッドです。

ここで注目してほしいのは、次のコードです。

Debug.Log($"HP: {Hp}");

Hp は EnemyStatus には定義されていません。MobStatus に定義されています。それなのに、EnemyStatus から使うことができています。

これが継承です。親クラスに用意されている機能を、子クラスで利用できるのです。

5. UnityのInspectorで確認してみよう

ここまで理解できたら、実際にUnityで確認してみましょう。

まず、実験用のプロジェクトを用意します。

  1. Unity 6 で新しい3Dプロジェクトを作成します。プロジェクト名は InheritanceSample などで構いません。
  2. Projectウィンドウの Assets の中に、Scripts フォルダを作成します。
  3. Scripts フォルダの中に MobStatus.cs と EnemyStatus.cs を作成し、2章の MobStatus と、4章の EnemyStatus のコードをそれぞれ書きます。

スクリプトのファイル名は、クラス名と必ず同じにしてください。MonoBehaviour を継承したクラスは、ファイル名とクラス名が違うと、GameObjectに追加できません。

続いて、Hierarchyウィンドウで空のGameObjectを作成し、名前を Slime に変更します。

次に、Slimeに EnemyStatus スクリプトを追加してください。Inspectorには、次のような項目が表示されるはずです。

Slime
Enemy Status (Script)
    Hp          100
    Attack       10
    Exp          50

少し不思議ではありませんか?

EnemyStatus には hp や attack というフィールドを書いていません。でも、InspectorにはHPと攻撃力が表示されています。

これは、EnemyStatus が MobStatus を継承しているからです。親クラスにある [SerializeField] を付けたフィールドも、子クラスのコンポーネントのInspectorに表示されます。

もちろん、ここでHPや攻撃力の値を変更することもできます。例えば、スライムのHPを30にして、ゴブリンのHPを80にすることも可能です。どちらも同じ EnemyStatus を使いながら、キャラクターごとに違う値を設定できるわけです。

なお、MobStatus は abstract なので、そもそもGameObjectにコンポーネントとして追加することはできません。Slimeには EnemyStatus だけを追加すれば、MobStatus の機能もすでに引き継がれています。

6. 継承したメソッドを呼び出してみよう

今度は、スライムにダメージを与えてみましょう。新しく EnemyTest.cs を、同じ Scripts フォルダに作成して、次のコードを書いてください。

using UnityEngine;

public class EnemyTest : MonoBehaviour
{
    private void Start()
    {
        EnemyStatus enemy =
            GetComponent<EnemyStatus>();

        if (enemy == null)
        {
            Debug.LogError("EnemyStatusがありません");
            return;
        }

        enemy.ShowEnemyInfo();
        enemy.TakeDamage(20);
        Debug.Log($"現在のHP: {enemy.Hp}");
    }
}

このスクリプトもSlimeに追加します。HPを100に設定した状態でゲームを再生してみてください。Consoleには、次のように表示されます。

HP: 100
攻撃力: 10
経験値: 50
現在のHP: 80

さて、ここで重要なことを確認しましょう。

enemy.TakeDamage(20);

この TakeDamage() メソッドは、どこに書かれていましたか? EnemyStatus でしょうか?

いいえ、MobStatus に書かれていましたね。EnemyStatus には TakeDamage というメソッドを書いていません。それでも呼び出せるのは、EnemyStatus が MobStatus を継承しているからです。

この動作を理解できたら、継承の基本がつかめたことになります。

7. GetComponentで親クラスを指定できる?

ここは少し発展的な内容です。

これまで、EnemyStatus を取得するときには、次のように書いていました。

EnemyStatus enemy =
    GetComponent<EnemyStatus>();

では、次のように書いたらどうなるでしょうか?

MobStatus mob =
    GetComponent<MobStatus>();

Slimeには EnemyStatus を追加していますが、MobStatus は追加していません(abstract なので追加できません)。それでも、EnemyStatus コンポーネントを MobStatus 型として取得できます。なぜでしょうか?

それは、EnemyStatus が MobStatus を継承しているからです。EnemyStatus は、MobStatus の一種として扱うことができます。そのため、次のようなコードも実行できます。

MobStatus mob = GetComponent<MobStatus>();

if (mob != null)
{
    mob.TakeDamage(20);
}

MobStatus 型の変数を使っていても、親クラスの TakeDamage() を呼び出せます。

ただし、MobStatus 型の変数からは、EnemyStatus だけに定義されている ShowEnemyInfo() をそのまま呼ぶことはできません。

この違いは、今後学習する「ポリモーフィズム」を理解するときにも重要になります。今は、子クラスのオブジェクトは親クラスの型としても扱えると覚えておきましょう。

8. プレイヤーもMobStatusを継承できる

MobStatus は、敵だけに使う必要はありません。HPや攻撃力は、プレイヤーにも必要ですね。そこで、プレイヤー用の PlayerStatus を作ってみます。

using UnityEngine;

public class PlayerStatus : MobStatus
{
    [SerializeField] private int level = 1;

    public int Level => level;
}

このコードでは、MobStatus を継承し、プレイヤー専用の level を追加しています。すると、記事の冒頭の図の上段のような関係になります。

EnemyStatus と PlayerStatus は、それぞれ MobStatus の機能を利用できます。さらに、敵には敵の情報、プレイヤーにはプレイヤーの情報を追加できます。

これが、継承を利用したクラス設計の基本的な考え方です。

9. 継承とGameObjectへのコンポーネント追加は違う

ここで、Unityを学習している皆さんに気をつけてほしいことがあります。それは、継承とコンポーネントの追加は、別の仕組みだということです。

例えば、GameObjectにRigidbodyやColliderを追加することがありますね。これは、異なる機能を持つコンポーネントを組み合わせています。

一方、EnemyStatus が MobStatus を継承するのは、クラス同士の関係です。次のように考えるとわかりやすいでしょう。

  • 継承:「EnemyStatusは、MobStatusの一種である」
  • コンポーネント:「SlimeというGameObjectは、RigidbodyやColliderを持っている」

Unityでは、この2つの考え方を組み合わせてゲームを制作していきます。

10. 最後に3つの問題を考えてみよう

ここまで読んだら、理解できているか確認してみましょう。

問題①

次のコードでは、親クラスはどちらでしょうか?

public class EnemyStatus : MobStatus
{
}

問題②

MobStatus に次のメソッドが定義されているとします。

public void Heal(int amount)
{
    // HPを回復する処理
}

EnemyStatus のオブジェクトから、Heal メソッドを呼び出せるでしょうか?

問題③

EnemyStatus に経験値を取得する Exp プロパティがあります。MobStatus 型の変数から、そのまま Exp プロパティを使えるでしょうか?

少し考えてから、答えを確認してください。


答え①

親クラスは MobStatus です。コロン : の後ろに書かれているクラスを継承します。

答え②

呼び出せます。EnemyStatus は MobStatus を継承しているため、親クラスの public メソッドを利用できます。

答え③

そのままでは使えません。Exp は EnemyStatus で新しく追加したプロパティだからです。MobStatus 型の変数から利用するには、型の扱いをさらに理解する必要があります。

まとめ

今回は、『Unity本格入門』に登場する MobStatus と EnemyStatus の関係を例に、C#の継承について学習しました。

継承では、あるクラスの機能を引き継いで、新しいクラスを作ることができます。今回の例では、MobStatus にHPや攻撃力、ダメージ処理などの共通部分をまとめました。そして、EnemyStatus で MobStatus を継承し、敵専用の経験値を追加しました。

このように設計すると、同じコードを繰り返し書く必要が少なくなります。

最初は : の意味がわかるだけでも十分です。でも、今後Unityでスクリプトを読むときには、クラス宣言の部分を少し意識してみてください。

public class EnemyStatus : MobStatus

この1行を見たときに、「ああ、EnemyStatusはMobStatusの機能を引き継いでいるんだな」と気づけるようになれば、今回の学習は成功です。

そして、わからない変数やメソッドが登場したときには、親クラスに定義されていないかを調べてみましょう。今まで不思議に思っていたコードの仕組みが、少しずつ理解できるようになるはずです。


※この記事のコードは、継承の基本を理解するための学習用サンプルです。書籍に掲載されているコードをそのまま再現したものではありません。

訪問数 10 回, 今日の訪問数 10回

広告

C#,Unity6,クラス

Posted by hidepon