Skip to content

Tutorials - Stats And Achievements

By Gramps


At some point you may want to save statistics to Steam's database and / or use their achievement system. You will want to read up about these in Steam's documentation as I won't be covering the basics on how to set it up in the Steamworks back-end.

Relevant GodotSteam classes and functions

Note

You may want to double-check our Initialization tutorial to set up initialization and callbacks functionality if you haven't done so already.

Preparations

First thing, you'll want to set up your achievements and statistics in the Steamworks back-end. Most importantly, your game does not need to be approved for release nor be released but you want to publish these changes live. If not, they will not be exposed to the Steamworks API and you will get errors when trying to retrieve or set them. Once they have been published you can continue on with this tutorial.

Getting Statistics And Achievements

GodotSteam versions 3.28 / 4.14 and newer do not need anything extra to get the local user's statistics and achievements as they are synced automatically by the Steam client at boot thanks to changes in SDK 1.61. In older versions, we had to specifically request them and await a callback to use them.

If you are using a GodotSteam version that is 3.27 / 4.13 or earlier, you'll want to look at an older version of this tutorial to set up the functions and callbacks. Since we do not have the type of docs that use versioning, you'll have to check it out in repo form.

For games like Skillet, we want to store our statistic and achievement data somewhere to display and alter so we'll create two dictionary variables to do so, each using the Steam API names as their keys for easy usage:

var achievements: Dictionary[String, bool] = {
    "achieve1": false,
    "achieve2": false,
    "achieve3": false
    }
var statistics: Dictionary[String, int] = {
    "highscore": 0,
    "health": 0,
    "money": 0
    }

Next we will create two separate functions after initializtion that will pull and check our data called load_steam_stats() and load_steam_achievements():

func initialize_steam() -> void:
    var initialize_response: Dictionary = Steam.steamInitEx()
    print("Did Steam initialize?: %s" % initialize_response)

    if initialize_response['status'] > Steam.STEAM_API_INIT_RESULT_OK:
        print("Failed to initialize Steam: %s" % initialize_response['status'])
        return

    load_steam_stats()
    load_steam_achievements()

If you do not plan on displaying the achievements in-game, you can skip that part. Statistics you'll still need to pull to get the current value and modify it for updating though. We will start with statistics.

Loading Statistics

Fresh Pull Each Time

The cleanest way to get stats is just to pull them fresh from Steam each time the game is loaded. As mentioned earlier, we use the Steam API names as the dictionary keys so we can just loop through the statistics and pull them all:

func load_steam_stats() -> void:
    for this_stat in statistics.keys():
        var stat_value: int = Steam.getStatInt(this_stat)
        print("Retrieved %s stat: %s" % this_stat, stat_value)

        # Store the value in our dictionary
        statistics[this_stat] = stat_value
    print("Steam statistics loaded")

Load Local File And Compare

However, if we are saving the statistics to a local save file, we will want to check our local copy against Steam's and update everything accordingly:

# Process statistics
func load_steam_stats() -> void:
    for this_stat in statistics.keys():
        var steam_stat: int = Steam.getStatInt(this_stat)

        # The set_statistic function below in the Setting Statistics section
        if statistics[this_stat] > steam_stat:
            print("Stat mismatch; local value is higher (%s), replacing Steam value (%s)" % [statistics[this_stat], steam_stat])
            set_statistic(this_stat, statistics[this_stat])

        elif statistics[this_stat] < steam_stat:
            print("Stat mismatch; local value is lower (%s), replacing with Steam value (%s)" % [statistics[this_stat], steam_stat]))
            set_statistic(this_stat, steam_stat)

        else:
            print("Steam stat matches local file: %s" % this_stat)

    print("Steam statistics loaded")

If either value is higher, then we call the set_statistic() function to smooth that out; this function is down in the Settings Statistics section. If they are already equal, which they should be, nothing happens and we move on.

Loading Achievements

Very similarly to statistics, we can choose to pull them fresh each time or load in a local copy then compare them to what Steam has.

Fresh Pull Each Time

We simply pull the achievement with getAchievement() but check to make sure it even exists in the Steamworks back-end first:

# Process achievements
func load_steam_achievements() -> void:
    for this_achievement in achievements.keys():
        var steam_achievement: Dictionary[bool, bool] = Steam.getAchievement(this_achievement)

        # Does the achievement actually exist in the Steamworks back-end?
        if not steam_achievement['ret']:
            print("Steam does not have this achievement, ignoring it")
            continue

        achievements[this_achievement] = steam_achievement['achieved']
    print("Steam achievements loaded")

If that fails and you are sure you entered the achievement in the Steamworks back-end, there is a good chance you did not publish the changes yet and will have to do so for this to work.

