Automating Unity3D Builds with FAKE
So among the different types of games and apps we build at Hitcents, one particularly troublesome type to deal with is basically any game built with Unity. Unity builds can be painfully slow at times, so not automating them is a huge productivity killer.
Unity has released Unity Cloud Build, which is a nice solution, but it doesn’t fulfill our needs:
- Only iOS and Android are currently supported (we need Windows, too)
- Not much control over Unity player settings
- Our SCM we use for Unity, PlasticSCM, is not supported
- It could be tough to figure out what went wrong with a build — you can’t exactly RDP into the build agent and open Unity.
- Builds can a really long time… Like an hour and a half… Ugh.
We already have TeamCity setup, with a build agents running on a PC and a Mac for other apps. So setting up Unity builds ourselves, seemed like a great option. We also chose to use FAKE, as we get all the benefits of checking our build scripts into source control, etc. If you are looking for an intro on FAKE, check out my earlier blog post on it.
Invoking Unity from the Command Line
//NOTE: we are currently using Unity 4.6.3, so code in the BuildScript class will change slightly for different versions of Unity. Some of the features of IL2CPP and PlayerSettings.shortBundleVersion are not available prior to 4.6.3.
So how do Unity builds from the command line work exactly? A little weird. Basically there are a set of command line options for running Unity.exe documented here. For making a build, you invoke a specific method in one of your scripts that starts the build via Unity’s editor APIs. Generally you invoke a Unity build like this from command line on Windows:
Then you would need a class in your Unity project in an Editor folder such as this Android example:
This would use the default player settings you have declared for Android in your Unity project. The GetScenes method we’ll reuse throughout, it grabs the level settings you have saved in Unity.
So what are some gotchas with Unity builds?
- Creating a build with Unity is generally two steps: build the player, then build the native project that Unity spits out. Android is the only mobile platform that Unity spits out the app directly.
- On Windows output is not logged to the console, but to the default log file location. This is certainly annoying for automated builds, since TeamCity doesn’t have a great way to capture this log. (Maybe someone can provide a workaround in comments?)
- On Mac output is logged to the console, but terminal windows will lose focus as Unity is opened. I think this causes some weird behavior in FAKE with invoking Unity on Mac with Shell.Exec — I have some workarounds when we get there.
Yeah, Unity can be pretty janky in a lot of places… But it is awesome enough at doing pure game development to ignore some of these rough edges.
Using FAKE with Unity
So after setting this up for our games, I found out that @devboy has a nice FAKE library for Unity. If you are starting a new set of build scripts for a game, definitely check out his library. I’ll show how to invoke Unity directly, which should still apply for your game.
I generally use these FAKE helper methods to invoke Unity:
So then to build for Android, the simplest platform, I would setup a FAKE build target as such:
Setting up automatic versioning and other settings
Setting up proper version numbers during a build is fairly straightforward. I take advantage of the environment variable that TeamCity declares during a build, at the top of my FAKE script:
I also setup my own environment variable, VERSION_NUMBER, that we can take advantage of inside our Unity script:
One thing to note is that I am declaring many of the Unity build and player settings in C# code. I personally trust this a little better than the player settings file Unity stores this information in. It also helps out in cases where a developer might overwrite these settings in the Unity preference pane on accident.
So then our Android build could be improved to take advantage of the version number like so:
So the slash direction for keystoreName has to be an absolute path and Unix-style? OK, that’s cool…
Android is easy, since you get an apk directly. In the case of iOS, Unity generates an Xcode project that you have to build afterwards. Maybe one day Unity can improve this…
We are also somewhat in a transition phase for iOS builds with Unity. 64 bit support on iOS is somewhat experimental/beta but is fairly stable as of Unity 4.6.3. Unity is in the process of replacing its Mono scripting backend with a new technology called IL2CPP. So at Hitcents we are currently building both IL2CPP and Mono versions of our Unity games in parallel, since we’ve found a few issues in IL2CPP builds. Let’s walkthrough how to set these up.
In our Unity script, for making Mono builds:
And for iOS IL2CPP builds:
So the settings for the ScriptingBackend and Architecture values are not present via strongly typed properties yet, so SetPropertyInt is a workaround for now… Is it that hard to write a property, Unity, really?
So these methods would export an Xcode project into the Scratch/Xcode directory during a build. So to build the Xcode project, we use these helper methods:
And we create a series of FAKE build targets such as:
We would also setup 64 bit targets:
//HACK: because of some issues with running Unity command line on a Mac, I would not recommend putting a FAKE dependency on the “ios-player” target. For some reason, code execution in FAKE will continue immediately (not waiting for the Unity process to finish) when Unity is invoked on a Mac. Our TeamCity configuration invokes “ios-player” and “ios” in two build steps, which happens to work just fine.
We plan to distribute our Unity games on the Windows store for desktop, tablet, and Windows phone. Unity has several options to support this, but currently the latest and greatest option is to create a Windows Universal app for Windows 8.1 and Windows Phone 8.1. Unity exports a Visual Studio solution that you have to build to get a final app package. The main drawback with using Unity on Windows is that you cannot target AnyCPU like with a standard C#/Xaml project. Unity uses C++ libraries, and it currently only supports ARM and x86. This means you have a build for each CPU architecture, which is somewhat annoying.
That being said, here is how we setup our Windows build in the BuildScript class:
Then in our FAKE script:
Running Unit Tests
Tests? what tests? Game developers write tests?
We do have some amount of unit tests using Unity Test Tools, which is a way for writing NUnit and integration tests with Unity. We definitely do not have enough tests though, in my opinion.
Running tests are fairly straightforward, we just need the following FAKE target:
UnityTest.Batch.RunUnitTests is included with the Unity Test Tools asset, so you don’t have to add any Unity scripts for this. The only other step we had to take was to setup the TeamCity configuration to process TestResults.xml as test results, there might be a way to do this inside the FAKE script, however.
Running Unity command line can be fairly junky compared to the experience we get with Visual Studio and MSBuild or even Xamarin Studio, but it is definitely better than someone on our team taking the time to manually make builds. Our builds take about 30 minutes on the Mac and 40 minutes on Windows (so 40 min total because they run in parallel). These are running on spare physical machines that are by no means great hardware–definitely better than waiting an hour and a half on Unity cloud build. We could even scale this out to save more time by adding more TeamCity build agents. We are currently making Android, Windows 8.1, and Windows Phone 8.1 builds on Windows and iOS builds with both Mono and IL2CPP backends.
Final thoughts: Unity, go home you’re drunk.comments powered by Disqus