Een artikel over
Door Geert Beuneker.
Ondersteuning voor Steam Workshop toevoegen aan je Unity game kan de waarde en levensduur ervan enorm verhogen. En laten we eerlijk zijn, het is gewoon cool om te zien hoe mensen dingen bouwen die in je spel geïntegreerd kunnen worden. Toen ik begon te werken aan Steam Workshop integratie in ons eigen spel, was het behoorlijk moeilijk om goede up-to-date tutorials te vinden over hoe je dit moet doen.
De Steam documentatie over dit onderwerp is vrij uitgebreid, maar kan moeilijk te volgen zijn omdat er veel doodlopende paden zijn, een aantal misleidingen en het kan over het algemeen behoorlijk verwarrend zijn. Daarom heb ik besloten deze stap-voor-stap gids te schrijven over hoe je Steam Workshop in je Unity game kunt integreren.
Deze gids leidt je door de verschillende stappen die nodig zijn om Steam Workshop integratie toe te voegen aan je Unity game:
Maar voordat we iets kunnen doen moet je eerst je Steam back-end configureren om de Steam Workshop in te schakelen voor je spel. Gelukkig kun je de workshop alleen voor ontwikkelaars laten zien, zodat je hem kunt testen voordat je hem online zet.
Omdat ik nog nooit een mod heb gemaakt, was deze stap eigenlijk vrij verrassend voor mij. Ik dacht dat je gewoon op de knop "nieuw item maken" op de Steam Workshop pagina kon drukken en vanaf daar bestanden kon uploaden, maar dat is niet het geval.
Workshop items kunnen alleen worden gemaakt en gewijzigd via een extern programma of script dat de juiste Steam back-end functies aanroept. Dat betekent dat als je je spelers wilt toestaan om workshopitems te maken, je ze ofwel een tool voor het maken van mods moet bieden of het in je bestaande spel moet integreren. In deze handleiding gebruiken we Unity als tool om workshopitems te maken en te uploaden.
Voor ons Unity project hebben we het pakket Steamworks.NET toegevoegd voor eenvoudige integratie met de Steam API. Houd er rekening mee dat je ook de steam_appid.txt
in je project moet toevoegen of bijwerken zodat deze overeenkomt met de app id
van je game, anders werkt het niet! Zorg er ook voor dat Steam actief is voordat je verder gaat.
We beginnen met het maken van een CreateItem aanroep naar de 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);
}
Vervolgens wordt het resultaat van deze aanroep verwerkt in onze HandleCreateItemResult functie
/// <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}");
}
Volgens Steam moet de gebruiker eerst de Steam Workshop legal agreement accepteren om workshopitems te kunnen maken. Daarom behandelen we dat geval ook in onze callback handler en sturen we de gebruiker indien nodig door naar de pagina.
Als alles goed is gegaan en we een succesvolle reactie krijgen: gefeliciteerd! We hebben een nieuw leeg workshop item gemaakt, bekijk het op je Steam Workshop pagina!
Opmerking
Als je hier fouten tegenkomt kun je een paar dingen controleren:
steam_appid.txt
van je project is ingesteld op de app Id
van je project.Op dit punt hebben we een leeg workshop item gemaakt. Nu is het tijd om het te vullen met echte inhoud! Steam schrijft niet voor wat voor soort inhoud het moet zijn. Het enige wat ze erover zeggen is dat je moet voorkomen dat je je content zipt om de bestandsvergelijkingen nauwkeuriger te maken. Je wijst gewoon een map aan en Steam uploadt de inhoud van die map naar de Steam Workshop.
Wat er in die map staat bepaal je helemaal zelf. Voor ons spel gebruikten we bijvoorbeeld ModTool om de inhoud van een Unity scene in een map te verpakken, zodat we die later konden laden.
Vergelijkbaar met de CreateItem flow hierboven, moeten we StartItemUpdate handle en dan een call maken naar de 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 biedt verschillende opties om de inhoud van een item in te stellen. Voor ons voorbeeld hebben we het zo eenvoudig mogelijk gehouden, zodat het makkelijk te begrijpen is wat er gebeurt. We uploaden alleen de inhoud, de thumbnail en de vereiste changeNotes. Voor de inhoud en de afbeelding kun je gewoon het lokale pad op je systeem opgeven en Steam doet de rest!
Je kunt gewoon de titel en beschrijving instellen op de Steam Workshop pagina van je item. Vreemd genoeg is er echter geen manier om de thumbnail aan te passen, dat moet om de een of andere reden nog steeds via code gebeuren.
Op het moment van schrijven zijn dit alle opties die je kunt configureren voor je workshop-items:
Je kunt deze vinden in de documentatie.
Opmerking
Als je fouten tegenkomt, zorg er dan voor dat:
steam_appid.txt
van je project is ingesteld op de app Id
van je project.Dit is het gedeelte dat mij het meest in verwarring bracht. Steam biedt verschillende manieren om Steam Workshop items op te halen, maar geen daarvan gaf me de gegevens die ik echt nodig had, zoals... je weet wel: DE ECHTE BESTANDEN.
Dus om je de frustratie te besparen van het doorkammen van steam's back-end met onvolledige voorbeelden en verschillende doodlopende wegen, laat ik je precies zien hoe het moet.
Wat belangrijk is om te weten, is dat Steam alle workshopitems van tevoren downloadt. Je downloadt de bestanden niet echt als het spel draait, je vraagt gewoon aan Steam waar het de gedownloade workshopinhoud heeft opgeslagen.
Zoals ik al zei zijn er meerdere manieren om de items waarop een gebruiker is geabonneerd terug te halen. Maar de methode die voor mij het beste werkte was door gebruik te maken van CreateQueryUserUGCRequest. Dus we beginnen met het verzenden van een query voor de items waarop de huidige Steam gebruiker is geabonneerd.
/// <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);
}
Vervolgens kunnen we de daadwerkelijke informatie van de workshopitems ophalen uit het zoekresultaat.
/// <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 hebben hier een coroutine gebruikt omdat we ook de thumbnail wilden ophalen, wat alleen kan door te wachten op het resultaat van de web request. Als je de thumbnail niet nodig hebt, kun je gewoon de coroutine verwijderen en alle informatie direct ophalen in de HandleQueryCompleted functie.
De contentFolder
vertelt je waar de bestanden staan voor het workshop item. Dit zijn precies dezelfde bestanden die je eerder hebt geüpload. Hoe je deze bestanden vanaf dit punt behandelt en in je spel laadt, is helemaal aan jou!
Opmerking
Als je fouten tegenkomt, controleer dan of:
steam_appid.txt
van je project is ingesteld op de app Id
van je project.Alle informatie die in dit artikel is gebruikt, is ook te vinden in de Steam workshop guide documentatie. Als je eenmaal weet waar je moet zoeken, is het niet al te ingewikkeld, maar voor Unity ontwikkelaars denk ik dat dit veel makkelijker te volgen is.
Voor mij persoonlijk biedt het een heel gemakkelijk overzicht van copy/paste codeblokken die ik ook in toekomstige projecten kan gebruiken. Ik hoop dat dit artikel je veel tijd en moeite heeft bespaard bij het doorspitten van de Steam documentatie. En aan mijn toekomstige ik die dit artikel leest en zich afvraagt hoe hij Steam Workshop in zijn Unity game kan implementeren: graag gedaan.
Developer en Onderzoeker
Als een georganiseerde ontwikkelaar met een master in computer science, streeft Geert naar uitmuntendheid in zijn vak, waarbij hij voortdurend bezig is zichzelf verder te ontwikkelen en niewe kennis op te doen. Geert heeft de ambitie om programmeren tot een ambacht te verheffen.