Load Local File And Compare

Just like with statistics, we will compare our local save file data against what Steam has and make adjustments if necessary:

# Process achievements
func load_steam_achievements() -> void:
    for this_achievement in achievements.keys():
        var steam_achievement: Dictionary = Steam.getAchievement(this_achievement)

        # Does the achievement actually exist in the Steamworks back-end?
        if not steam_achievement['ret']:
            print("Steam does not have this achievement, defaulting to local value: %s" % achievements[this_achievement])
            continue

        if achievements[this_achievement] == steam_achievement['achieved']:
            print("Steam achievements match local file, skipping: %s" % this_achievement)
            continue

        set_achievement(this_achievement)

    print("Steam achievements loaded")

This only triggers our set_achievement() function if they are not the same values. You can see that function in the next section below.

Setting Achievements

Setting the achievements and statistics is pretty simple too. We'll start with achievements. You need to tell Steam the achievement is unlocked and then store it so it visually 'pops' in overlay. For this we will create two functions, one for achievements and the other for storing things and popping those achievements:

func set_achievement(this_achievement: String) -> void:
    if not achievements.has(this_achievement):
        print("This achievement does not exist locally: %s" % this_achievement)
        return
    achievements[this_achievement] = true

    if not Steam.setAchievement(this_achievement):
        print("Failed to set achievement: %s" % this_achievement)
        return

    print("Set acheivement: %s" % this_achievement)
    store_steam_data()


func store_steam_data() -> void:
    if not Steam.storeStats():
        print("Failed to store data on Steam, should be stored locally")
        return
    print("Data successfully sent to Steam")

If we don't call storeStats() the achievement pop-up won't trigger but the achievement should be recorded. However, we will still have to call storeStats() at some point to upload them.

Notes

In Godot 4.x with the introduction of Vulkan, depending on a variety of factors, your Steam overlay may not be working correctly so you will not see this pop. Rest assured, when run from the Steam client this will work correctly though.

Setting Statistics

Statistics follow a pretty similar process; both int and float based ones. How much and whether or not you are setting the full value or just an increment is all handled in the Steamworks back-end, outlined in Valve's docs here.

For our example, as we do in Skillet, we will just be incrementing the value by 1 so we don't need to worry about the previous stat value:

func set_statistic(this_stat: String, new_value: int = 1) -> void:
    if not Settings.statistics.has(this_stat):
        print("This statistic does not exist locally: %s" % this_stat)
        return
    # Set our local version
    Settings.statistics[this_stat] += new_value

    # Set Steam's version
    if not Steam.setStatInt(this_stat, new_value):
        print("Failed to set stat %s to: %s" % [this_stat, new_value])
        return

    print("Set statistics %s succesfully: %s" % [this_stat, new_value])
    store_steam_data()

You can see we call our store_steam_data() function like when setting achievements. This isn't necessary to do each call, you can just do this at the end when a user is quitting the game.

Notes

If your stat is configured as "increment only", setting the value to a new and lower value will cause you to get a false response when setting.

Resetting Stats And Achievements

Though it is best used for testing prior to release, I will often allow users to reset their statistics and achievements in the Options section of the game. To do this, you just need to call one specific function, resetAllStats(). Passing true to this function will reset the achievements too; in case you want to reset both:

func reset_statistics() -> void:
    print("Resetting all statistics and achievements for local user")
    if not Steam.resetAllStats(true):
        print("Failed to reset statistics and achievements")

Alternatively, you may want to clear an achievement instead of all your statistics. In this case, we will use clearAchievement() instead:

func reset_achievement(this_achievement: String) -> void:
    print("Resetting achievement %s" % this_achievement)
    if not Steam.clearAchievement(this_achievement):
        print("Failed to reset achievement: %s" % this_achievement)

More Yet

There are a variety of additional functions for statistics and achievements this tutorial does not cover like settings / getting stats and achievement for other users than the local one, getting the icons or descriptions from Steam for achievements, global stats, etc. You can read more about them in our docs here or check out Skillet's codebase to see some implmentations.

That's it for now!

Additional Resources

Video Tutorials

Prefer video tutorials? Feast your eyes and ears!

'Godot + Steam tutorial' by BluePhoenixGames

'How Achievements Are Done' by FinePointCGI

'Let's Talk About Statistics' by FinePointCGI

'Godot 4 Steam Achievements' by Gwizz

Example Project

You can view this tutorial in action with more in-depth information by checking out our upcoming free-to-play game Skillet on Codeberg. This link will take you to the direct file where this tutorial comes from but feel free to explore the rest of the project too!