An article about
By Geert Beuneker.
Adding Steam Workshop support to your unity game can greatly add to its value and longevity. And let’s face it, it’s just cool to see people build stuff that can be integrated into your game. When I started working on Steam Workshop integration into our own game it was pretty hard to find proper up-to-date tutorials on how to do it.
The Steam documentation on the topic is pretty extensive but can be hard to follow as there are a lot of dead ends, some misdirection and can be overall pretty confusing. So, I decided to write this step-by-step guide on how to integrate Steam Workshop into your Unity game.
This guide will walk you through the different steps required to add Steam Workshop integration into your Unity game:
But before we can do anything you must first configure your Steam back-end to enable the Steam Workshop for your game. Luckily, you can just enable it to just show for developers so you can test it before you put it online.
Having never created a mod before this step was actually quite surprising to me. I thought you’d just be able to press a “create new item” button on the Steam Workshop page and upload files from there but that is not the case.
Workshop items can only be created and modified through an external program or script which calls the right Steam back-end functions. That means that in order to allow your players to create workshop items, you have to either provide them a mod creation tool or integrate it into your existing game. For the purposes of this guide we will use unity as the tool to create and upload workshop items.
For our Unity project we added the Steamworks.NET package for easy integration with the Steam API. Keep in mind that you should also add or update the steam_appid.txt
in your project to match your game’s app id
, otherwise it won’t work! Also make sure that Steam is running before continuing.
We start by making a CreateItem call to the Steam back-end
/// <summary>
/// Create new workshop item
/// </summary>
public void CreateWorkshopItem()
{
// Retrieved from the project's steam_appid.txt. Can be manually inserted here as well
var appId = SteamUtils.GetAppID();
// Make the call to the steam back-end
var createHandle = SteamUGC.CreateItem(appId, EWorkshopFileType.k_EWorkshopFileTypeCommunity);
var callResult = CallResult<CreateItemResult_t>.Create(new CallResult<CreateItemResult_t>.APIDispatchDelegate(HandleCreateItemResult));
callResult.Set(createHandle);
}
Then, the result of this call will be handled in our HandleCreateItemResult function
/// <summary>
/// Callback event for when the item creation event has completed.
/// </summary>
private void HandleCreateItemResult(CreateItemResult_t result, bool bIOFailure)
{
if (result.m_bUserNeedsToAcceptWorkshopLegalAgreement)
{
var url = $"steam://url/CommunityFilePage/{result.m_nPublishedFileId}";
Debug.LogError($"Cannot create workshop item. Please accept workshop legal agreement: {url}");
Application.OpenURL(url);
}
if (result.m_eResult == EResult.k_EResultOK)
Debug.Log($"Workshop item created! (https://steamcommunity.com/sharedfiles/filedetails/?id={result.m_nPublishedFileId})");
else
Debug.LogError($"Workshop item creation failed: {result.m_eResult}");
}
According to Steam the user must first accept the Steam Workshop legal agreement in order to create workshop items. Therefore we handle that case in our callback event as well and redirect the user to the page as needed.
If everything went correctly and we got a success response then congratulations! We created a new empty workshop item, check it out on your Steam Workshop page!
Note
If you encounter errors here you can check a couple of things:
steam_appid.txt
is set to your project’s app Id
At this point we have created an empty workshop item. Now it’s time to fill it with actual content! Steam does not dictate what type of content it should be. The only thing they say about it is that you should avoid zipping your content to make the file comparisons more accurate. You simply point to a folder and Steam will upload the contents of that folder to the Steam Workshop.
What is in that folder is completely up to you. For example, for our game we used ModTool to package the contents of a unity scene to a folder so we could load it in later.
Similar to the CreateItem flow above, we have to make StartItemUpdate handle and then make a call to the Steam back-end.
public class UpdateItemParams
{
public string contentPath;
public string imagePath;
public string changeNotes;
}
/// <summary>
/// Set or update a workshop item's content.
/// </summary>
public void UpdateWorkshopItem(PublishedFileId_t itemId, UpdateItemParams updateItemParams)
{
// Retrieved from the project's steam_appid.txt. Can be manually inserted here as well
var appId = SteamUtils.GetAppID();
// Initialize the item update
var updateHandle = SteamUGC.StartItemUpdate(appId, itemId);
// Sets the folder that will be stored as the content for an item. (https://partner.steamgames.com/doc/api/ISteamUGC#SetItemContent)
SteamUGC.SetItemContent(updateHandle, updateItemParams.contentPath);
if (!string.IsNullOrEmpty(updateItemParams.imagePath))
{
// Sets the primary preview image for the item. (https://partner.steamgames.com/doc/api/ISteamUGC#SetItemPreview)
SteamUGC.SetItemPreview(updateHandle, updateItemParams.imagePath);
}
// Make the call to the steam back-end
var itemUpdateHandle = SteamUGC.SubmitItemUpdate(updateHandle, updateItemParams.changeNotes);
var callResult = CallResult<SubmitItemUpdateResult_t>.Create(new CallResult<SubmitItemUpdateResult_t>.APIDispatchDelegate(HandleItemUpdateResult));
callResult.Set(itemUpdateHandle);
}
/// <summary>
/// Handle the item update result
/// </summary>
private void HandleItemUpdateResult(SubmitItemUpdateResult_t result, bool bIOFailure)
{
if (result.m_eResult == EResult.k_EResultOK)
{
var url = $"steam://url/CommunityFilePage/{result.m_nPublishedFileId}";
Debug.Log($"Update Item success! ({url})");
}
else
Debug.LogError($"Workshop item update failed: {result.m_eResult}");
}
Steam provides several different options for setting the item content. For our example we kept it as barebones as possible so it’s easy to understand what is going on. We only upload the content, the thumbnail image and the required changeNotes. For the content and the image you can simply provide the local path on your system and Steam will figure out the rest!
You can simply set the title and description on the Steam Workshop page of your item. However, strangely enough there is no way to edit the thumbnail, that still needs to be done through code for some reason.
As of the time of writing, these are all the options you can configure for your workshop items:
You can find those in the documentation.
Note
If you encounter errors make sure that:
steam_appid.txt
is set to your project’s app Id
This is the part that confused me the most. Steam provides several different ways of retrieving Steam Workshop items none of which gave me the data I actually needed like… you know: THE ACTUAL FILES.
So to spare you the frustration of combing through steam’s back-end with incomplete examples and several dead-ends I’ll show you exactly how to do it.
What is important to know is that Steam downloads all the workshop items beforehand. You don’t really download the files when the game is running, you simply ask Steam where it stored the downloaded workshop content.
As I said there are multiple ways of retrieving a user’s subscribed items. But the method that worked best for me was by using CreateQueryUserUGCRequest. So we start by sending a query for the items the current Steam user is subscribed to.
/// <summary>
/// Retrieve all the user's subscribed workshop items
/// </summary>
private void GetSteamWorkshopItems()
{
// Retrieved from the project's `steam_appid.txt`. Can be manually inserted here as well
var appId = SteamUtils.GetAppID();
// Create a query request
var queryRequest = SteamUGC.CreateQueryUserUGCRequest(
SteamUser.GetSteamID().GetAccountID(),
EUserUGCList.k_EUserUGCList_Subscribed,
EUGCMatchingUGCType.k_EUGCMatchingUGCType_Items_ReadyToUse,
EUserUGCListSortOrder.k_EUserUGCListSortOrder_VoteScoreDesc,
appId,
appId,
1);
// Make the call to the steam back-end
var queryHandle = SteamUGC.SendQueryUGCRequest(queryRequest);
var callResult = CallResult<SteamUGCQueryCompleted_t>.Create(new CallResult<SteamUGCQueryCompleted_t>.APIDispatchDelegate(HandleQueryCompleted));
callResult.Set(queryHandle);
}
Next, we can retrieve the actual information of the workshop items from the query result.
/// <summary>
/// Handle the Steam UGC query result
/// </summary>
private void HandleQueryCompleted(SteamUGCQueryCompleted_t response, bool bIOFailure)
{
StartCoroutine(LoadItemsRoutine(response));
}
/// <summary>
/// Coroutine to get the information from Steam Workshop items.
/// </summary>
private IEnumerator LoadItemsRoutine(SteamUGCQueryCompleted_t response)
{
for (uint i = 0; i < response.m_unNumResultsReturned; i++)
{
// Get the Steam Workshop item from the query
SteamUGC.GetQueryUGCResult(response.m_handle, i, out var workshopItem);
// Get the size, folder and timestamp of the Steam Workshop item
SteamUGC.GetItemInstallInfo(workshopItem.m_nPublishedFileId, out var size, out var contentFolder, 255, out var timestamp);
// Do something with the contents of the Steam Workshop item here!
Debug.Log($"File content path: {contentFolder}");
// Get the mod title
Debug.Log(workshopItem.m_rgchTitle);
// Get the mod description
Debug.Log(workshopItem.m_rgchDescription);
// Load the mod thumbnail image
SteamUGC.GetQueryUGCPreviewURL(response.m_handle, i, out var imageUrl, 255);
Sprite thumbnail = null;
using (UnityWebRequest uwr = UnityWebRequestTexture.GetTexture(imageUrl))
{
yield return uwr.SendWebRequest();
if (uwr.result != UnityWebRequest.Result.Success)
Debug.Log(uwr.error);
else
{
var texture = DownloadHandlerTexture.GetContent(uwr);
thumbnail = Sprite.Create(texture, new Rect(0, 0, texture.width, texture.height), Vector2.zero);
}
}
}
}
We used a coroutine here because we also wanted to retrieve the thumbnail which can only be done by waiting for the result of a web request. If you don’t need the thumbnail you can simply remove the coroutine and retrieve all the information directly in the HandleQueryCompleted function.
The contentFolder
tells you where the files are for the workshop item. These are the exact same files you uploaded earlier. How you handle and load these files into your game from this point on is completely up to you!
Note If you encounter errors make sure that:
steam_appid.txt
is set to your project’s app Id
All of the information used in this article can be found on steam’s workshop guide documentation as well. Once you know where to look it’s not too complicated, but for Unity developers I think this is much easier to follow.
For me personally, it provides a really easy overview of copy/paste code blocks I can use in future projects as well. I hope this article has saved you a lot of time and effort combing through steam’s documentation. And to my future self reading this article wondering how to implement Steam Workshop into his Unity game: you’re welcome.