【Unity】継承したクラスに空の Start() を残してはいけない理由
はじめに
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つです。
- 親側は
protected virtual void Start() - 子側は
protected override void Start() - 子の中で
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() をまず消す」。この習慣をつけておくと、こうしたトラブルを防げます。











ディスカッション
コメント一覧
まだ、コメントがありません