partially­disassembled

Engineering blog from Studio Drydock.

Headless automation in Unity

First published 08 October 2019

Sometimes we need to run some game code but have no need to render anything. For example, as part of our continuous integration at Studio Drydock, we have an AI bot play through the game and post errors and stats to a Slack channel. Running Unity headless is many times faster than running with a graphics device, and this post explains how to set that up.

Batch mode

The first part to get working, if you haven't already, is running Unity in “batch mode”. This is simply calling an arbitrary static method in your game code from the command-line in a new instance of Unity, and then exiting.

A typical use-case for this is building your game for a target platform. Here's an example static method that you might add to your game code (in an Editor subdirectory) for this purpose:

public static class BatchModeEditorUtility
{
    static string[] sceneList = { "Menu", "Level01", "Level02" }; // TODO: change this for your game

    public static void BuildiOS()
    {
        var report = BuildPipeline.BuildPlayer(new BuildPlayerOptions()
        {
            locationPathName = "build/ios",
            scenes = sceneList,
            target = BuildTarget.iOS,
            options = BuildOptions.None
        });

        Debug.Log($"Build result: {report.summary.result}, {report.summary.totalErrors} errors");
        if (report.summary.totalErrors > 0)
            EditorApplication.Exit(1);
    }
}

To test this, exit Unity (you cannot run multiple Unity instances at once for a given project), and from a command line on Windows:

C:\Program Files\Unity\Hub\Editor\2019.2.7f1\Editor\Unity.exe -batchmode -nographics -quit -logFile - -executeMethod BatchModeEditorUtility.BuildiOS

Or on macOS:

/Applications/Unity/Hub/Editor/2019.2.7f1/Unity.app/Contents/MacOS/Unity -batchmode -nographics -quit -logFile - -executeMethod BatchModeEditorUtility.BuildiOS

(You will need to replace the Unity version number in the path with the correct one for your project).

Running a gameplay test

The above is great for running single one-off batch tasks like processing some assets or building the game, but doesn't let the game actually run. For this, we want to remove the -quit argument, and instead explicitly exit the game on some gameplay event (for example, successfully completing the game test).

Here's an example of the style of editor and game code you would set up to support this.

// This class must live in an Editor folder
public static class BatchModeEditorUtility
{
    public static bool isBatchMode;

    public static void Play()
    {
        isBatchMode = true;
        EditorSceneManager.OpenScene("Assets/Scenes/MainMenu.unity"); // TODO replace with desired start scene
        EditorApplication.EnterPlaymode();
    }
}

// This class must live in with game code
public static class BatchModeUtility
{
    public static void Exit(int errorCode = 0)
    {
        // Check that this instance was actually launched from a batch mode session, so that game code
        // doesn't inadvertently exit the editor during development.
        if (Application.isBatchMode)
            EditorApplication.Exit(errorCode);
    }
}

Don't forget to add a call to BatchModeUtility.Exit somewhere in your game, otherwise it will never exit. Once again, run Unity from the command-line after exiting the editor:

C:\Program Files\Unity\Hub\Editor\2019.2.7f1\Editor\Unity.exe -batchmode -nographics -logFile - -executeMethod BatchModeEditorUtility.Play

(The only difference from the previous example is removing the quit optin and changing which method is executed).

Unlocking framerate

Even though the game is running without graphics, by default it is still frame-limited according to the project settings, and running in realtime (for example, at 60 FPS). Add these lines into BatchModeEditorUtility.Play to fix this:

Application.targetFrameRate = -1;   // No maximum frame rate to sleep on
QualitySettings.vSyncCount = 0;     // No GPU vSync
Time.captureFramerate = 30;         // Simulate 30 FPS (TODO consider adjusting this for your project)

With the combination of these changes, Unity will now no longer slow down to hit a target framerate, but will also lock the timestep provided by Time.deltaTime (and derived values such as timeSinceLevelLoad and fixedDeltaTime) to simulate the game running at 30 FPS (i.e., the delta time received by your update functions will be 33ms).

Note that this technique is not specific to batch mode; you can run visual tests at high-speed with exactly the same settings. If you need to restore your settings (for example, toggling in/out of a fast mode for development):

Application.targetFrameRate = 30;   // Sleep to target 30 FPS (TOOD consider adjusting for your project)
QualitySettings.vSyncCount = 2;     // GPU vSync to every second frame (i.e., 30 FPS for a 60 FPS display such as on an iPhone)
Time.captureFramerate = 0;          // Disable the framerate simulation, use real-time.

Reduce log verbosity

By now you've probably noticed that when Unity logs from a batch-mode session, you get entire call-stacks for every log line. This makes the log very difficult to read and is usually unnecessary, so I disable the stack traces when entering batch-mode for non-error log messages:

PlayerSettings.SetStackTraceLogType(LogType.Log, StackTraceLogType.None);
PlayerSettings.SetStackTraceLogType(LogType.Warning, StackTraceLogType.None);
PlayerSettings.SetStackTraceLogType(LogType.Assert, StackTraceLogType.None);

Limitations

The biggest limitation of running headless is the obvious one: there's no graphics device! For most games this won't affect gameplay, but if you have some kind of framebuffer or texture read-back, then it's not going to work. It also precludes saving out screenshots for debug or marketing purposes.

You can still run batch mode without the -nographics option if you need the graphics device, however the unlocked framerate is not nearly as high – I have not discovered a way to temporarily enable/disable frame rendering in Unity (perhaps using the new scriptable render pipeline is a solution?).

Next Steps

This should be enough to get you started! Next time I'll give some tips on integrating Unity into an external build script (e.g., continuous integration). Follow either the RSS feed on this page or @AHolkner on Twitter to get a notification when that chapter is up.