Unity asmdef分離で実現する疎結合設計とビルド時間短縮の実践

1ファイル修正するたび30秒のリコンパイルを待つ。その30秒が積み重なり、チーム全体で月に数十時間のロスが生まれていました。原因は、すべてのスクリプトが単一のアセンブリへ詰め込まれていたこと。

やわさんです。今回はUnityの1ファイル修正で30秒待たされていたリコンパイル地獄を、asmdefによるアセンブリ分割と依存の逆転で解消した話を紹介します。コアを独立アセンブリに切り出すことで、影響範囲を限定しつつテストも軽量化できた実践例です。

改善の概要

Unityプロジェクトの肥大化に伴い、コアモジュールの変更がプロジェクト全体の再コンパイルを引き起こしていました。asmdefによる論理的な境界の導入と、インターフェースを用いた依存の逆転によって、コアを独立したアセンブリとして切り出し、変更の影響範囲を限定。結果、リコンパイル時間の短縮と、テスト実行時の不要な依存排除を両立させています。

前提知識:Assembly Definitionとコンパイル単位

Unityでは、プロジェクト内のC#スクリプトがデフォルトですべてAssembly-CSharp.dllという単一のアセンブリにまとめられます。1つのスクリプトを変更すると、このアセンブリ全体が再コンパイルされるため、プロジェクト規模が大きくなるほど待機時間が増大。

Assembly Definition(asmdef)ファイルは、スクリプトを論理的な単位に分割し、依存関係を明示的に記述する仕組みです。asmdefを配置したディレクトリ配下のスクリプトは独立したアセンブリとなり、他のアセンブリへの依存はreferencesフィールドで宣言します。変更の影響を受けるアセンブリだけが再コンパイルされ、全体の待機時間を抑えられる仕組み。

直面していた課題

プロジェクトの成長とともに、コアモジュールが多数の機能と暗黙的に結合していました。

  • コアの1ファイルを変更するたび、UI・ゲームロジック・インフラ層を含む全スクリプトが再コンパイルされる
  • 依存関係が可視化されておらず、どのモジュールがコアに依存しているか把握しにくい
  • テスト実行時に無関係なアセンブリまでロードされ、テストの起動が遅い

特に、エディタ上での反復作業が多い開発フェーズでは、リコンパイル待ちが開発体験を大きく損なっていました。

リファクタリング前のアーキテクチャ

以下の図は、asmdefを導入する前の依存関係です。すべてのスクリプトがAssembly-CSharpに含まれており、境界が存在しません。

暗黙的依存
暗黙的依存
暗黙的依存
暗黙的依存
Assembly-CSharp.dll
コアモジュール
UIモジュール
ゲームロジック
インフラ層

この状態では、コアモジュールの変更が全体の再コンパイルを引き起こし、影響範囲の見通しが立ちませんでした。テスト実行時も、コアのテストに無関係なUIやインフラのコードまでロードされます。

設計方針と疎結合化の戦略

リファクタリングの方針として、以下の3点を軸に据えました。

  • コアの独立:コアモジュールを独自のasmdefとして切り出し、他のモジュールに依存しない状態にする
  • 依存の一方向化:UI・ゲームロジック・インフラ層がコアに依存する方向に整理し、逆方向の依存を排除する
  • インターフェースによる抽象化:コアが具象クラスに依存していた箇所をインターフェース経由に変更し、依存性注入で実装を差し替え可能にする

分離の粒度は、機能の凝集度と変更頻度を基準に決定しました。例えば、UIとゲームロジックは変更頻度が高く、互いに独立して開発されることが多いため、別アセンブリに分割。一方、インフラ層は横断的に利用される基盤機能であり、独立したアセンブリとして最下層に配置しました。

リファクタリング後のアーキテクチャ

asmdef導入後の依存関係は以下の通り。コアが独立し、他のモジュールからの一方向依存に整理されています。

Core.asmdef
Infrastructure.asmdef
UI.asmdef
GameLogic.asmdef
Tests.asmdef

