A brief introduction to Steamworks that cuts to the point
August 17, 2017
Like most game developers, I’m very interested in getting my game on Steam. Like most indie game developers, I lack any sort of access to the resources/knowledge that would easily clarify for me what “being on Steam” actually means.
It’s also hard to bridge this gap in knowledge because, though a lot of Steam’s functionality is well documented, knowing where to start in that massive store of documentation, as well as what ties all the disparate pieces together, can be difficult. On top of that, general discussion about the Steam platform happens on a private forum that is only open to developers that have been approved for the platform already through either Steam Direct or a Valve referral. So for someone starting out, looking for answers to basic questions can be difficult.
Because of this, I wanted to write a high-level overview for people who are just setting out, trying to get Steam working with their games. Specifically, I wanted to dive into the Steamworks SDK, the Valve-provided software library that gives you access to things like Workshop, Leaderboards, Achievements, and so on.
Steamworks is already well documented by Valve, but it’s written from the perspective of someone who is using the native C++ library and likely already knows how all its functionality intersects. If this isn’t you, even better! What follows is for that second person, someone who is making a game in a higher-level language and just wants a simple Steamworks integration (it exists, I promise!). More specifically, this post is targeted at people using C# in some for or another, but ideally using Unity as their game engine.
Steamworks is two things. First, it’s the developer portal for managing everything about your game’s presence on Steam, from changing your game’s banner pictures to managing sales to listing controller support, etc. Secondly, it’s an SDK Valve provides that lets you interoperate with everything else Steam provides like Workshop, Leaderboards, Servers, Achievements, etc. Keep that order in mind! The SDK “is only required to upload your content to Steam”, source(https://partner.steamgames.com/doc/sdk). This means you can totally forego tackling nearly all of the aforementioned stuff of the SDK and just focus on how to use it to get your game on Steam. However, the SDK does offer a lot of other great, useful functionality. So let’s get it up and running!
If you’re a C++ developer you can go ahead and add the library to your game via the instructions here.
But, if you’re a C#/Unity developer, we’ve got a little work to do. Because native C++ headers/source aren’t compatible with Unity, we need to use a wrapper library that allows us to integrate the SDK’s functionality. Such a wrapper library would allow us to use high level C# functions to call low level C++ functions. In the past, the go-to library for this was Steamworks.NET, which is exactly as it sounds: Steamworks implemented with .NET. However, it is exactly as it sounds, to a fault.
Steamworks.NET provides one-to-one mapping of Steamworks functions to C# functions, but that means you need to fully understand Steamworks as a library to get anything done. For someone who is just getting started and wants to do simple tasks, this can be a lot of work. If you want to do something more complex, Steamworks.NET requires you to essentially write your own utility wrapper on top of their wrapper, which seems to defeat the purpose of a wrapper in the first place.
Due to these limitations and other reasons, Facepunch Studios (of Rust and Garry’s Mod fame) set out to build a better Steamworks library for C#/Unity with the explicit purpose of making everything easier.
It gets rid of the need for tons of code scaffolding to do basic (and complex) tasks in Steamworks, which lets you focus on just “working” with Steam. It’s also currently being used in Rust, which means it is production tested on one of the highest user-volume games on Steam. Complex tasks are abstracted away into simple function calls, and the library itself is only three files so it doesn’t cause any extra bloat. I can’t emphasize enough how much more of a godsend it is for someone starting out; it’s truly wonderful. The creator of Steamworks.NET has even said that Facepunch.Steamworks is “basically what I wanted the next step of Steamworks.NET to be” and that it “should probably be the default choice for most people”. Steamworks.NET is still there for people who want to implement their own version of what Facepunch.Steamworks essentially is, but in my opinion, if it’s good enough for Rust it’s good enough for me. So how does it work and what’s so special about it? Let’s get started.
First off, It’s easy to think that you need to be an approved Steamworks developer to start working with Steamworks, but the truth is that you can start using the SDK right now without needing to have gone through the process of registering. This is because Valve provides developers with a test “AppID” of 480 that you can program against.
An AppID is how your game is uniquely identified on Steam (and Steamworks), and is one of the primary things you get when you get your game properly registered. It “stakes out” your section of Steam/Steamworks and allows you to wholly own anything associated with that AppID. AppID 480 corresponds to “SpaceWar”, a demonstration game Valve made and open-sourced that shows of some of the capabilities of Steamworks (you should check it out!).
While a unique AppID is nice and obviously required for your game at some point, the test AppID (480) allows you to work with Steam’s services as if your game were live. Your real AppID should be substituted in once you get one, but in the meantime, 480 will serve you just fine. That said, probably don’t create a server with the name “My ‘Trademark Pending Game Name’ Server”.
So, let’s download the Facepunch.Steamworks library (FP lib from now on), knowing we can properly test it with AppID 480. Head to the releases section of its Github page (the library is fully open source, MIT licensed) and download the latest release. Once you extract the .zip file you should see a few folders. The READMEs go into more detail, but you essentially need to copy a small set of these files over to your Unity project, depending on your platform. The Facepunch.Steamworks files are the lib itself, and the steam_api files are platform specific files that contain the actual Steamworks SDK. Your Unity directory should look something like this after your files are imported (assuming Windows x86/x64):
Unity Project Folder
|— Assets
|— Plugins
|— Facepunch.Steamworks
|— Facepunch.Steamworks.dll
|— Facepunch.Steamworks.pdb
|— Facepunch.Steamworks.xml
|— steam_api.dll (Windows x86)
|— steam_api64.dll (Windows x64)
|— steam_appid.txt
The steam_appid.txt
is literally a text file with only your AppID in it, so for our cases it should just be a text file with 480
in it, no quotes. The .dll
, .pdb
, and .xml
files come from the Release folder of the downloaded .zip file, using whichever .NET
version you’re targeting. For Unity, 3.5 is fine. If you’re starting out with a clean project, Facepunch has also graciously provided a small test project that does a lot of this for you that you can you to start off your project.
Once you’ve copied all the lib’s files to their proper directory locations, you’re basically setup! All we need to do now is to write some code to get everything integrated. I’m going to copy over the test file from the test project and just abbreviate it here for clarity’s sake.
using System;
using UnityEngine;
using System.Collections;
using System.Linq;
using Facepunch.Steamworks;
public class SteamTest : MonoBehaviour
{
void Start ()
{
// Don't destroy this when loading new scenes
DontDestroyOnLoad( gameObject );
// Configure for Unity
// This is VERY important - call this before doing anything
Facepunch.Steamworks.Config.ForUnity( Application.platform.ToString() );
// Create the steam client using the test AppID (or your own AppID eventually)
new Facepunch.Steamworks.Client( 480 );
// Make sure we started up okay
if ( Client.Instance == null )
{
Debug.LogError( "Error starting Steam!" );
return;
}
// Print out some basic information
Debug.Log("My Steam ID: " + Client.Instance.SteamId);
Debug.Log("My Steam Username: " + Client.Instance.Username );
Debug.Log("My Friend Count: " + Client.Instance.Friends.AllFriends.Count() );
}
private void OnDestroy()
{
if ( Client.Instance != null )
{
// Properly get rid of the client if this object is destroyed
Client.Instance.Dispose();
}
}
void Update()
{
if ( Client.Instance != null )
{
// This needs to be called in Update for the library to properly function
Client.Instance.Update();
}
}
}
And… that’s it! If you attach that script to a GameObject
in your scene and enter play mode, you should see yourself in Steam as playing “Spacewar”, and see the console print out some basic Steam information about you (if not, make sure you are actually logged onto Steam).
Once you’re set up, accessing deeper Steam functionality is pretty easy since the FP lib covers and wraps almost all parts of the standard Steamworks SDK. The question does still stand though as to “what” that stuff is, so here’s a small list and some descriptions of what you can work with (in the FP lib):
The best way to see how to use a given function is to see if there is a working example of it in the Facepunch.Steamworks test project (NOTE: this is not the Unity test project), and model the implementation in your game.
Most of these features are documented further in the the FP lib’s wiki, but only a few classes really properly filled out. If you don’t see any example, check out the library’s code and see if the function is implemented at all. If not, see how far you can get implementing it on your end, or just submit an issue to the library. The Facepunch team is generally very responsive, so they can let you know if something is being worked on or not and even guide you on how to give back to the community, if you go the route of implementing it yourself.
When working with the FP lib (or even the native API), you’ll notice it’s not always as easy to work with as simply calling something like Client.Instance.SteamId
. This is because the Steamworks SDK (and FP lib by extension) makes heavy use of asynchronous functions so that your game doesn’t hang every time you want to do some non-trivial interaction with Steam. Without async calls, you would have to wait for the main Steam server to respond before any more of your code could execute, which is obviously error-prone and frustrating, as far as play experience goes. So, to use the library, you will need to get familiar with the idea of delegates and callbacks. It may be a lot for someone just getting started, but it’s easy to understand once you grasp the core idea. I’ll give an example.
If you want to get a list of all the lobbies in your game using the FP lib, you would do this:
client.LobbyList.Refresh();
Notice how there is no return or assignment happening there. If that’s the case, how do we actually get what we just requested? In Steamworks, once this function is called, the Steam backend gets everything ready for you, and then sends you some data via a “callback”. Almost literally, Steam “calls you back” to say “Hey, your data is ready! Here!”.
To receive the call, we need to pick up the phone, or as it’s actually known, “subscribe”, to the callback. This is done by defining a function that takes in the data the callback provides. Sometimes, no data is provided, and the callback is used primarily as a “hook”, or a way to know that it’s OK to resume. Once you’re inside your callback handler, you can safely continue operation. Here’s an example:
void Start()
{
// Subscribe to the proper event by defining the function you want to be called when OnLobbyJoined is called by Steam.
client.Lobby.OnLobbyJoined += OnLobbyJoinedCallback;
// Call the function
Native.Lobby.Join(LobbyID);
}
void OnLobbyJoinedCallback(bool success)
{
// If we're inside here, the callback has properly fired!
// We can check the status of this callback by seeing if the bool "success" that Steam passed in is true or false
if(success)
{
// If there were no errors, we can safely call functions in here that require a specific circumstance
// For example, we can now print out the id of the Lobby's Owner
Debug.Log(client.Lobby.Owner);
}
}
Understanding this pattern will be immensely helpful as you use the library, as well as for understanding what Steamworks is actually doing when you inevitably peek at the actual documentation. If you want a more in-depth discussion of how this works, I definitely recommend Valve’s documentation on the topic, as well as some bits on the Steamworks.NET website.
From here on, you’re free to implement whatever you want! Valve has no requirements here, but if you are on the platform it is obviously in your best interest to engage with it and its community in the ways Valve has made available. Once you get registered with Steam Direct you can simply swap in your AppID and carry over any Steam functionality you implemented with the test AppID.
I really hope this is helpful to anyone looking to get started with Steam, and I obviously strongly recommend people check out Facepunch.Steamworks on Github. If you feel so inclined, try contributing in small ways like filling out the documentation, or if you want something meatier to chew on submit an issue and ask if anything needs doing. If you have any questions about this article, feel free to reach out to me @kkukshtel. I’d also love it if you followed my game on Twitter @isotacticsgame or sign up for the newsletter.
Published on August 17, 2017.