【Unity】継承したクラスに空の Start() を残してはいけない理由

2026年10月21日

広告

はじめに

Unityでゲームを作っていると、こんな経験はありませんか?

  • 親クラスに書いた初期化処理が、なぜか動かない
  • HPが0のままになっている
  • エラーは出ていないのに、動きがおかしい

実はこれ、子クラスに残っている「空の Start()」が原因かもしれません。この記事では、その理由と対処法を解説します。

今回のコード

敵やプレイヤーに共通する「ステータス」を、親クラス MobStatus にまとめたとします。

using UnityEngine;

public class MobStatus : MonoBehaviour
{
    protected int hp;

    void Start()
    {
        hp = 100;   // 共通の初期化
        Debug.Log("HPを初期化しました");
    }
}

そして、プレイヤー用の子クラスを作ります。Unityで新しいスクリプトを作ると、こうなっているはずです。

using UnityEngine;

public class PlayerStatus : MobStatus
{
    void Start()
    {
    }

    void Update()
    {
    }
}

この状態で実行すると、「HPを初期化しました」は表示されません。hp も 0 のままです。

原因:Unityは「一番下の子クラス」の Start() だけを呼ぶ

Unityは、Start() や Update() などのメッセージ関数を、実行時に探して呼び出しています。このとき呼ばれるのは、一番子供にあたるクラス(今回は PlayerStatus)で定義された Start() だけです。

PlayerStatus に Start() がある
  → PlayerStatus.Start() が呼ばれる(中身は空)
  → MobStatus.Start() は呼ばれない

つまり、子クラスの空の Start() が、親の Start() の出番を奪ってしまうのです。

「何も書かない」と「何もしないと書く」は違う

子クラスに Start() を「何も書かない」ことと、空の Start() を書いて「何もしない」と定義することは、動作が違います。

子クラスに Start() がない場合は、Unityが継承元をたどり、親クラスの Start() を実行します。今回の PlayerStatus から Start() を削除すると、MobStatus.Start() の hp = 100; が実行され、HPが100になります。

子クラスに空の Start() がある場合は、Unityが子クラスの Start() を呼び出します。中身が空なので、その呼び出しでは何も処理しません。親クラスの Start() が自動で続けて呼ばれることはないため、親の初期化は実行されず、今回の hp は初期値の0のままです。空の Start() は「Start() が存在しない」という意味にはなりません。

ここでの動作は、C#の override を書いたから起こるものではありません。Start() はUnityが呼び出すメッセージ関数であり、最初の例のように virtual や override がなくても、子に同名・同じ引数の Start() があれば、親の Start() は自動では実行されません。

親の初期化に子の処理を追加したい場合は、後の「解決方法②」のように、親を protected virtual void Start()、子を protected override void Start() とし、子の中で base.Start(); を呼びます。virtual は子でオーバーライドできるようにする指定、override はそのメソッドを子でオーバーライドする指定です。この2つを付けるだけでは、親の処理は実行されません。base.Start(); が親の Start() を明示的に呼び出し、その後に子の初期化を書けば、親、子の順で処理できます。

なお、最初の例の void Start() は、アクセス修飾子を省略しているためC#では private です。そのままでは子から base.Start() で呼ぶことも、override することもできません。解決方法②では、子から呼べる protected と、オーバーライドを許可する virtual を親に付けています。base.Start() 自体は、子からアクセスできる親の通常のメソッドにも使えますが、override を使うには親側が virtual などである必要があります。

なぜ気づきにくいのか

  • エラーも警告も出ない(文法的には正しいコードなので、コンパイルは通ります)
  • 書いた本人は「中身が空だから何も起きないはず」と思っている
  • Unityが自動で作ってくれるテンプレートなので、そもそも自分で書いた意識がない

原因が見えにくいため、何時間も悩んでしまう初心者がとても多いポイントです。

解決方法①:使わない Start() は削除する

一番シンプルな対処法です。子クラスで Start() を使わないなら、消してしまいましょう。

using UnityEngine;

public class PlayerStatus : MobStatus
{
    // Start() は書かない → 親の Start() が呼ばれる
}

これで MobStatus.Start() が実行され、HPが正しく初期化されます。Update() も同様に、使わないなら消しておくのがおすすめです。

解決方法②:親の処理も呼びつつ、子の処理も追加する

子クラスでもプレイヤー固有の初期化をしたい場合は、virtual と override を使い、base.Start() で親の処理を呼び出します。

using UnityEngine;

public class MobStatus : MonoBehaviour
{
    protected int hp;

    protected virtual void Start()    // virtual を付ける
    {
        hp = 100;
        Debug.Log("HPを初期化しました");
    }
}

public class PlayerStatus : MobStatus
{
    protected override void Start()   // override を付ける
    {
        base.Start();                 // 親の処理を呼ぶ
        Debug.Log("プレイヤー固有の初期化をしました");
    }
}

ポイントは次の3つです。

  1. 親側は protected virtual void Start()
  2. 子側は protected override void Start()
  3. 子の中で base.Start(); を呼ぶ

base.Start() を忘れると、最初の例と同じく親の処理が動かないので注意しましょう。

Start() 以外でも同じ

この現象は Start() だけの話ではありません。Awake()、Update()、OnEnable()、OnDestroy() など、Unityのメッセージ関数すべてで同じことが起こります。

ここまでの内容を、図でもう一度整理しておきましょう。

補足:親クラスは abstract にするとさらに安心

今回の MobStatus は、PlayerStatus や EnemyStatus の共通部分をまとめるための親クラスです。MobStatus 単体をゲームオブジェクトにアタッチすることはありません。このように「継承して使うこと」を前提にしたクラスには、abstract を付けておくと、設計の意図がコードで伝わります。

using UnityEngine;

public abstract class MobStatus : MonoBehaviour   // abstract を付ける
{
    protected int hp;

    protected virtual void Start()
    {
        hp = 100;
        Debug.Log("HPを初期化しました");
    }
}

abstract を付けたクラスは、そのままではインスタンスを作れません。Unityでもコンポーネントとしてアタッチできないので、親クラスを間違ってオブジェクトに付けてしまうミスを防げます。子クラス(PlayerStatus など)の書き方は、解決方法②と同じです。

ここで押さえておきたいのは、abstract は「親クラス単体では使えない」ようにするだけで、Start() の呼ばれ方は変えない、という点です。子クラスに空の Start() が残っていれば、abstract を付けても親の Start() は実行されません。この記事の対処法(削除する、または virtual / override + base.Start())は、abstract の有無にかかわらず必要です。

また、protected abstract void Start(); のようにメソッドを abstract にすると、親クラスには処理本体を書けなくなります。今回のように親で HP を初期化したい場合は、protected virtual void Start() を使います。

抽象クラスそのものについては、【Unity】バトルゲームで抽象クラスを使ってみる でも扱っています。

まとめ

  • Unityのメッセージ関数は、一番下の子クラスで定義されたものだけが呼ばれる
  • 子クラスに空の Start() があると、親の Start() が実行されない
  • エラーは出ないので、気づきにくい
  • 対処法は2つ
    • 使わないなら削除する
    • 使うなら virtual / override + base.Start()
  • 親クラスを単体で使わないなら、abstract を付けて継承専用にしておくと安心(ただし、空の Start() の問題は abstract では防げない)

継承を使うときは、「新しいスクリプトを作ったら、使わない Start() と Update() をまず消す」。この習慣をつけておくと、こうしたトラブルを防げます。

訪問数 46 回, 今日の訪問数 1回

広告

C#,Unity,クラス

Posted by hidepon