Unity Instantiate重い問題の対処法|責務分離でパフォーマンスを改善

プロファイラを開いた瞬間、「Instantiateが重い」という診断に待ったをかけたくなりました。フレーム落ちの犯人はInstantiateメソッド自体ではなく、その呼び出し先のクラスが抱え込んだ複数の責務だったのです。今回、単一責任原則(Single Responsibility Principle)に基づいたクラス分割によって、オブジェクト生成時のパフォーマンスを改善した事例を紹介します。

何が起きたか

あるプレハブのInstantiate呼び出しで顕著なフレーム落ちが発生していました。プロファイラで調べると、生成処理そのものよりもAwake/Startで実行される初期化処理が重い。該当クラスは複数の役割を抱え込んでいたため、一つのオブジェクト生成で不要な初期化まで走っていました。クラス設計の責務分離によって、この問題を根本から解決した経緯を記録します。

症状の観測とプロファイル結果

問題の発端は、特定のプレハブをシーン上に動的生成した際のフレーム落ち。ユーザー体験としては、オブジェクトが画面に現れる瞬間にカクつきが発生し、操作が一時的に止まったように感じられます。60fpsを維持すべき処理が、この生成タイミングで一時的に30fps以下まで低下していました。

Unity Profilerで詳細を追うと、Instantiateメソッドの実行時間そのものは短く、問題はその直後のAwake/Start内で走る初期化処理にあった。具体的には、以下のような処理が一度に実行されていたのです。

  • 複数のコンポーネントへの参照取得(GetComponent呼び出しの連鎖)
  • UIテキスト・画像への初期値設定
  • ネットワーク通信用のコールバック登録
  • 状態管理用の内部変数初期化

これらがすべて単一のクラス内で実行されていたため、オブジェクト生成の度に不要な処理まで走り、フレーム落ちを引き起こしていました。

根本原因――責務の肥大化

該当クラスを読み解くと、一つのクラスが以下の役割を同時に担っていました。

役割具体的な処理内容
データ管理オブジェクトの状態(座標、パラメータ等)を保持
UI更新画面上のテキスト・画像を変更
ネットワーク通信サーバーへのリクエスト送信・レスポンス受信
状態制御オブジェクトのライフサイクル管理(生成・破棄タイミング等)

この構造は、Single Responsibility Principleに反する典型的な設計ミスです。クラスが複数の理由で変更される状態になっており、どれか一つの機能を修正するたびに他の機能にも影響が及ぶリスクを抱えていました。

さらに問題だったのは、オブジェクト生成時にすべての初期化が実行される点。例えば、ネットワーク通信を使わないシーンでもコールバック登録が走り、UI更新が不要なタイミングでもテキスト参照を取得していた。結果として、Instantiate呼び出しの直後に重い初期化処理が一度に発火し、フレームレートを圧迫していたのです。

改善方針――責務ごとのクラス分割

解決策として、クラスを役割ごとに分割し、各々が単一の責務だけを持つ構造へ再設計しました。具体的には以下の方針で分割を進めています。

分割前(Before)

  • 単一のクラスがデータ・UI・通信・状態をすべて管理
  • Awake/Startですべての初期化が一度に実行
  • 不要な処理も毎回走る
  • クラス名に社内固有の名称を含む

分割後(After)

  • データ管理クラス、UI制御クラス、通信クラスに分離
  • 各クラスは自分の責務に必要な初期化のみ実行
  • 必要な処理だけを必要なタイミングで実行
  • クラス名を役割ベースの一般名へ抽象化

クラス名の抽象化は、将来の拡張性を確保する意図もあります。社内固有のプロジェクト名やコードネームをクラス名に含めると、用途が変わった際に名前と実態が乖離するリスクがある。役割を表す一般名にしておくことで、コードの可読性と再利用性を高めました。

実装の詳細

分割後の構成は以下のようになっています。

Yes
No
Yes
No
GameObject生成
データ管理クラス初期化
UI更新が必要?
UI制御クラス初期化
スキップ
ネットワーク通信が必要?
通信クラス初期化
完了

データ管理クラス

オブジェクトの状態(座標、パラメータ等)のみを保持。Awakeでは最小限のフィールド初期化だけを実行し、他のクラスへの依存を持ちません。状態の変更は外部からのメソッド呼び出しを通じて行い、内部で勝手にUI更新や通信を行わない設計にしました。

UI制御クラス

画面表示に関する処理を担当します。データ管理クラスの状態を監視し、変更があった場合にのみテキスト・画像を更新。Awakeでは必要なコンポーネント参照の取得だけを行い、初期値の設定はStartで遅延実行する形に変更しました。これにより、UI更新が不要なシーンではこのクラスをアタッチせず、初期化処理自体をスキップできるようになっています。

通信クラス

サーバーとのやり取りを担当。データ管理クラスから必要な情報を受け取り、リクエスト送信とレスポンス処理を実行します。通信が必要になった時点で初めてコールバックを登録し、不要な場合は何もしません。この遅延初期化により、ネットワークを使わないシーンでの無駄な処理を排除しました。

各クラスの依存関係は一方向に整理。データ管理クラスは他に依存せず、UI制御クラスと通信クラスがデータ管理クラスを参照する構造です。これにより、循環参照を避け、テストやメンテナンスがしやすい設計になっています。

改善結果

リファクタリング後、該当プレハブのInstantiate呼び出し時のフレーム落ちは解消されました。プロファイラ上で確認すると、Awake/Startの実行時間が大幅に短縮され、60fpsを維持したままオブジェクト生成が完了するようになっています。

具体的な数値としては、以下の改善を確認しました。

項目改善前改善後
Awake/Start処理時間約15ms約3ms
フレームレート30fps以下に低下60fps維持
GetComponent呼び出し回数8回2回

体感としても、オブジェクト生成時のカクつきが完全になくなり、操作の滑らかさが向上。実機テストでも同様の改善が確認されています。

現状の制約と今後の課題

今回の改善は特定のプレハブに対するものであり、プロジェクト全体への展開はまだ途中です。既存のコードには、まだ責務が分離しきれていないクラスが残っており、段階的にリファクタリングを進める予定。

また、一部の古いクラスは他のシステムとの依存関係が深く、分割のリスクが高いため慎重に進める必要があります。これらについては、新規機能追加のタイミングで少しずつ整理していく方針を採っています。

まとめ

Instantiateが重い原因は、生成されるオブジェクトのクラスが複数の責務を抱え込み、不要な初期化を実行していたことにありました。責務分離によってクラスを再設計し、各々が単一の役割だけを持つ構造へ変更することで、パフォーマンスを改善できた。

設計原則は教科書的な話に聞こえるかもしれませんが、実際のパフォーマンス問題を解決する鍵になります。「Instantiateが重い」と感じたら、まずプロファイラで初期化処理を追い、そのクラスが何をしているのかを見直してみてください。同じ境界でつまずく人が減れば幸いです。