はじめに
この記事におけるソースコードはすべて Public Domain です。
また本記事において利用しているUnityバージョンは2019.2です。
Assembly Definitionとは何か
Assembly Definitionという機能をご存知でしょうか。
これは「C#のビルドファイル(アセンブリ)を分割して出力する」ことができる機能であり、Unity 2017.3で追加されました。
特にライブラリやC#アセットを公開することがある人は絶対に覚えておくべき機能です。
「Assembly Definitionよくわからん」という状態でもプログラムは一応作成できますが、プロならば絶対に覚えておくべき機能です。
なお、Assembly Definition自体はUnityの機能の名前です。
実際にこの機能を有効化するためにユーザが定義するファイルのことをAssembly Definition Filesと呼びます。
(長いので略してadfと呼びます)。
Assembly Definitionを利用した場合のUnityの挙動
Assembly Definitionを利用した場合、アセンブリが分割されてコンパイル・ビルドされることになります。
そのためAssembly Definitionを利用するときと利用しないときとでは、コーディングの工程が大きく変化します。
そのためAssembly Definitionを利用する場合はその挙動をしっかり把握する必要があります。
1. csprojが分解される
adfを定義した場合、その定義ごとに別れてcsprojファイルが生成されることになります。
このとき、adfを定義していない従来のアセンブリはすべてAssembly-CSharpに収められます。
2. 差分ビルドされる
ソースコードに修正があった場合、該当するアセンブリのみがコンパイルされます。
そのため全体でコンパイル時間が短くなります。
3. 参照関係を明示的に設定する必要がある
Assembly Definitionを有効化するとアセンブリが分割されます。
そのため「どのアセンブリが、他のどのアセンブリに依存しているか」を手動で設定する必要があります。
4. Assembly-CSharpの扱いが特殊になる
Assembly-CSharp はadfを定義していないスクリプトがすべて押し込められる、いわゆる「全部入り」のアセンブリです。最初はすべてのスクリプトがこのアセンブリに入った状態となります。
つまり、adfを定義するということは、「Assembly-CSharpアセンブリからモジュールを抜き出して分割していく」と同義になります。
さて、このAssembly-CSharpですが、参照関係が特殊となっています。
- 他の
adfで分割されたアセンブリへは、Assembly-CSharpからすべて参照可能 - 逆に他のアセンブリから、
Assembly-CSharpへは絶対にアクセスできない
Assembly Definitionを利用しているときはこの性質が問題となることがあるため、この挙動はしっかりと把握しておきましょう。
Assembly Definition Filesの定義方法
Assembly Definition Filesの作成
adfはProject Viewにおいて、定義したいディレクトリ上で右クリックメニューから作成することができます。

adfが配置された時点でそのディレクトリ以下のcsprojファイルが分割され、アセンブリも別に生成されます。

/Library/ScriptAssembliesを覗くとBanana.dllに分割されている。
Assembly Definition Filesの設定
単にdllに分割するだけなら、adfを作成するだけで完了です。
より突っ込んだ設定、たとえば次の設定が行いたい場合はInspector Viewにて設定することができます。
- このアセンブリ内でのみ
unsafeコードの利用を許可したい - 他の
adfによって定義されたアセンブリに依存したい - 特定のプラットフォームでのみ有効化したい
- Editor拡張/Testコードだと設定したい
General
- Allow 'unsafe' Code : このアセンブリ内でunsafeコードを許可するか
-
Auto Referenced : 本体側のアセンブリ(
Assembly-CSharp)から参照するか -
Override References : すでにコンパイル済みの
dllを参照に追加するか
この「Auto Referenced」にチェックが入っている場合、毎回コンパイル対象になります。
Assembly-CSharpから直接参照しないものはオフにしておくとよいです。
Define Constraints
文字列を設定します。ここに設定された文字列が Player SettingsのScripting Define Symbolsに定義されていた場合のみビルドされます。

