Een artikel over

Hoe voeg je Steam Workshop support toe aan je Unity game

Door Geert Beuneker.

Hoe voeg je Steam Workshop support toe aan je Unity game

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:

  1. Workshop items maken
  2. Inhoud van workshopitems uploaden
  3. Downloaden en gebruiken van workshop items

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.

Een nieuw workshop item maken

Referentie documentatie

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:

  • De steam_appid.txt van je project is ingesteld op de app Id van je project.
  • De Steam Workshop is ingeschakeld in de Steam back-end voor je project
  • Steam staat aan

Inhoud uploaden naar een workshopitem

Referentie documentatie

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:

  • SetItemTitle Stelt een nieuwe titel in voor een item.
  • SetItemDescription Stelt een nieuwe beschrijving in voor een item.
  • SetItemUpdateLanguage Stelt de taal in van de titel en beschrijving die worden ingesteld in deze item update.
  • SetItemMetadata Stelt willekeurige metadata in voor een item. Deze metadata kan worden geretourneerd vanuit query's zonder de werkelijke inhoud te hoeven downloaden en installeren.
  • SetItemVisibility Stelt de zichtbaarheid van een item in.
  • SetItemTags Stelt willekeurige door de ontwikkelaar gespecificeerde tags in op een item.
  • AddItemKeyValueTag Voegt een key-value tag paar toe aan een item. Sleutels kunnen naar meerdere verschillende waarden verwijzen (1-to-many relatie).
  • RemoveItemKeyValueTags Verwijdert een bestaande sleutel-waarde-tag van een item.
  • SetItemContent Stelt de map in die zal worden opgeslagen als de inhoud voor een item.
  • SetItemPreview Stelt de primaire voorbeeldafbeelding voor het item in.

Je kunt deze vinden in de documentatie.

Opmerking
Als je fouten tegenkomt, zorg er dan voor dat:

  • De inhoud van het bestand niet groter is dan de workshoplimiet
  • De steam_appid.txt van je project is ingesteld op de app Id van je project.
  • De Steam Workshop is ingeschakeld in de Steam back-end voor je project
  • Steam staat aan (dit is vooral gericht aan mezelf)

Steam Workshop items downloaden en gebruiken

Referentie documentatie

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:

  • Je daadwerkelijk geabonneerd bent op workshopitems. Je bent niet automatisch geabonneerd op je eigen workshop items!
  • De steam_appid.txt van je project is ingesteld op de app Id van je project.
  • De Steam Workshop is ingeschakeld in de Steam back-end voor je project
  • STEAM STAAT AAN (serieus, ik heb hier zoveel tijd aan verspild. Die uren krijg ik nooit meer terug)

Conclusie

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.

Over de auteur.

Geert Beuneker

Geert Beuneker

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.