Steam integration with Unity – Achievements, Leaderboards, Building

Summary

Setting up Steam integration for Unity. The video goes into the basics of creating achievements and leaderboards using the Unity Steamworks package. Also covers building/uploading to Steam via the Steamworks SDK. No paid addons used.

Script

Beyond the video, I’ve also included some possible implementation for displaying the leaderboard data.
Full script below (Creative Commons 4.0)

using Steamworks;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class AchiMan : MonoBehaviour
{
    [System.Serializable]
    public struct achiID
    {
        public string steamID;
        public string androidID;
    }
    [SerializeField] achiID[] achiIDs;

    public int platform;
    bool isAchiUnlocked;

    public void UnlockAchi(int _index)
    {
        isAchiUnlocked = false;
        switch (platform)
        {
            case 0: //Steam
                TestSteamAchi(achiIDs[_index].steamID);
                if (!isAchiUnlocked)
                {
                    SteamUserStats.SetAchievement(achiIDs[_index].steamID);
                    SteamUserStats.StoreStats();
                }
                break;
            default:
                break;
        }
    }

    void TestSteamAchi(string _id)
    {
        SteamUserStats.GetAchievement(_id, out isAchiUnlocked);
    }


    /*
    public void RelockAchi(int _index)
    {
        TestSteamAchi(achiIDs[_index].steamID);
        Debug.Log($"Achi with ID: {achiIDs[_index].steamID} unlocked = {isAchiUnlocked}");
        if (isAchiUnlocked)
        {
            SteamUserStats.ClearAchievement(achiIDs[_index].steamID);
            SteamUserStats.StoreStats();
        }
    }
    */
}

The achiID struct contains a section for steamID and androidID by default but simply add/remove any integrations you need. You set the key’s in the editor via the achiIDs variable, and then just pass in an index to UnlockAchi when you want to unlock an achievement.

using Steamworks;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public class LeadMan : MonoBehaviour
{
    private  SteamLeaderboard_t s_currentLeaderboard;
    private  bool s_initialized = false;
    private  CallResult<LeaderboardFindResult_t> m_findResult = new CallResult<LeaderboardFindResult_t>();
    private  CallResult<LeaderboardScoreUploaded_t> m_uploadResult = new CallResult<LeaderboardScoreUploaded_t>();
    private  CallResult<LeaderboardScoresDownloaded_t> m_downloadResult = new CallResult<LeaderboardScoresDownloaded_t>();

    public struct LeaderboardData
    {
        public string username;
        public int rank;
        public int score;
    }
    List<LeaderboardData> LeaderboardDataset;

    public  void UpdateScore(int score)
    {
        if (!s_initialized)
        {
            Debug.LogError("Leaderboard not initialized");
        }
        else
        {
            //Change upload method to 
            SteamAPICall_t hSteamAPICall = SteamUserStats.UploadLeaderboardScore(s_currentLeaderboard, ELeaderboardUploadScoreMethod.k_ELeaderboardUploadScoreMethodKeepBest, score, null, 0);
            m_uploadResult.Set(hSteamAPICall, OnLeaderboardUploadResult);
        }
    }

    private void Awake()
    {
        SteamAPICall_t hSteamAPICall = SteamUserStats.FindLeaderboard("Highscores");
        m_findResult.Set(hSteamAPICall, OnLeaderboardFindResult);
    }

    private void OnLeaderboardFindResult(LeaderboardFindResult_t pCallback, bool failure)
    {
        Debug.Log($"Steam Leaderboard Find: Did it fail? {failure}, Found: {pCallback.m_bLeaderboardFound}, leaderboardID: {pCallback.m_hSteamLeaderboard.m_SteamLeaderboard}");
        s_currentLeaderboard = pCallback.m_hSteamLeaderboard;
        s_initialized = true;
    }

     private void OnLeaderboardUploadResult(LeaderboardScoreUploaded_t pCallback, bool failure)
    {
        Debug.Log($"Steam Leaderboard Upload: Did it fail? {failure}, Score: {pCallback.m_nScore}, HasChanged: {pCallback.m_bScoreChanged}");
    }

    //change ELeaderboardDataRequest to get a different set (focused around player or global)
    public void GetLeaderBoardData(ELeaderboardDataRequest _type = ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobal, int entries = 14)
    {
        SteamAPICall_t hSteamAPICall;
        switch (_type)
        {
            case ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobal:
                hSteamAPICall = SteamUserStats.DownloadLeaderboardEntries(s_currentLeaderboard, _type, 1, entries);
                m_downloadResult.Set(hSteamAPICall, OnLeaderboardDownloadResult);
                break;
            case ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobalAroundUser:
                hSteamAPICall = SteamUserStats.DownloadLeaderboardEntries(s_currentLeaderboard, _type, -(entries/2), (entries/2));
                m_downloadResult.Set(hSteamAPICall, OnLeaderboardDownloadResult);
                break;
            case ELeaderboardDataRequest.k_ELeaderboardDataRequestFriends:
                hSteamAPICall = SteamUserStats.DownloadLeaderboardEntries(s_currentLeaderboard, _type, 1, entries);
                m_downloadResult.Set(hSteamAPICall, OnLeaderboardDownloadResult);
                break;
        }
        //Note that the LeaderboardDataset will not be updated immediatly (see callback below)
    }

    private void OnLeaderboardDownloadResult(LeaderboardScoresDownloaded_t pCallback, bool failure)
    {
        Debug.Log($"Steam Leaderboard Download: Did it fail? {failure}, Result - {pCallback.m_hSteamLeaderboardEntries}");
        LeaderboardDataset = new List<LeaderboardData>();
        //Iterates through each entry gathered in leaderboard
        for (int i = 0; i < pCallback.m_cEntryCount; i++)
        {
            LeaderboardEntry_t leaderboardEntry;
            SteamUserStats.GetDownloadedLeaderboardEntry(pCallback.m_hSteamLeaderboardEntries, i, out leaderboardEntry, null, 0);
            //Example of how leaderboardEntry might be held/used
            LeaderboardData lD;
            lD.username = SteamFriends.GetFriendPersonaName(leaderboardEntry.m_steamIDUser);
            lD.rank = leaderboardEntry.m_nGlobalRank;
            lD.score = leaderboardEntry.m_nScore;
            LeaderboardDataset.Add(lD);
            Debug.Log($"User: {lD.username} - Score: {lD.score} - Rank: {lD.rank}");
        }
        //This is the callback for my own project - function is asynchronous so it must return from here rather than from GetLeaderBoardData
        FindObjectOfType<HighscoreUIMan>().FillLeaderboard(LeaderboardDataset);
    }
}

