Unityでゲームやアプリを開発していると、初期化のタイミングが原因で謎のバグに遭遇することがあります。
Start()
より前に処理が動いてnull参照になるOnEnable()
が意図しないタイミングで実行されるDontDestroyOnLoad
を使ったオブジェクトが2重に動作する
これらはすべて「初期化のタイミングが曖昧なまま設計されたこと」によって発生する問題です。
本記事では、Unityの初期化に潜む罠と、それを避けるための設計アプローチについて紹介します。
1. Unityの初期化タイミングは意外と信用できない
Unityには以下のようなライフサイクルメソッドがあります:
メソッド | 呼ばれるタイミング | 問題になりやすい点 |
---|---|---|
Awake() |
インスタンス生成直後 | 他の依存オブジェクトが未生成の可能性がある |
OnEnable() |
オブジェクトが有効化された瞬間 | 再呼び出しの罠。非同期下でバグりやすい |
Start() |
Awake() の後、初回フレーム前 |
順序が保証されず、意図より遅延することも |
Update() |
毎フレーム | 初期化が完了していないまま動き出すと致命傷に |
「この順で呼ばれるだろう」と想定してコードを書くと、条件によっては順番が崩れ、バグになります。
2. 実際に遭遇した初期化バグの例
2-1. Singleton.Instanceがnullのまま呼ばれて落ちる
- 他のスクリプトの
Start()
から参照された時点で、Singleton側のAwake()
がまだ呼ばれていなかった
2-2. DontDestroyOnLoadが再生成され、2重動作に
- 複数シーンでGameManagerを生成し、チェック漏れで重複
2-3. OnEnable()で意図せずイベント登録され暴走
SetActive(true)
で毎回イベント購読→多重登録→破綻
3. 初期化を「呼ばれる側」に任せると破綻する
Unityのライフサイクルメソッドは、便利な反面「順番」を保証してくれません。
つまり「このコンポーネントが用意できたら呼ばれるはず」という設計は破綻しやすい構造です。
4. 明示的なInit()関数で順序をコントロールする
おすすめのアプローチは以下の通り:
Init()
という明示的な初期化関数を用意し、GameManagerなどの管理スクリプトから呼ぶ- 初期化が完了したかどうかを判定する
isInitialized
フラグを持たせる - ライフサイクルメソッド内では絶対に他オブジェクトに依存しない
public class EnemySpawner : MonoBehaviour {
private bool isInitialized = false;
public void Init(GameManager manager) {
this.manager = manager;
isInitialized = true;
}
void Update() {
if (!isInitialized) return;
// 通常処理
}
}
5. 個人開発で特にハマりやすいパターン
ScriptableObject
から参照される MonoBehaviour- UI表示とロジック初期化が絡む時(例:スコアやステータス)
DontDestroyOnLoad
と非同期読み込みの組み合わせ
こうした状況では、明示的な初期化順制御が非常に重要になります。
おわりに
Unityの初期化処理は便利ですが、それに頼りきると“壊れる初期化”を生んでしまいます。
特にシーンをまたいだ設計や、複数オブジェクトの連携が発生するプロジェクトでは、初期化順のズレが命取りです。
「誰が・いつ・どの順で」初期化するのか。
これを明示的に制御することで、バグの温床を根本から断ち切ることができます。
今後の設計の参考になれば幸いです。
ブログでもUnityや個人開発ネタを発信中です!
開発ノウハウやアプリ制作過程、Unity連携系のハマりポイントなど
より深掘りした内容をブログにまとめています。
▶ https://syunpp.com
公開中のアプリ一覧はこちら!
実際にUnityで開発してリリース済みのアプリ一覧をまとめています。
▶ https://syunpp.com/公開中のアプリ一覧/
YouTubeショートでもゲーム開発の裏側を公開中!
Unityで制作中のゲームの進捗や演出テスト、実装の様子を
ショート動画でタイムラプス形式にまとめて発信しています。
▶ https://www.youtube.com/@syunpp_8413/shorts
コメント