1. はじめに
この記事では、Assembly Definition Reference の概要とどんな場面のときに利用するべきかを、2つの具体的なユースケースとともにご紹介します。
想定読者
- Unityにおけるアセンブリ分割(Assembly Definition)の基礎を理解している方
-
.asmrefをいつ・どのように使えば効果的なのか、具体的なユースケースを知りたい方 - Unityのコアモジュールや、OSSなどの外部パッケージの
internalなクラスやメソッドを、リフレクションなしで拡張・ハックしたい方
2. Assembly Definition Reference(.asmref)とは?
まずは前提知識として、ベースとなる Assembly Definition(.asmdef) の役割と制約について整理し、それを解決する Assembly Definition Reference(.asmref) の仕組みを解説します。
1. Assembly Definition(.asmdef)の役割
- アセンブリの定義・作成 :スクリプトをグループ化し、独立した個別のアセンブリ(.dll)としてコンパイルします
- 導入のメリット:コード変更時のコンパイル時間を大幅に短縮し、コード間の依存関係を綺麗に整理・制限できます
2. .asmdef が持つ強力な制約と課題
- フォルダ配下の強制アセンブリ化:.asmdef は自身が配置されたフォルダ配下の全スクリプトを強制的に単一のアセンブリにまとめます
- 発生する課題:アセンブリ分割のために物理的なフォルダ構成を強制的に合わせる必要があり、ディレクトリ設計の自由度が犠牲になっていました
3. Assembly Definition Reference(.asmref)による解決
この物理フォルダの制約を解決するために登場したのが .asmref です。
新しいアセンブリを作るのではなく、「自身のフォルダ内のスクリプトを、既存の別アセンブリに追加・紐付ける」 役割を持ちます。
対象のフォルダ内に .asmref を配置し、インスペクターで参照先の .asmdef を指定すると、物理配置はそのままで、対象スクリプトを既存アセンブリの一部としてコンパイルすることができます。
3. ユースケース1:機能別のフォルダ構成(垂直分割)とアーキテクチャのレイヤー設計を両立したいとき
課題
オニオンアーキテクチャなどのレイヤー設計(依存関係のルール)を保とうとする場合、
「フォルダ構成の綺麗さ」と「アセンブリ管理の簡潔さ」がどうしても両立できない
という構造上のジレンマが生じていました。
具体的には、次の2つの選択肢のどちらかを選ぶしかありませんでした。
アプローチ1:物理配置をアセンブリの境界に合わせる(フォルダの水平分割)
アセンブリの数を最小限に抑えるためには、水平分割をする手段が用いられます。
水平分割の場合、特定の機能を修正・確認したいとき、関連するコードが各レイヤーフォルダに散らばってしまいます。
Assets/
├── Domain/
│ ├── FeatureA/
│ └── FeatureB/
├── Application/
│ ├── FeatureA/
│ └── FeatureB/
├── Presentation/
│ ├── FeatureA/
│ └── FeatureB/
└── Infrastructure/
├── FeatureA/
└── FeatureB/
アプローチ2:機能ごとに個別にアセンブリを定義する(フォルダの垂直分割)
機能ごとのカプセル化を優先し、以下のように機能内のレイヤーすべてに個別の .asmdef を置く方法もあります。しかし、これでは機能が増えるたびに、個別にアセンブリを定義しなくてはなりません。結果として、プロジェクト全体のアセンブリ数とそれらの依存関係の管理が複雑になってしまいます。
Assets/
├── FeatureA/
│ ├── Domain/
│ ├── Application/
│ ├── Presentation/
│ └── Infrastructure/
└── FeatureB/
├── Domain/
├── Application/
├── Presentation/
└── Infrastructure/
解決
この「アセンブリ管理は楽だが機能が散らばる(水平分割)」か、「機能はまとまるがアセンブリ管理が破綻する(垂直分割)」か、という二者択一の課題を完全に解決してくれるのが .asmref です。
以下のように asmdefは別のフォルダで定義を行い、各機能のフォルダには asmrefを追加して、該当するレイヤーのアセンブリを参照するようにします。
Assets/
├── Assemblies/
│ ├── Domain/
│ │ └── Domain.asmdef <-- ドメイン層のアセンブリ実体
│ ├── Application/
│ │ └── Application.asmdef <-- アプリケーション層のアセンブリ実体
│ ├── Presentation/
│ │ └── Presentation.asmdef <-- プレゼンテーション層のアセンブリ実体
│ └── Infrastructure/
│ └── Infrastructure.asmdef <-- インフラストラクチャ層のアセンブリ実体
├── FeatureA/
│ ├── Domain/
│ │ └── Domain.asmref <-- 参照先:「Domain.asmdef」
│ ├── Application/
│ │ └── Application.asmref <-- 参照先:「Application.asmdef」
│ ├── Presentation/
│ │ └── Presentation.asmref <-- 参照先:「Presentation.asmdef」
│ └── Infrastructure/
│ └── Infrastructure.asmref <-- 参照先:「Infrastructure.asmdef」
└── FeatureB/
├── Domain/
│ └── Domain.asmref <-- 参照先:「Domain.asmdef」
├── Application/
│ └── Application.asmref <-- 参照先:「Application.asmdef」
├── Presentation/
│ └── Presentation.asmref <-- 参照先:「Presentation.asmdef」
└── Infrastructure/
└── Infrastructure.asmref <-- 参照先:「Infrastructure.asmdef」
この構成を採用するメリットは以下の通りです。
- ディレクトリ制約からの解放:機能単位でフォルダがまとまるため、実装の追加や確認がスムーズになります
-
アセンブリ保守コストの最小化:アセンブリ実体
.asmdefはレイヤー数(4つ)に固定されます。新機能の追加時も、各フォルダに.asmrefを置くだけで自動的にアセンブリ構造へ組み込めます
4. ユースケース2:変更不可な PackageCache 内のinternal クラスを拡張したいとき
課題
通常、C#において別アセンブリの internal クラスにアクセスしたい場合は、以下のように「フレンドアセンブリ(InternalsVisibleTo)」に指定することでアクセス可能になります。
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("MyExtension.Editor")]
この仕組みを用いてテストや拡張機能を作るケースが多々あります。
しかし、UnityのコアモジュールやUnity Package Manager経由でインストールされた外部パッケージは、Library/PackageCache/ に実体が配置されます。これらは読み込み専用として管理されているため、パッケージ内のソースコードを直接書き換えることはできません。そのため、通常であればフレンドアセンブリで解決できるような場面でも、internal 要素に対してリフレクションを使わざるを得ませんでした。
解決
ここで .asmref を活用し、「InternalsVisibleTo 属性だけを、ターゲットパッケージの内部に滑り込ませる」 というアプローチをとります。
以下のように、フォルダ・ファイルの配置をします。ポイントは、.asmref の影響範囲を限定させるために、AssemblyInfo フォルダを独立させる点です。
Assets/
└── MyPackageExtension/
├── MyExtension.asmdef <-- 独自アセンブリ
├── MyExtensionScript.cs
└── AssemblyInfo/ <-- .asmref専用にフォルダを分離
├── TargetPackage.asmref <-- 参照先:「TargetPackage.Editor」
└── AssemblyInfo.cs
.asmref の効果により、AssemblyInfo.cs は TargetPackage.Editor アセンブリの一部として扱われます。
結果として、InternalsVisibleTo の宣言がアセンブリ内から行われたものとして機能し、MyExtension からの internal アクセスが可能になります。
実装サンプル
独自アセンブリ「MyExtension」からの internal アクセスを許可します。
using System.Runtime.CompilerServices;
// 独自アセンブリ「MyExtension.Editor」からの internal アクセスを許可させる
[assembly: InternalsVisibleTo("MyExtension")]
MyExtension.asmdef に属するスクリプトです。上記のインジェクションにより、TargetPackage.Editor の internal を自由に利用できます。
using UnityEditor;
using TargetPackage.Editor; // ターゲットアセンブリの名前空間
namespace MyExtension.Editor
{
public static class MyExtensionScript
{
public static void DoHack()
{
// 本来は別アセンブリからアクセスできない internal クラスだが、
// 後付けした InternalsVisibleTo によって、アクセス可能になる。
var utility = new InternalPackageUtility();
utility.ExecuteInternalProcess();
}
}
}
このアプローチのメリットは以下の通りです。
-
リフレクションが不要: 完全に型安全な状態で
internalなクラスやメソッドを呼び出せるため、エディタ拡張やハックの安定性が向上します - パッケージコードのローカル管理が不要: パッケージをローカルにコピーして直接書き換える必要がないため、パッケージのアップデート追従が容易になります
-
影響範囲の最小化: 専用の
AssemblyInfoフォルダを作成して.asmrefを配置しているため、プロジェクト内の他のコードに不要なアセンブリ汚染を引き起こしません
5. まとめ
本記事では、Unityの Assembly Definition Reference(.asmref) の仕組みと、それを活かした2つのユースケースをご紹介しました。
- 機能別のフォルダ構成とレイヤー分割の両立: 物理的なディレクトリ構造に縛られず、アセンブリ割当てを柔軟に変更することで、綺麗にカプセル化されたアーキテクチャを実現します
-
PackageCache内の
internalクラスの拡張: 読み込み専用の外部パッケージに対し、リフレクションなしで安全にinternalメンバーへアクセスするハックを可能にします
Assembly Definition Reference という機能があることは知っているけれど、具体的にいつ、どう使えば効果的なのか分からない...という迷いを解消し、実践的な使いどころを理解してもらうための助けになれば幸いです。
6. 参考文献