Unityで基底クラスと派生クラスを継承したときのライフサイクルメソッドの扱い
Unity でゲームオブジェクトを設計する際、共通処理を 基底クラス(親クラス) にまとめ、個別の挙動を 派生クラス(子クラス) に実装するのはよくあるパターンです。
しかし、このとき ライフサイクルメソッド(Awake / Start / Update など) の挙動を理解していないと、思わぬエラーにつながります。
事例:親で Rigidbody を取得しているがエラーになる
以下のように親クラス BaseMover で Rigidbody を取得し、子クラス PlayerMover を実際のオブジェクトにアタッチするケースを考えます。
public class BaseMover : MonoBehaviour
{
protected Rigidbody _rb;
protected void Start()
{
_rb = GetComponent<Rigidbody>();
}
protected void Update()
{
_rb.AddForce(Vector3.forward); // NullReferenceException
}
}
public class PlayerMover : BaseMover
{
protected void Start()
{
// 親の Start() は自動では呼ばれない!
}
}
Unity でクラスを設計するとき、メソッドやフィールドに protected を付けるかどうかは迷いやすいポイントです。
基準を整理すると次のようになります。
1. Unity のライフサイクルメソッドの場合
- Awake, Start, Update などは、Unity が内部から呼ぶので private でも動作します。
- ただし、子クラスでオーバーライドしたいなら protected virtual にする必要があります。
基準
- 派生クラスで拡張予定 → protected virtual
- 派生クラスで触らせない → private
2. フィールドの場合
protected
- 親クラスで初期化し、子クラスでも直接利用してほしい変数
- 例:protected Rigidbody _rb;→ 親で GetComponent して、子で _rb.AddForce(…) するような場合
private
- 子クラスから直接触らせたくない変数
- 外部からはインスペクタやプロパティで操作させたい場合
プロパティ経由(推奨ケースも多い)
- 直接触らせるとバグの温床になるときは、private フィールドにして protected なプロパティを公開
private Rigidbody _rb;
protected Rigidbody Rb => _rb;
→ 読み取り専用にできるので安全
3. メソッドの場合
protected
- 共通処理を親にまとめ、子から呼びたいとき
- 例:
protected void Jump()
{
_rb.AddForce(Vector3.up * 5, ForceMode.Impulse);
}
→ 子は Jump() を呼べる
private
- 親内部だけで使うヘルパー処理(子に渡す必要がないもの)
4. 基準まとめ表
用途 | private | protected |
---|---|---|
Unity標準のライフサイクル (Start/Updateなど) | 拡張しないなら private でOK | 派生クラスで拡張予定なら protected virtual |
フィールド | 子に触らせない設計 → private | 子にも使わせたい設計 → protected |
メソッド | 内部のみに閉じる処理 → private | 子から利用/拡張してほしい処理 → protected |
5. 実務での指針
- まず private にする
- 「子クラスからも使いたい」と明確に必要になったときに protected に切り替える
- 「オーバーライドさせたい」場合だけ protected virtual にする
つまり protected はデフォルトでは不要で、「子でも使う/拡張させる設計」のときに初めて付けるのが基準です。
この場合、子に Start を定義すると 親の Start は呼ばれず、_rb が初期化されないまま使われてエラーになります。
原因:Unityのライフサイクルメソッドは自動継承されない
C# の通常のメソッドとは異なり、Unity が自動で呼び出す特殊メソッド(Awake / Start / Update など)は、子に同名メソッドがあると親は呼ばれないという仕様になっています。
解決方法
方式A:base.Start() を呼ぶ
子でも初期化を追加したいなら、必ず base.Start() を呼びます。
public class BaseMover : MonoBehaviour
{
protected Rigidbody _rb;
protected virtual void Start()
{
_rb = GetComponent<Rigidbody>();
}
}
public class PlayerMover : BaseMover
{
protected override void Start()
{
base.Start(); // 親の初期化を呼ぶ
// 子の初期化処理
}
}
Tips
- virtual/override を付けて意図を明確化。
- base.Start() を忘れると即エラー。
方式B:初期化は Awake に寄せる(推奨)
依存コンポーネント(Rigidbody, Collider など)の取得は Awake に書くのが安全です。
[RequireComponent(typeof(Rigidbody))]
public class BaseMover : MonoBehaviour
{
protected Rigidbody _rb;
protected virtual void Awake()
{
_rb = GetComponent<Rigidbody>();
}
}
public class PlayerMover : BaseMover
{
// Start は従来どおり使ってOK
protected void Start()
{
// 子の初期化(親のAwakeですでにRigidbodyは取得済み)
}
}
Tips
- [RequireComponent] で付け忘れ防止。
- 実務では「Awake → コンポーネント取得」「Start → 外部同期・状態リセット」で役割分担。
方式C:子に Start を書かない
子に特別な初期化が不要なら、子の Start を削除するだけで親の処理が動きます。
public class PlayerMover : BaseMover
{
// Start を書かない → 親の Start が自動で呼ばれる
}
Tips
- 一番シンプルで安全。
- 追加の初期化が必要になったら方式Aに切り替え。
方式D:使う直前に遅延取得する
保険的に「nullならGetComponentする」方法もあります。
protected Rigidbody _rb;
Rigidbody Rb => _rb ??= GetComponent<Rigidbody>();
protected virtual void Update()
{
Rb.AddForce(Vector3.forward);
}
Tips
- パフォーマンスや明示性の面では基本非推奨。
- 毎フレーム呼ばれる Update 内での利用は注意。
図解で理解するライフサイクル呼び出し関係
ケース1:親と子に両方 Start がある(親は呼ばれない)

ケース2:子に Start を書かない(親だけが呼ばれる)

ケース3:子で base.Start() を呼ぶ(両方呼ばれる)

よくあるハマりポイント
症状 | 原因 | 解決策 |
---|---|---|
NullReferenceException が出る | 子に Start を書いて親が呼ばれなかった | base.Start() を呼ぶ or 子の Start を削除 |
Awake で初期化したのに値が null のまま | 実際は Start で初期化していた | コンポーネント取得は Awake に寄せる |
Rigidbody を付け忘れて動かない | インスペクタでアタッチしていない | 親クラスに [RequireComponent(typeof(Rigidbody))] を付ける |
Update で null エラーが出る | 初期化が遅れている、または呼ばれていない | 遅延取得(_rb ??= GetComponent<Rigidbody>())で保険をかける |
子の Start を追加したら動かなくなった | Unity のライフサイクルは親自動呼び出しをしない仕様 | 仕様を理解し base.Start() を忘れない |
実務での指針
- Awake → コンポーネント取得
- Start → 外部との同期や状態リセット
- Update → 毎フレーム処理
子クラスに Start を書くなら必ず base.Start() を呼ぶ
子で初期化が不要なら Start 自体を削除する
まとめ
- Unity のライフサイクルメソッドは 親クラスが自動で呼ばれない
- 子で初期化したいなら base.XXX() を忘れず呼ぶ
- 依存コンポーネントは Awake で初期化するのが安全
- 子に Start を書かないのもシンプルで有効な方法
ディスカッション
コメント一覧
まだ、コメントがありません