(RIPENED_BANANAが定義されていた場合のみこのアセンブリは有効となる。ちなみに意味は"完熟バナナ")
Assembly Definition References
他のadfによって定義されたアセンブリを参照する場合に設定します。
超重要。
たとえばJuice MixerはBananaとMilkに依存している場合は、Assembly Definition Referencesでそれぞれに参照を追加しなければいけません。
Platforms
どのプラットフォームで有効化するか設定します。
/Editorにadfを定義した場合はEditorにチェックを入れる必要があります。
(Editor Testの場合も同様)
(エディタ拡張などはEditorにチェックをいれてアセンブリを別ける必要がある)
Assembly Definition Filesのディレクトリ構造
adfはディレクトリをネストして定義しても問題はありません。
その場合も「ディレクトリ単位」でアセンブリが分割されビルドされます。
Assembly Definitionを利用するメリット・デメリット
Assembly Definitionは基本的には定義した方がメリットは大きいのですが、一応デメリットも存在します。
メリット・デメリットを把握した上で実際に利用するかどうかを決めるとよいでしょう。
メリット
-
差分コンパイルになるためEditor上でのコンパイル時間が短くなる (
Auto Referencedを適切にOFFにしておく必要あり) internalやprivate protectedなどのアクセスレベルが活用できるunsafeコードを有効化する範囲をアセンブリ単位で限定できる- モジュールの依存関係を強制できる
- ライブラリ同士の干渉を最小限に抑えてプロジェクトに追加できる
- 対象プラットフォームごとにアセンブリレベルで別けてビルドができる
デメリット
-
Asset Bundleと併用した場合に挙動がややこしいことになる - アセンブリをまたいで
partialクラスが定義できない -
staticを使っていたときに分割しにくい - たまに謎のコンパイルエラーが出て動かなくなる
Assembly Definitionを利用するべきシチュエーション
次のようなシチュエーションでは必ずAssembly Definitionを利用するべきです。
ライブラリやアセットを公開する場合
ライブラリや、スクリプトを含んだアセットを公開する場合はadfを定義してアセンブリを分割するべきです。
むしろやってくださいおねがいします。
理由としては「adfが定義されていないライブラリはAssembly-CSharpアセンブリに入ってしまう」からです。
これは自身のプロジェクトがAssembly Definitionを活用していた場合に非常に面倒な問題になります。
場合を分けて説明します。
セーフ:Assembly Definitionを一切利用していない場合
自身のプロジェクトも、導入した外部ライブラリも、どれもadfを定義されていない場合です。
この場合は問題ありません。
セーフ:全員がAssembly Definitionを利用している場合
自身のプロジェクトと導入した外部ライブラリ、その両方がadfを定義していた場合です。
この場合はアセンブリ同士の参照を正しく設定すれば問題ありません。
アウト:自身のプロジェクトのみadfを定義していた場合
この場合はアウトです。
自身のプロジェクトはadfを定義しているが、導入したライブラリには定義されていない場合。
この場合はライブラリがAssembly-CSharpに入り込んでしまうため、自身のモジュールからアクセスができないためエラーになってしまいます。
こうなると自身でライブラリにadfを定義してアセンブリを分割する必要がでてきます。
そしてライブラリ側の構造が歪でadfをキレイに定義できない、定義してもstaticやpartialを乱用していてエラーが出まくる…、みたいな地獄な状況になる場合もあります。
なのでもし、これからライブラリやモジュールを作って公開するつもりがある人は必ずAssembly Definitionを意識した作りにしてください。
マジでお願いします。マジで。
アーキテクチャに沿った開発を行う場合
クリーンアーキテクチャなど、レイヤ間での依存関係をしっかり定義したい場合はAssembly Definitionがかなり有用です。
adfでモジュール間の依存を定義することで、「Domainから外への参照を禁止する」といったことがプロジェクト設定で制御することが可能になります。
合わせて覚えておくべき技
Assembly Definitionを利用する場合、覚えておくと開発効率の向上につながるテクニックがいくつかあります。
Editor上でコンパイルエラーが出た場合の対策
Assembly Definitionを使っていると、コードに変更がなくてもコンパイルエラーが出てしまうことがあります。本当によく起きます。
これはエディタがキャッシュしているアセンブリがおかしくなってしまったことが原因の場合が多いです。
そのため次の手順を行うと復旧することがあります。
-
\Library\ScriptAssembliesを削除してみる - (↑で直らないなら)Unity Editorを再起動する
- (↑で直らないなら)該当するAssetのReimportを行う
- (↑で直らないなら)
\Libraryを消してUnity Editorを再起動する
アセンブリ単位でのアクセスレベルを利用する
C#のアクセス修飾子にはアセンブリ単位でアクセスを制御するものがあります。
外部公開するライブラリを作るときなどに便利に使えるためお勧めです。
詳しくは岩永さんのサイトを見てもらうとよいのですが、一応解説します。
アクセスレベル
C#のアクセス修飾子には次のものがあります。
| レベル | 説明 | 備考 |
|---|---|---|
| public | どこからでもアクセス可能 | ガバガバ |
| protected | クラス内とその派生クラスからのみアクセス可能 | 継承するときに使う |
| internal | 同一アセンブリのクラス内からのみアクセス可能 | アセンブリ内からはpublic、外からはprivateに相当 |
| protected internal | 同一アセンブリのクラス内 OR 派生クラスからアクセス可能 | |
| private protected | 同一アセンブリのクラス内 AND 派生クラスからアクセス可能 | C# 7.2以降 |
| private | クラス内からのみアクセス可能 | 一番厳しい |
とくにinternal、protected internal、private protectedはAssembly Definitionを利用している場合のみに利用できるアクセスレベルです。
これらを活用することでライブラリの実装が少し楽になったりします。
利用例1:管理用のメソッドの定義
internalを利用することで、「同一アセンブリ内からのみ呼び出せるメソッド」を定義することができます。
これはライブラリやフレームワークを作成する場合に、オブジェクトの管理メソッドなどを定義するときに活用することができます。
/// <summary>
/// 敵
/// </summary>
public class Enemy
{
/// <summary>
/// ライブラリ外から呼びだせるのはこっち
/// </summary>
public void Destroy()
{
/*
* なんか実装
* いろいろ処理が走ったり走らなかったり
*/
}
/// <summary>
/// ライブラリ内部でリソース管理用に呼び出すのはこっち
/// ライブラリ内からのみコール可能
/// </summary>
internal void ForceDestroy()
{
/*
* なんか実装
* Force()より単純化されていたり、複雑だったり…
*/
}
}
利用例2:ファクトリの強制
クラスや構造体のコンストラクタのアクセスレベルをinternalにすることで、ファクトリ経由したインスタンス化をライブラリ外に強制することができます。
/// <summary>
/// User
/// </summary>
public class User
{
/// <summary>
/// Userの識別子
/// </summary>
public int Id { get; }
/// <summary>
/// コンストラクタをinternalで定義する
/// </summary>
internal User(int id)
{
Id = id;
}
}
public class UserFactory
{
/// <summary>
/// UserIdを発行してもらうのに使う
/// </summary>
private readonly UserClient _userClient;
public UserFactory(UserClient userClient)
{
_userClient = userClient;
}
/// <summary>
/// Userのファクトリメソッド
/// </summary>
public async Task<User> CreateUserAsync()
{
// Idをサーバから発行してもらって使う、など
var newId = await _userClient.CreateUserIdAsync();
return new User(newId);
}
}
using UserAssembly;
using UnityEngine;
public class Sample : MonoBehaviour
{
// Inject from DI Container
[Inject] private UserFactory _userFactory;
public async void Start()
{
/*
* 「Cannot access internal constructor 'User' here」
* とエラーが出てインスタンス化できない
*/
// var user = new User(123);
/*
* ファクトリ経由ならインスタンス化できる
*/
var user = await _userFactory.CreateUserAsync();
}
}
internalアクセスを外部アセンブリに公開する
たとえばライブラリを作っているときにこのテストコードを用意した場合、ライブラリ本体とテストコードは別のアセンブリに別れてしまいます。
そうなってくると、internalなどのアクセスレベルにテストコードからはアクセスができない、といった状況になってしまいます。
この問題を解決する方法としてはAssemblyInfo.csを定義すればOKです。
これについてはすでに別の記事で解説しているため、そちらを参考にしてください。
まとめ
-
Assembly Definitionはプログラマなら必須で覚えておくべき機能 - ライブラリやアセットを公開する場合は必ず
adfを定義しよう -
internalなどのアクセスレベルを活用しよう












