Unity Build Pipelines Part 1: Automating Multiplatform Builds with Steam Integrations

July 27, 2019 ☼ UnitySteamworks

Introduction

A while back I wrote a post that introduced the idea of using the Facepunch.Steamworks library in Unity. This post is a bit of a companion to that that leverages some things I’ve learned along the way, especially in regards to delivery and how things work in the actual build pipeline.

I would also be lying if I told you I wasn’t writing as well for my own reference. Like I touched on in that Steamworks post, a lot of knowledge about Steam/Steamworks moves around in private online communities as well as inside of large orgs that have no incentive to share this knowledge. It’s because of this that I consider it a prime responsibility of the indie community to share knowledge with each other, so to that end I want to share some of that secret knowledge to help others.

This post isn’t going to be a long one, and mainly focuses around a single aspect of development, the build pipeline. If you’ve ever built” your game in Unity, you know what this is in the most simple form: Ctrl/Cmd+Shift+B and click Build and Run”. This works! It works for most small things! But as a game gets bigger and starts gathering more dependences (anything from external plugin DLLs, license files, images, etc.), a full build” of your game starts requiring extra steps of copy+paste into the final directory before delivery. If you have to build even 3-5 times a day this can become incredibly tedious.

But there is good news! What some people may not know is that that process can be scripted, meaning you don’t ever need to actually open that Unity build menu. Additionally, you can start tying in other aspects to your so-called build pipeline” to start automating a lot of the tedious work I just described. Instead of walking through all the ways this may manifest, I’m going to once again focus on Steam, and what’s required when using the Facepunch.Steamworks library on multiple platforms.

Building a Unity Game For Steam

Just having the Facepunch.Steamworks DLL inside the Unity Editor isn’t enough to get the game working with Steam. Instead, that library requires you to also copy specific files to specific locations. That’s easy to remember how to do if you have to build your game only once ever, but on a game that is in full production, you’re talking building multiple times a week or day, and adding in a manual step of copying the right files to the right places introduces error-prone behavior into something that should be set it and forget it”. So let’s do that!

The first thing to do is to make a script in Unity that ties into the Build system. Luckily, all this really means is calling BuildPipeline.BuildPlayer with the right arguments, but when tying this to a static script that can be accessed from the Editor menus, you’re able to essentially compose a single menu item that builds your game exactly how you want. See below for a simple example:

using UnityEditor;
using UnityEngine;
using System.Diagnostics;

public class BuildPlayer 
{
    [MenuItem("Build/Build Standalone PC")]
    public static void BuildPCPlayer ()
    {
        //get the path
        string path = $"{Application.dataPath}/../bin/MyGameName/pc"; 
        // Build player
        BuildPipeline.BuildPlayer(EditorBuildSettings.scenes, path + "/game.exe", BuildTarget.StandaloneWindows, BuildOptions.None);
        // Do other things with the build folder
    }
}

(You can read more about this generally at Unity’s site here)

As part of this build step, and as is present in Unity’s example, you can also do some file copying after you’ve built your game into a folder. This is perfect for our Steam use case, which dictates files be copied to the build directory.

The main files we need to copy over from Facepunch.Steamworks are libsteam.dll, libsteam.dylib, and the steamappid.txt. Depending on your platform, you need only some of these files, and each goes into a different place.

For PC: PC is easy in that both files just go to a single directory. This is the code I use to build PC:

[MenuItem("Build/Build Standalone PC")]
public static void BuildPCPlayer ()
{
    string path = $"{Application.dataPath}/../bin/cantata-{VersionNumber.Version}/pc"; 
    string buildfilespath = $"{Application.dataPath}/../buildfiles";
    
    UnityEngine.Debug.Log("Starting PC Build");
    // Build player
    BuildPipeline.BuildPlayer(EditorBuildSettings.scenes, path + "/cantata.exe", BuildTarget.StandaloneWindows, BuildOptions.None);
    UnityEngine.Debug.Log("PC Build Done, copying build files");
      //Copy libraries
    UnityEngine.Debug.Log("Starting Copying Steam Libraries PC");
    FileUtil.CopyFileOrDirectory( $"{buildfilespath}/steam/steam_appid.txt", path + "/steam_appid.txt" );
    FileUtil.CopyFileOrDirectory( $"{buildfilespath}/steam/steam_api.dll", path + "/steam_api.dll" );
    FileUtil.CopyFileOrDirectory( $"{buildfilespath}/steam/steam_api64.dll", path + "/steam_api64.dll" );
    UnityEngine.Debug.Log("Done Copying Steam Libraries PC");
    UnityEngine.Debug.Log("DONE BUILDING PC");
}