The LeadMan is a little more complicated. It finds the Leaderboard “Highscores” in the Awake function so be sure to change the Script Execution Order under Project Settings and place this script to execute time after Steam has been initialized.

Rather than using the usual CallBack that’s called each Update in the SteamManager, it works using CallResults so it works asynchronously. This means you can’t just set GetLeaderBoardData to return something, because it might be a few frames before OnLeaderboardDownloadResult actually goes through.
Instead, you want to put a call back at the end of OnLeaderboardDownloadResult itself. You’ll notice that the end of the script does this, passing the LeaderboardDataset that was just filled into my HighscoreUIMan (script below).

By changing the ELeaderboardDataRequest you can get different set of Leaderboard entries, I use three separate button to call GetLeaderBoardData, each passing in a different type and causing the LeaderboardDataset so the user can see global rank, friend’s ranks, and their own rank.
The number of entries can also be passed into the function.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.UI;

public class HighscoreUIMan : MonoBehaviour
{
    [SerializeField] Transform holder;
    [SerializeField] GameObject highscorePrefab;
    List<GameObject> highscorePrefabs = new List<GameObject>();

    public void BtnBeginFillLeaderboardLocal()
    {
        FindObjectOfType<LeadMan>().GetLeaderBoardData(Steamworks.ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobalAroundUser, 14);
    }

    public void BtnBeginFillLeaderboardGlobal()
    {
        FindObjectOfType<LeadMan>().GetLeaderBoardData(Steamworks.ELeaderboardDataRequest.k_ELeaderboardDataRequestGlobal, 14);
    }

    public void BtnBeginFillLeaderboardFriends()
    {
        FindObjectOfType<LeadMan>().GetLeaderBoardData(Steamworks.ELeaderboardDataRequest.k_ELeaderboardDataRequestFriends, 14);
    }

    public void FillLeaderboard(List<LeadMan.LeaderboardData> lDataset)
    {
        Debug.Log("filling leaderboard");
        foreach(GameObject g in highscorePrefabs)
        {
            Destroy(g);
        }

        foreach (LeadMan.LeaderboardData lD in lDataset)
        {
            GameObject g = Instantiate(highscorePrefab, holder);
            highscorePrefabs.Add(g);
            FillHighscorePrefab(g, lD);
        }
    }

    void FillHighscorePrefab(GameObject _prefab, LeadMan.LeaderboardData _lData)
    {
        _prefab.transform.Find("username").GetComponent<Text>().text = _lData.username;
        _prefab.transform.Find("score").GetComponent<Text>().text = _lData.score.ToString();
        _prefab.transform.Find("rank").GetComponent<Text>().text = _lData.rank.ToString();
    }
}

This was not in the video but it’s quite straight forward. The FillLeaderboard function instantiates a number of prefabs (each containing 3 text fields: username, score, rank) to hold the data passed in from OnLeaderboardDownloadResult. It uses the vertical/horizontal layout groups to ensure everything fits correctly (see below)

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.