Scripting builds and deployments for Unity and Butler

Outside of my creative works etc, my line of work is Software Development, I work on various projects of different tech and states one thing most of those projects have in common though is the use of build scripts to assist with things like CI/CD some of which I now use in my game development workflow.

In this post we'll be covering the build and deployment script I use in my workflow.

Why use build scripts in the first place?

Everyone would have their reasons for wanting to use them, for me, it makes the whole process of deployment prepping more or less seamless. Once I've completed the feature set I want to release, from the root of the unity project I can just run something like:

./build-and-deploy.ps1 0.0.1

And that will build the Unity project, then if there are no issues it will attempt to push a deployment using itch.io's butler tool. It's that simple.

Script setup.

For me I started with the build script itself, I worked out what I needed to run the build and what would have needed to be changed during the build itself.

I came up with the following batch script build.cmd:

@echo on

SET /p BUILDNUM=< buildNum.txt

SET UNITYVERSION=2019.2.6f1
SET PRODUCTNAME="Dungeons of Loot"
SET COMPANYNAME="Luke Parker"
SET TARGET=Windows

SET VERSION=0.0.0
IF NOT [%1]==[] (set VERSION=%1)

SET TAG=""
IF NOT [%2]==[] (set TAG=%2)

SET DEFINES=""
IF NOT [%3]==[] (set DEFINES=%3)

SET BUILDLOCATION="./Build/%TARGET%"

rmdir -S %BUILDLOCATION%
mkdir %BUILDLOCATION%

>buildManifest.txt (
    echo ProductName=%PRODUCTNAME%
    echo CompanyName=%COMPANYNAME%
    echo Version=%VERSION%.%BUILDNUM%%TAG%
    echo BuildLocation=%BUILDLOCATION%
    echo Define=%DEFINES%
)

"E:\Programs\Unity\%UNITYVERSION%\Editor\Unity.exe" -projectPath ./ -quit -batchMode -executeMethod BuildHelper.%TARGET%

del /f buildManifest.txt

Things like the unity path and unity version I initially had a something that would have been passed in but soon realised that it didn't change that often if at all per project.

The buildManifest.txt file is used when we set up variables in unity before the build itself which is done in this class BuildHelper.cs:

using System.Linq;
using System.IO;
using System.Collections.Generic;
using UnityEditor;

public static class BuildHelper
{
    private static string _buildLocation;
    private static bool _variablesSetupRequired = true;

    public static void Windows()
    {

        SetupVariables();
        BuildPipeline.BuildPlayer(GetScenes(),_buildLocation + ".exe",BuildTarget.StandaloneWindows64,BuildOptions.None);
    }

    private static string[] GetScenes()
    {
        return EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray();
    }

    private static void SetupVariables()
    {
        if (!File.Exists("./buildManifest.txt"))
        {
        PlayerSettings.productName = "Product Name Here";
        PlayerSettings.companyName = "Luke Parker";
        PlayerSettings.forceSingleInstance = true;
        PlayerSettings.bundleVersion = "0.0.0.0";
        PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone,"");
        _buildLocation = "./Build/";
        }
        else
        {
            using (var fs = new FileStream("./buildManifest.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
            {
                using (var sr = new StreamReader(fs))
                {
                    var fileData = new Dictionary<string, string>();
                    while (!sr.EndOfStream)
                    {
                        var line = sr.ReadLine().Split('=');
                        fileData.Add(line[0], line[1].Replace("\"", ""));
                    }

                    PlayerSettings.productName = fileData["ProductName"];
                    PlayerSettings.companyName = fileData["CompanyName"];
                    PlayerSettings.forceSingleInstance = true;
                    PlayerSettings.bundleVersion = fileData["Version"];
                    PlayerSettings.SetScriptingDefineSymbolsForGroup(BuildTargetGroup.Standalone, fileData["Define"]);
                    _buildLocation = fileData["BuildLocation"] + "/";
                    #if IS_DEMO_MODE
                    _buildLocation += "Demo/";
                    #else
                    _buildLocation += "Full/";
                    #endif
                    _buildLocation += fileData["ProductName"].Replace(" ", "_");
                }
            }
        }
    }
}

This file would sit in an Editor folder in the Assets root of your project and would be called on the -executeMethod flag, this is what would determine how your project is built.

Next is the deployment script, again this is a batch script which compiles the passed in variables and runs butler with the correct parameters.

@echo off

SET /p BUILDNUM=< buildNum.txt

SET GAME=a-game
IF NOT [%1]==[] (set GAME=%1)

SET TARGET=Windows
IF NOT [%2]==[] (set TARGET=%2)

SET VERSION=0.0.0
IF NOT [%3]==[] (set VERSION=%3)

SET CHANNEL=windows
IF NOT [%4]==[] (set CHANNEL=%4)

SET SUBFOLDER=""
IF NOT [%5]==[] (set SUBFOLDER=%5)

butler push ./build/%TARGET%%SUBFOLDER% username/%GAME%:%CHANNEL% --userversion %VERSION%.%BUILDNUM%

This script pushes the contents of /build/Windows/ to the game and sets its correct channel and version.

Side Note: This can be configured to accept any kind of exe to push up etc. Though you will need to manually create the game on Itch.io first.

Finally it's the script that runs both of these, the build-and-deploy.ps1 script. This takes in only the frequently changed information such as version number, tag and any defines. Then it runs both of the previous scripts using that and info embedded in it:

param(
 [string] $version = "0.0.0",
 [string] $tag = "",
 [string] $define = ""
)

$projectName="Dungeons of Loot"
$itchioProjectName="dungeons-of-loot"

Function Build(){
    Write-Host "Building $projectName (Windows)"
    ./build.cmd $version "-$tag" $define

    if($LASTEXITCODE -ne 0){
        throw "Failed to build $projectName ($LASTEXITCODE)"
    }

    Write-Host "Build Complete (Windows)"
}

Function Deploy([string]$target, [string]$channel, [string]$subFldr){
    Write-Host "Deploying $projectName ($target) to itch.io"
    ./deploy.cmd $itchioProjectName $target $version $channel $subFldr

    if($LASTEXITCODE -ne 0){
        throw "Failed to deploy $version to itch.io. ($LASTEXITCODE)"
    }

    Write-Host "Deployed $projectName ($target) version $version to itch.io"
}

Build

[string] $subFolder = "/Full"
if($define -eq "IS_DEMO_MODE"){
    $subFolder = "/Demo"
}

Deploy "windows" $tag $subFolder

Depending on the size of your project you may only have to wait a matter of a minute before it's live on itch.io but once it is you can use this to rebuild and push out patches/updates as and when needed.

Like this:

If you've enjoyed this or gained something from it please support me by either buying Dungeons of Loot or buying me a Ko-Fi.

I haven't quite decided what I'm going to cover next. I might go through part of my design or idea process. If you have any thoughts on what I could do then feel free to join my Discord.

Have a good one, where ever you are.

Luke Parker

Luke Parker