Managing a large Unity project can be challenging. As projects grow, the effort required to manage dependencies and keep script files organized increases dramatically. Iteration time also suffers, as making a small change to a single script results in the entire project being recompiled.
In this post, we’ll cover one way you can better manage complexity in your Unity projects: assembly definitions.
What are assembly definitions in Unity?
By default, Unity compiles all runtime scripts into a single assembly called
Assembly-CSharp.dll. While this is a convenient starting point for small projects, assembly definitions allow you to tell Unity to organize your code into multiple smaller assemblies. Splitting a codebase into multiple assemblies and explicitly defining the dependencies between them allows Unity to skip recompiling portions of the code that aren’t affected by a change. For instance, if assembly A depends on assembly B, and assembly B depends on assembly C, only assemblies A and B need to be recompiled when the code in assembly B changes.
What are the benefits of assembly definitions in Unity?
Organizing your code into smaller assemblies allows for strict enforcement of rules around encapsulation and modularity. Code in a given assembly can only access the types and functions in that assembly and any assemblies it explicitly depends on. In the example above, assembly C would have no access to anything from assemblies A or B. Sharing libraries between projects also becomes easier. Since the dependencies of the assembly are explicit, you can be sure that code from that assembly will work in another project that contains its dependencies.
How can you start using assembly definitions in Unity?
You can easily create an assembly definition asset in the project window of the Unity editor. All scripts within the same directory or a subdirectory of the assembly definition will be grouped into that assembly. Options for configuring the assembly, such as the assembly name and its dependencies, can be set in the inspector for the assembly definition asset. If you’ve imported packages like the Embrace SDK from Unity’s package manager, the code in those packages will also be divided into assemblies. In order to reference code from those packages in your own assembly, add them as references in the inspector for your assembly definition.
What are some best practices for organizing assembly definitions?
Here are a few ways to get the most out of your assembly definitions.
Create logical groups
It’s best to create logical groups when setting up a project with assembly definitions.. Types which are part of the same overall gameplay system, are tightly coupled, or are likely to be modified together should generally be grouped into a single assembly. For instance, it might make sense to create a single assembly definition for your gameplay code, and a separate one for code related to an in-app purchase system.
It can be helpful to think of assemblies as top-level namespaces. If you’d naturally put two types into the same namespace, there is a decent chance they belong in the same assembly. In fact, each assembly can be configured with a root namespace in the assembly definition inspector.
Using logical groups makes it relatively straightforward to manage dependencies between your assemblies. Circular dependencies are not supported, so code with mutual references should either be grouped into the same assembly or should both depend on a third, shared assembly.
Group platform-specific implementations
Assembly definitions are also useful for grouping platform-specific implementations. You can exclude specific assemblies from compilation for certain platforms so they will not cause conflicts or unnecessary build size on platforms where they aren’t needed. This is particularly useful when implementing things like C# wrappers around native Android or iOS libraries which would not build on the opposite platform, or stripping out touchscreen input systems on platforms which don’t support them.
Avoid the urge to overengineer
In terms of compilation time, adding more and more smaller assemblies gives diminishing returns. The complexity of managing dependencies between assemblies also increases with the number of assemblies in a project.
What are some advanced use cases for assembly definitions?
Unity provides several advanced conditions when working with assembly definitions, such as only including an assembly in certain versions of Unity, or if a certain symbol is defined, or if some package manager is imported.
Defining symbols per-assembly
Assembly definitions also allow for symbols to be defined per-assembly, rather than just project-wide.
For instance, the Embrace SDK uses an assembly weaver to inject code to automatically capture network request logs. If the symbol to enable this feature is defined in project settings, the SDK processes every line of code from every assembly in the project at build time looking for network requests. In most projects, this leads to a fair amount of wasted time as the weaver looks through assemblies that don’t contain any network requests. If we knew that only a certain set of assemblies contained code that communicated with the server, defining the symbol on those assemblies directly using the assembly definition inspector would tell the weaver to skip all other assemblies, which could significantly improve compilation and build times.
Working with shared libraries
These types of controls are especially useful when implementing shared libraries that may be used in different projects, contain different versions of a given dependency, or rely on different versions of Unity. For example, imagine you’re working on a save-game system that you intend to use in a number of projects. Your save-game system uses JSON to serialize data, but each of your projects uses a different JSON library. Rather than forcing all of those projects to conform to a single JSON dependency, you can use assembly definitions to link to the appropriate library in each project automatically based on the defined symbols.
Leveraging assembly definition references
Assembly definition references are useful if your Unity projects are organized in a way that makes it difficult to group code in the same assembly together in the same folder. Think of assembly definition references like pointers to an assembly. They’re created through the asset menu just like assembly definitions, but rather than defining a new assembly, they point to an existing one. Any code that exists in a folder containing an assembly definition reference will be built into the assembly that the reference points to.
Key takeaways about assembly definitions
Assembly definitions are a helpful way to manage complexity in Unity projects. They allow you to enforce encapsulation and modularity in your code, strip out unnecessary code during compilation, and reduce compile and build times. However, as with any optimization, it’s important to avoid overengineering. Start by creating assembly definitions based on logical groups in your code, and look for opportunities to refactor when working with shared libraries, platform-specific code, or symbols that are only relevant in specific parts of your code.
These tips will help provide your team a much better developer experience when working in Unity. If you’re building Unity mobile games, another challenge is maintaining good user experiences at scale. Embrace allows developers to create amazing mobile experiences with 100% unsampled data across every user journey. Get started for free today.