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!