In Unity version 2017.3 we got introduced to Assembly Definition Files that showed a lot of potential to solve some code related problems in big Unity projects. The feature has been developed and improved in following versions and I want to stress that all features shown in this post are based on the Unity 2020.2 version. Most features are also available in older version. I will also refer to Assembly Definition Files with the short form ADF.
In my opinion ADFs solve many problems in Unity. Three of them are most important to me:
- Compile time
- Clearer dependencies
- Easier extraction of modules
Let us look at a short overview on how ADFs work and then dig into some tricks and details you should know about before migrating your project to use ADFs.
Predefined Assemblies vs. ADFs
The name “Assembly-CSharp.dll” ring a bell in your head? You probably have seen it when Unity generates a CSharp project or compiles your code in the finished game Player build. These assemblies are predefined and contain the compiled result from the scripts in Unity that are not in ADFs. Before ADF this was all of your code.
So everything lives in one DLL? Not quite. Actually there are 4 main predefined assemblies:
Depending under which directory path a script file has been saved, Unity decides in which of the predefined assemblies the code will be compiled.
Here you can see the most common directories with “magic” names that indicate the compiled assemblies:
Assembly Definition Files break this pattern and do not rely on special names. You just throw a special kind of Asset (via Create→Assembly Definition) in a directory and that tells Unity to compile every script file in this directory and sub directories without ADF to an own DLL file. So now you can decide which code gets compiled into which assembly.
They work very much like .csproj that you create in Rider or Visual Studio. Every script file in the .csproj gets compiled to an own assembly.
Getting the hang of dependencies when using a lot of ADFs can be a bit tricky. In general you can remember that all code that is included in a ADF can not access code that is not in an ADF. It makes sense to have all your code in ADF so all code could in theory reach all other code when explicitly set.
Especially with old plugins from the AssetStore or old libraries developed before ADF you often have to convert them to using ADF so you can use the plugin/library from within your game code. I will show an example of this later.
On the right you can see an info graphic depicting the dependencies between the “Game” ADFs and the predefined assemblies. As you can see, the ADF can not access the predefined assemblies. Only the other way around is possible.
When clicking on a ADF asset in Unity you get an inspector with a wide array of properties. I want to bring some examples and extended documentation to them.
I will highlight some of the properties that I think are worth mentioning. Obvious ones are omitted.
|Name||Normally Unity just uses the file name to expose in code. This name is a bit special because it is used to set the name of the DLL. The file name in this case does not matter.|
|Auto Referenced||Check the Dependencies section. If set to
|No Engine References||Control if default assemblies from Unity should be included. This includes UnityEngine.dll, UnityEditor.dll, etc.This is a great option if you have code that should also compile outside of Unity. For example, shared code between Unity and a server that does not run on Unity basis. Code using UnityEngine would throw an compile error.|
|Override References||If enabled you can control which assembly files in the project should be referenced in the ADF. By default all .dll files which are active are referenced|
|Root Namespace||To hint in the .csproj what root namespace your IDE should use although the code directory path does not include the root namespace|
|Define constraints||You can set defines that have to be
|Assembly Definition References||This section allows you to tell Unity what other ADF are needed to compile this ADF you are editing. This features helps Unity to improve compile time since it now understands which parts of the code depend on others.|
|Platforms||This should be quite easy to understand. You just specify the platform on which this ADF should be active. All the in this ADF will be only compiled with the given platform settings being true.|
|Version Defines||Very rarely used but can be helpful. Especially if you are developing a Plugin/Library that uses Unity packages as dependencies. Let’s say you are creating UI Helper Tools. Here you can specify different behaviour based on the version of a Unity Package. So for example you could have different behaviour based on “com.unity.ugui” version 1 and version 1.2. Check the Unity documentation HERE for examples.|
You can also check the Unity Documentation which has been improved a lot since the first release HERE.
Usually all Unity serialised files are in the YAML format. ADFs are in the JSON format. I think this decision was taken for maximum compatibility between IDEs that have Unity integrations (e.g. Rider, Visual Studio). So if you want to edit the ADF manually you can do so with every standard JSON tool.
Assembly Definition References
There is handy tool Unity gave to developers when migrating existing code libraries to using ADF: Assembly Definition References ( ADR ).
Basically a ADR is a copy of an existing ADF in your project that inherits all properties/settings. So when is this useful?
Let us look at an example. I prepared a little Plugin with some scripts:
Pretty standard plugin. We have a
Core directory with some base stuff. We have some
Editor directories for Unity Editor code and two features.
Let us convert it to ADF!
Create a ADF at the base of the Plugin.
Now all scripts are in one ADF.
Everything looks fine. Until we build a
Player for our users. Unity will complain about compile errors because it can’t find the UnityEditor namespace. Remember that ADFs disable ALL magic names that Unity has provided before. This includes the
Editor directory! Unity is now trying to compile our editor only code which does not work outside of the editor. To fix this we have to create another ADF for the Editor scripts.
We give this ADF the settings to only include the
Editor platform. This way Unity ignores it when we build a
Still compile errors! We have only taken care of one Editor directory. We also have to mark the code in
Feature2 to be Editor only. Now comes the critical role of ADRs. We create references in the other Editor directories to apply the same settings to the other Editor code.
In the ADRs we reference the TrollPlugin.Editor ADF and this makes our build green again.
We could have just duplicated the ADF and be done with it.
The two key points why you don’t want to copy are:
- Every ADF name in the import settings must be unique in the project. If you copy the ADF you have to always give it a new name. And that results in multiple .csproj files created. This fragments your project setup.
- If you want to change a setting (e.g. platforms or defines) you have to change it on all duplicated ADFs.
Compile Time Test
To stress the compiler a bit I imported the JSON.NET source code 5 times into the project. To be able to have multiple versions in the code I changed the namespaces in each copy to be unique.
Every JSON copy gets their own ADF.
The time is measured by an editor script that checks in the
EditorApplication.update callback if
EditorApplication.isCompiling changed to false.
MacBook Pro 2018, 2.6 GHz 6-Core i7, 2400 MHz DDR4
|With ADF||Re-import script file||3.87s|
|No ADF||Re-import script file||5.94s|
In the test results we can already see a big improvement for using ADF when changing single files that trigger a recompile.
I guess the compiler does some optimisation because we are basically compiling the same code 5 times with only different namespaces. So the real world project gain could be higher.
You can find the the test project attached to this post.
Using Assembly Definition Files certainly has some overhead in terms of project setup and maintenance. But I think the advantages outweigh the costs! Having structured dependencies allows a much clearer code structure where not everything can access everything. Suddenly your Core/Base code is accessing the Features.
You start a new project and you say: “Hey, I just want to extract that InventorySystem out of my old game.” You then realise that the Inventory System is actually accessing the
PlayerController and also the
CameraModel? Clearer rules which code should depend on other code can help here.
Additionally you are getting features like better compile time and conditional code compile without placing #if all over the code.