各アセンブリの責務は次の通りです。

  • Core.asmdef:ドメインロジックとデータモデルを含む。他のアセンブリに依存しない
  • UI.asmdef:画面表示とユーザー入力の処理。Coreのインターフェースを通じてロジックを呼び出す
  • GameLogic.asmdef:ゲーム固有の状態管理と制御フロー。Coreに依存し、UIから独立
  • Infrastructure.asmdef:ファイルI/O、ネットワーク通信など横断的な基盤機能
  • Tests.asmdef:Coreとインフラのテストコード。UIやゲームロジックから独立

依存の逆転による疎結合化

当初、コアモジュールがUI側の具象クラスを直接参照していた箇所がありました。これをインターフェースに置き換え、コア側でインターフェースを定義し、UI側で実装するよう変更。具体例として、通知機能の実装を挙げます。

📦 Before:具象クラスへの直接依存

  • コアがNotificationUIクラスを直接参照
  • コアの変更時にUI側もリコンパイルされる
  • テスト時に実際のUI実装が必要

✅ After:インターフェース経由の依存

  • コアがINotificationServiceインターフェースを定義
  • UI側でNotificationUIINotificationServiceを実装
  • テスト時はモック実装を注入可能

この変更により、コアの変更がUI側の再コンパイルを引き起こさなくなり、テスト時にUI実装に依存せず済むようになりました。

実装の詳細

asmdefファイルの作成と配置

各モジュールのルートディレクトリに、以下のようなasmdefファイルを配置しました。

// Assets/Scripts/Core/Core.asmdef
{
  "name": "Core",
  "references": [
    "Infrastructure"
  ],
  "includePlatforms": [],
  "excludePlatforms": [],
  "allowUnsafeCode": false,
  "overrideReferences": false,
  "precompiledReferences": [],
  "autoReferenced": true,
  "defineConstraints": [],
  "versionDefines": [],
  "noEngineReferences": false
}
// Assets/Scripts/UI/UI.asmdef
{
  "name": "UI",
  "references": [
    "Core",
    "Infrastructure"
  ],
  "includePlatforms": [],
  "excludePlatforms": [],
  "allowUnsafeCode": false,
  "overrideReferences": false,
  "precompiledReferences": [],
  "autoReferenced": true,
  "defineConstraints": [],
  "versionDefines": [],
  "noEngineReferences": false
}

referencesフィールドで依存先のアセンブリ名を明示することで、Unityエディタが依存関係を強制します。逆方向の依存を記述しようとすると、循環参照としてコンパイルエラーが出るため、意図しない依存の混入を防げます。

循環参照の解消

当初、コアとゲームロジック間で循環参照が発生しました。コアがゲームロジック側のクラスを参照しつつ、ゲームロジックもコアのクラスを参照していたため。これをインターフェース抽出で解消しました。

具体的には、ゲームロジック側で定義していた状態管理クラスへの参照を、コア側でIGameStateProviderインターフェースとして定義し直しました。ゲームロジック側でこのインターフェースを実装し、コアへ注入する形に変更することで、一方向の依存に整理。

検証結果

asmdef分離後、リコンパイル時間の体感的な改善が見られました。定量的な計測は実施していませんが、開発者からは「エディタでの待機時間が明らかに減った」という声が上がっています。

テスト実行の独立性も向上。コアのテストを実行する際、UIやゲームロジックのアセンブリがロードされなくなり、テストの起動が軽量化しています。また、Unity編集画面でアセンブリの依存グラフが可視化されるため、コードレビュー時に「この変更はどのアセンブリに影響するか」を判断しやすくなりました。

現状の制約と今後の課題

一部の古いコードはまだasmdefへ移行できておらず、段階的に整理を進める必要があります。また、asmdefの粒度については、今後の運用で調整が必要。過剰に細分化すると管理コストが増すため、機能の変更頻度と凝集度を見ながら適切な境界を探っていきます。

まとめ

Assembly Definitionによる依存関係の整理と、インターフェースを用いた依存の逆転によって、コアモジュールの疎結合化を実現しました。リコンパイル時間の短縮とテスト容易性の向上を両立させ、開発体験の改善につながっています。

副次的な効果として、新規メンバーがコードベースを理解する際の助けにもなりました。アセンブリの境界が明示されることで、「どのモジュールがどこに依存しているか」が一目で分かり、影響範囲の把握が容易に。

中規模以上のUnityプロジェクトで同様の課題に直面している方の参考になれば幸いです。