For non-trivial Unity projects, you should be using DLL’s to modularise your code. They massively speed up development time and code re-use. But …
How do you ship a DLL that simultaneously works in different, incompatible, versions of Unity?
Unity and backwards compatibility
In any given version of Unity a small amount (5% or less) of the changes break backwards or forwards compatibility.
This is a good thing. Typically an API is re-designed, fixing long-term problems (e.g. the Particle System API was rewritten in Unity 5.4 to add the missing parts that for years could only be accessed through the Editor).
But how do you support both versions of Unity at once? In game development, this is only an issue during development, and only when changing Unity versions: you need to test both new and old before moving your project to new. But it’s a one-off risk/cost.
Making plugins for the Asset Store is a much bigger problem: you need to simultaneously support many versions of Unity (all the ones that your customers are using).
Unity’s Conditional Compilation
Unity understood this, and added a system of #define’s that are automatically created and maintained by Unity. For instance, we can write code like this:
#if UNITY_5_6 public static string builtForUnityVersion = "5.6"; #elif UNITY_5_5_OR_NEWER public static string builtForUnityVersion = "5.5 or newer"; #else public static string builtForUnityVersion = "5.4.x or older"; #end
The problem: MonoDevelop is not fully integrated with Unity
Those #define’s (e.g. UNITY_5_6) only exist inside the Unity Editor. So the above code breaks as soon as you move the Unity script into a DLL. I was surprised none of this was pre-integrated, but read on … it’s easy enough to fix.
And, worse – even if you hack around this, the Mono compiler will only include the code for one version.
As of 2017, so far as I can tell … Unity has an incomplete system of conditional compilation: it’s only done in-editor, and it doesn’t play nicely with DLLs … unless you do the manual setup listed in this blog post?
Solution: Project changes
There’s two parts. Firstly we want to extend Unity’s conditional compilation support to the MonoDevelop editor.
Secondly, we want to ship so-called “Fat” DLL’s that simultaneously contain the different versions of the sourcecode for different versions of Unity, and we want Unity to correctly discard / use whichever parts fit the current version of Unity that is used to build your game.
Making MonoDevelop support Unity Conditional Compiles
This was a lot harder than it should be – MonoDevelop’s interface sucks. Even after doing it successfully, trying to re-do it later on a different project it took me a while to figure it out. So, to make sure I can do it easily next time, here’s a guide…
Step 1: Define workspace-level settings
MonoDevelop has both “project” level and “workspace” level settings. Unlike real, production-quality IDEs … MonoDevelop doesn’t show you the difference between these, and won’t allow you to see one while editing the other. This is IMHO absurd (and is just one more reason we all hate MonoDevelop so much :)).
For contrast, here’s how Apple’s Xcode does it. This is a very good UI/UX for editing overridable settings:
On the right hand side you have the workspace settings, then moving left you have the project’s overrides, then on the extreme left you have the “actual values resulting from applying all overrides”.
For bonus points, Apple hilights in green exactly where overrides are happening, so you can see at a glance both “what” the values are and also “why”.
In MonoDevelop, you need to open your Workspace settings (pink icon), and create multiple Configurations. You will already have Debug and Release, but add something memorable. I currently only care about 5.4-or-newer and 5.3-or-older, so I made these:
Note that under “Build” on the left there are only two menu items – this is how you know it’s the Workspace you’re editing, not the Project!
Step 2: Clone the workspace settings into project settings
Stupid design decision in MonoDevelop: we cannot actually set those workspace settings – they are merely labels. But if we don’t create the labels in the workspace, MonoDevelop later craps-out and won’t build properly.
For each project in your workspace (with Unity development, you should always have two – one for your DLL and another for your Editor-scripts DLL), open the settings, go to Configurations, and re-create the Configurations you already created, using exactly the same names.
Step 3: Tell MonoDevelop to link workspace settings to project settings
Go back to the workspace, re-open the Configurations dialog, and click the second tab. This shows you whether MonoDevelop thinks your settings with same name in project should be shared with the workspace. If you used exactly identical Configuration names, it guesses correctly and all is well. If not, you can manually edit this screen and select which in workspace maps to which in project:
Step 4: Set the settings!
For each project in your workspace, you must manually set the setting. You cannot share them. You cannot set them at the workspace level – that would be useful and intelligent, and the obvious thing to do :).
First, make sure you’ve told MonoDevelop which configuration you’re editing (oh, this hurts – MonoDevelop makes it trivial to edit the “wrong” configuration, and tricky to edit the “right” one). If you’ve done everything correctly so far, the dropdown in top left of screen, immediately to the right of the PLAY button, should now have your Configurations as options. Select one (e.g. the “5.4 or newer” one).
Open the build settings (and repeat this for each project) and go to the Compiler menu:
…we need to change the field “Define symbols”. This has the same effect on the compiler as Unity’s built-in conditional compilation system.
So, for Unity 5.4, we need to change that to contain:
DEBUG; UNITY_5; UNITY_5_4; UNITY_5_4_OR_NEWER; UNITY_5_3_OR_NEWER; UNITY_5_2_OR_NEWER; UNITY_5_1_OR_NEWER; UNITY_5_0_OR_NEWER
Note that we’ve added a lot more than merely “UNITY_5_4”. If you’re using Unity API’s that changed (e.g fixed bugs) in minor versions, you’ll have to go even further and add “UNITY_5_4_2” (for example).
Technically you should also add UNITY_4_OR_NEWER etc – but at this point I’m fairly happy saying I no longer support 5.4.x (finally!).
…and repeat / re-do for each Configuration
- Select the other configuration (e.g. “Unity 5.3 or older”)
- Open the build settings, go to Compiler
- Overwrite the define of “DEBUG” with “DEBUG; UNITY_5_3” … etc
…and repeat AGAIN for each Project
i.e.: all the above, including doing each Configuration separately. Oh yes. It’s fun!
Testing it works
Finally, you should have a dropdown on top left that lets you switch, and MonoDevelop will LIVE update the syntax colouring to show that it’s honouring the different compiler settings.
Here’s the dropdown in one state:
…and here it is in a different state:
Using this to support different Unity versions
Ideally we’d now stitch this binaries together into a bigger, multi-version DLL. I didn’t bother, for two reasons:
- Unity has an proprietary system for version compatibility on Asset Store which are REQUIRED to use anyway
- I don’t trust MonoDevelop and Mono and Unity to fully collaborate on dynamic re-linking of a fat binary
It probably works, but the amount of fiddling around it took to get this far has me asking: is it worth optimizing this further? :)
So, instead, go the Unity way!
Shipping different incompatible DLL Versions in Unity
Unity requires you to make a separate Unity Project (complete copy of your game/source/plugin/demo scenes/etc) for each version of Unity that you want to appear on the Asset Store as “supported”.
If you don’t do this, Unity will block people with older versions from downloading/purchasing/upgrading your asset at all. So it’s not really optional.
All you need to do is:
- Open your MonoDevelop project
- Select the version of Unity you’re building for in dropdown
- Build All
- Open your Unity project (need a unique copy of project per Unity version, obviously, since Unity forces this)
- Drag/drop the 2x DLL’s (plain and Editor) into your project
- Upload to AssetStore (Unity will automatically use the version number of Unity Editor)
- Repeat the above steps for a different version of Unity
Et voila! You have easy-to-maintain, clean source code, shipping via easy-to-use DLLs, spanning multiple incompatible versions of Unity, and all enforced/checked/maintained automatically by the C# compiler.
Going further: supporting Windows and Mac users on the same project
This was good, but we can go further with custom config.