You can see I’m also referencing a build files” folder in game’s data path. This is a folder whose contents are static and contain the files I need to build my game. Because these never change, I know I can simply copy them from my game’s working project directory at the build files location and into the built game’s folder.

Build Menu Options

To copy files over, you can use Unity’s FileUtil class to easily copy files. Because you know exactly where you’re building, you can then use that path as your target path for any necessary copy actions.

The VersionNumber.Version is a custom class I wrote to grab the proper version number for the build, and I’ll go more into that in Part 2 of this post series.

For OSX: OSX is more tricky here. OSX applications run from a .app, which is somewhat like a .exe file on Windows. However, what even some seasoned OSX users often don’t know know is that the .app is actually a directory as well, and as such can be opened like a folder. This is readily apparent on Windows, where .apps are just folders, but less obvious on OSX.

Either way, we can treat the .app here as a folder and place the proper OSX files in the right OSX location. This is the code I use to build OSX:

[MenuItem("Build/Build Standalone OSX")]
public static void BuildOSXPlayer ()
{
    string path = $"{Application.dataPath}/../bin/cantata-{VersionNumber.Version}/osx"; 
    string buildfilespath = $"{Application.dataPath}/../buildfiles";
    
    UnityEngine.Debug.Log("Starting OSX Build");
    // Build player
    BuildPipeline.BuildPlayer(EditorBuildSettings.scenes, path + "/cantata.app", BuildTarget.StandaloneOSX, BuildOptions.None);
    UnityEngine.Debug.Log("OSX Build Done, copying build files");

    UnityEngine.Debug.Log("Starting Copying Steam Libraries OSX");
    FileUtil.CopyFileOrDirectory( $"{buildfilespath}/steam/steam_appid.txt", path + "/cantata.app/Contents/MacOS/steam_appid.txt" );
    FileUtil.CopyFileOrDirectory( $"{buildfilespath}/steam/libsteam_api.dylib", path + "/cantata.app/Contents/Frameworks/MonoBleedingEdge/MonoEmbedRuntime/osx/libsteam_api.dylib" );
    UnityEngine.Debug.Log("Done Copying Steam Libraries OSX");
    
    UnityEngine.Debug.Log("DONE BUILDING OSX");
}

The complexity of knowing to copy the dylib to the /Frameworks/MonoBleedingEdge/… folder is what inspired me to write this post in the first place because I was struggling to find any info on this and kept getting libsteam.dylib” not found errors. Shoutouts go to the posts on the Facepunch.Steamworks issues pages on Github for eventually showing me the way, but I’m also posting here, outside of a Github repo, in hopes that more eyes can more easily find this information.

NOTE: According to Garry himself, some of these issues with library copying specifically have been resolved in new versions of Unity, though the what of the exact changes is somewhat unclear. The information above is still relevant to building build pipelines, but in the future you just may not need to account for copying libraries specifically!

Additionally, so I don’t have to click (GOD FORBID) two menu items back to back, I also just wrote a simple function that calls both build functions and then give it its own Menu Item.

[MenuItem("Build/Build Standalone PC+OSX")]
public static void BuildStandalonePlayers ()
{
    BuildPCPlayer();
    BuildOSXPlayer();
}

Here’s what my menus look like in Unity for building:

Build Menu Options

Now when I build I just click that menu item, and then both PC/OSX get built to the right directory, ready to be immediately zipped up and sent away!

Build Directories


Knowing this, you should be able to get pretty far with packaging up any dependencies you may have when creating your builds! For me, I started doing this not only for Steam, but because I’m starting to distribute the pre-alpha of my game via Discord and needed a unified build system to push new builds.

There’s actually a lot more to that process as well than just uploading a zip file of your build, so I’m going to explore that in the next post of this series! Hope this helped!


Back To Index