The Godot Barn
Sign in
Sign in
Home
News & updates
Explore
Articles
Snippets
Shaders
Themes
Submit content
Sign in to submit content
Give or get help
Tutorials
Questions
Conversations
Coming soon!
GodotSteam Tutorials - Leaderboards
1
Description
"This tutorial will cover setting up leaderboards for your games.\r\n\r\n??? guide \"Relevant GodotSteam classes and functions\"\r\n\t* [Friend class](https:\/\/godotsteam.com\/classes\/friends.md)\r\n\t\t* [avatar_loaded](https:\/\/godotsteam.com\/classes\/friends.md#avatar_loaded)\r\n\t* [Main class](https:\/\/godotsteam.com\/classes\/main.md)\r\n\t\t* [set_leaderboard_details_max()](https:\/\/godotsteam.com\/classes\/main.md#set_leaderboard_details_max)\r\n\t* [RemoteStorage class](https:\/\/godotsteam.com\/classes\/remote_storage.md)\r\n\t\t* [fileExists()](https:\/\/godotsteam.com\/classes\/remote_storage.md#fileexists)\r\n\t\t* [fileShare()](https:\/\/godotsteam.com\/classes\/remote_storage.md#fileshare)\r\n\t\t* [fileWriteAsync()](https:\/\/godotsteam.com\/classes\/remote_storage.md#filewriteasync)\r\n\t* [User Stats class](https:\/\/godotsteam.com\/classes\/user_stats.md)\r\n\t\t* [attachLeaderboardUGC()](https:\/\/godotsteam.com\/classes\/user_stats.md#attachleaderboardugc)\r\n\t\t* [downloadLeaderboardEntries()](https:\/\/godotsteam.com\/classes\/user_stats.md#downloadleaderboardentries)\r\n\t\t* [downloadLeaderboardEntriesForUsers()](https:\/\/godotsteam.com\/classes\/user_stats.md#downloadleaderboardentriesforusers)\r\n\t\t* [getLeaderboardName()](https:\/\/godotsteam.com\/classes\/user_stats.md#getleaderboardname)\r\n\t\t* [findLeaderboard()](https:\/\/godotsteam.com\/classes\/user_stats.md#findleaderboard)\r\n\t\t* [findOrCreateLeaderboard()](https:\/\/godotsteam.com\/classes\/user_stats.md#findorcreateleaderboard)\r\n\t\t* [uploadLeaderboardScore()](https:\/\/godotsteam.com\/classes\/user_stats.md#uploadleaderboardscore)\r\n\t\t* [leaderboard_find_result](https:\/\/godotsteam.com\/classes\/user_stats.md#leaderboard_find_result)\r\n\t\t* [leaderboard_score_uploaded](https:\/\/godotsteam.com\/classes\/user_stats.md#leaderboard_score_uploaded)\r\n\t\t* [leaderboard_scores_downloaded](https:\/\/godotsteam.com\/classes\/user_stats.md#leaderboard_scores_downloaded)\r\n???\r\n\r\n!!! warning Note\r\nYou may want to [double-check our Initialization tutorial](https:\/\/thegodotbarn.com\/contributions\/tutorial\/49\/godotsteam-tutorials-initializing-steam) to set up initialization and callbacks functionality if you haven't done so already.\r\n!!!\r\n\r\n## Set Up{.block}\r\n\r\n### Variables\r\n\r\nFirst we will set up some basic variables that this tutorial uses:\r\n\r\n```gdscript\r\nvar current_handle: int = 0\r\nvar leaderboard_handles: Dictionary[StringName, int] = {\r\n\t\"leaderboard1_api_name\": 0,\r\n\t\"leaderboard2_api_name\": 0,\r\n\t\"leaderboard3_api_name\": 0,\r\n\t}\r\n```\r\n\r\nThe **current_handle** refers to the leaderboard handle we are actively using to pull entries or attach UGC to. This will be one of the integers in our **leaderboard_handles** dictionary. This variable is optional as GodotSteam actually stores the current leaderboard handle internally and can be called with **Steam.leaderboard_handle**.\r\n\r\nSpeaking of, the **leaderboard_handles** dictionary is simply just the leaderboard's Steamworks API name, set in [the Steamworks partner site](https:\/\/partner.steamgames.com), and its current handle.\r\n\r\n### Signals\r\n\r\nNow let's get our signals set up:\r\n\r\n::: tabs\r\n@tab:active Godot 2.x, 3.x\r\n```gdscript\r\nfunc _ready()-> void:\r\n\tSteam.connect(\"avatar_loaded\", self, \"_on_avatar_loaded\")\r\n\tSteam.connect(\"leaderboard_find_result\", self, \"_on_leaderboard_find_result\")\r\n\tSteam.connect(\"leaderboard_score_uploaded\", self, \"_on_leaderboard_score_uploaded\")\r\n\tSteam.connect(\"leaderboard_scores_downloaded\", self, \"_on_leaderboard_scores_downloaded\")\r\n```\r\n\r\n@tab Godot 4.x\r\n```gdscript\r\nfunc _ready()-> void:\r\n\tSteam.avatar_loaded.connect(_on_avatar_loaded)\r\n\tSteam.leaderboard_find_result.connect(_on_leaderboard_find_result)\r\n\tSteam.leaderboard_score_uploaded.connect(_on_leaderboard_score_uploaded)\r\n\tSteam.leaderboard_scores_downloaded.connect(_on_leaderboard_scores_downloaded)\r\n```\r\n:::\r\n\r\nIf you decide you want to attach UGC to your leaderboards, you will need a few more signals. Attaching UGC can happen one of two ways: using Steam Cloud features with the Remote Storage class or creating items with the UGC class. We will get into [more details in the Uploading UGC To Leaderboard section.](#uploading-ugc-to-leaderboard)\r\n\r\n## Getting Handles{.block}\r\n\r\nUnless you are creating leaderboards on the fly, we will just use **findLeaderboard()** which you will probably want to trigger once your leaderboard scene loads in:\r\n\r\n```gdscript\r\nSteam.findLeaderboard( achievement_handles.keys()[0] )\r\n```\r\n\r\nThis will grab the first leaderboard API name in our dictionary and look for the handle.\r\n\r\n!!! warning Note\r\nWhen using **findOrCreateLeader()**, as opposed to **findLeaderboard()**, make sure not to set the sort method or display types as none. Avoid these enums: LEADERBOARD_SORT_METHOD_NONE and LEADERBOARD_DISPLAY_TYPE_NONE.\r\n!!!\r\n\r\nOnce Steam finds your leaderboard it will pass back the handle to the **leaderboard_find_result** callback. The **\\_on_leaderboard_find_result()** function that it is connected to it should look something like this:\r\n\r\n```gdscript\r\nfunc _on_leaderboard_find_result(new_handle: int, was_found: int) -> void:\r\n\tif was_found != 1:\r\n\t\tprint(\"Leaderboard handle cound not be found: %s\" % was_found)\r\n\t\treturn\r\n\r\n\tcurrent_handle = new_handle\r\n\r\n\tvar api_name: String = Steam.getLeaderboardName(new_handle)\r\n\tleaderboard_handles[api_name] = current_handle\r\n\r\n\tprint(\"Leaderboard %s handle found: %s\" % [api_name, current_handle])\r\n```\r\n\r\nThe call to **getLeaderboardName()** will give us back the API name we just called (or should) and lets us update the leaderboard handle correctly. Big thanks to **TriMay** for the bit about getting the leaderboard name in the callback so we can assign the handle to the right leaderboard in our dictionary.\r\n\r\nOnce you have this handle you can use all the additional functions.\r\n\r\n### Getting Leaderboards In A Loop\r\n\r\nSome users have wanted to pull all leaderboard handles from the get-go. While it is possible, it definitely feels like it wasn't intended to work that way. However, these bits of code should help grab all the leaderboard handles.\r\n\r\n```gdscript\r\nfunc get_handles_in_loop() -> void:\r\n\tfor this_leaderboard in leaderboard_handles.keys():\r\n\t\tSteam.findLeaderboard(this_leaderboard)\r\n\t\tawait Steam.leaderboard_find_result\r\n```\r\n\r\nThis should loop through our dictionary, request each handle, and wait for Steam retrieve it then assign it using our callback function previously.\r\n\r\nOriginally, I had a timer for the **await** line `await get_tree().create_timer(0.5).timeout` which also worked pretty good. I did find that if you set the timeout to 0.25, it was too fast and some of the leaderboards would not be found. 0.5 was a sweet-spot that felt instant and reliably worked during testing.\r\n\r\n## Uploading Scores{.block}\r\n\r\nBefore we can download scores, we need to upload them. The function itself is pretty simple:\r\n\r\n```gdscript\r\nSteam.uploadLeaderboardScore( score, keep_best, details_array, leaderboard_handle )\r\n```\r\n\r\nObviously, **score** is the new score we want to set. Depending on what you choose for **keep_best** this may not work if you pass a lower score than the previously set one.\r\n\r\n**keep_score** sets whether or not to keep the higher \/ better score between the existing one and the one you are trying to update this leaderboard entry with.\r\n\r\n**details_array** are details about the score but are completely optional. If you don't add any, just pass a blank array; if you do add some, these must be integers in a PackedInt32Array (or PoolIntArray in Godot 3.x versions). They essentially can be anything and there are some additional notes and wizardry in the [Passing Extra Details section below.](#passing-extra-details) Even ***Valve*** says in their docs:\r\n\r\n> Details are optional game-defined information which outlines how the user got that score. For example if it's a racing style time based leaderboard you could store the timestamps when the player hits each checkpoint. If you have collectibles along the way you could use bit fields as booleans to store the items the player picked up in the playthrough.\r\n\r\nLastly, **leaderboard_handles** is the leaderboard handle we are updating. You do not have to pass the handle if you want to use the internally-stored one.\r\n\r\n!!! warning Note\r\nAccording to Valve's docs: \"uploading scores to Steam is rate limited to 10 uploads per 10 minutes and you may only have one outstanding call to this function at a time.\"\r\n!!!\r\n\r\n### Passing Extra Details\r\n\r\nIf you want to pass extra details, here are some neat hints from ***sepTN***:\r\n\r\n> You can add small data as detail, for example embedding the character name (as a string) into that leaderboard entry. If you try to put detail that has a bigger size than possible, it will simply ignore it. To retrieve it, you need to process it again because Steam will spit out arrays (PackedInt32Array).\r\n\r\nHere is some code that was shared:\r\n\r\n::: tabs\r\n@tab:active Godot 2.x, 3.x\r\n```gdscript\r\n# Godot 2 and 3 have no equivalent for to_int32_array I am aware of. Any corrections welcome!\r\n\r\nSteam.uploadLeaderboardScore(score, keep_best, var2bytes(details), handle)\r\n```\r\n\r\n@tab Godot 4.x\r\n```gdscript\r\nSteam.uploadLeaderboardScore(score, keep_best, var_to_bytes(details).to_int32_array(), handle)\r\n```\r\n:::\r\n\r\nWhen you download these scores and need to get our score details back to something readable, make sure you reverse this with **bytes_to_var()** or **bytes2var()** on Godot 4 and Godot 3 respectively.\r\n\r\nIf you want the score to be something like milliseconds, as one community member did, you will want to multiply that value by 1000 and submit it as an int.\r\n\r\n### Upload Callback\r\n\r\nOnce you pass a score to Steam, you should receive a callback from **leaderboard_score_uploaded**. This will trigger our **\\_on_leaderboard_score_uploaded()** function:\r\n\r\n```gdscript\r\nfunc _on_leaderboard_score_uploaded(success: int, this_handle: int, this_score: Dictionary) -> void:\r\n\tif success == 0:\r\n\t\tprint(\"Failed to upload score to leaderboard %s\" % this_handle)\r\n\t\treturn\r\n\r\n\tprint(\"Successfully uploaded score to leaderboard %s\" % this_handle)\r\n```\r\n\r\nFor the most part you are just looking for a success of 1 to tell that it worked. However, you may with to use the additional variables passed back by the signal for logic in your game. They are contained in the dictionary called **this_score** which contains these keys:\r\n\r\n- **score:** the new score as it stands\r\n- **score_changed:** if the score was changed (0 if false, 1 if true)\r\n- **new_rank:** the new global rank of this player\r\n- **prev_rank:** the previous rank of this player\r\n\r\n## Downloading Scores{.block}\r\n\r\n### Setting Max Details Or Not\r\n\r\nBefore we pull any leaderboard entries, you may want to set the maximum amount of details each entries contains in that **details_array** we saw earlier. If you do not save any details with the scores you can safely ignore this though.\r\n\r\nBy default, this is set to the maximum of 256; in GodotSteam versions 4.6 or older this will actually be set to 0. The value is stored internally by GodotSteam and can be accessed by **Steam.leaderboard_details_max**.\r\n\r\nYou can set this value to the number of details you expect to get back from this leaderboard but as long as the **leaderboard_details_max** is set to at least the highest number of details you should be fine. You can use **set_leaderboard_details_max()** to change this.\r\n\r\n### Getting Scores\r\n\r\nIn most cases you'll want to use **downloadLeaderboardEntries()** useless you just want to grab leaderboard entries for a specific group of players; in that case, you can use **downloadLeaderboardEntriesForUsers()** and pass an array of users' Steam IDs to it. Both will respond with the same callback:\r\n\r\n```gdscript\r\nSteam.downloadLeaderboardEntries( start_index, end_index, data_request_type, leaderboard_handle )\r\n```\r\n\r\n**start_entry** is the index to start at, relative to the value we set for **data_request_type**.\r\n\r\n**end_entry** is the index to end at, relative to the value we set for **data_request_type**.\r\n\r\n**data_request_type** is what kind of leaderboards you want to get, [you can read more details about it in the SDK documentation](https:\/\/partner.steamgames.com\/doc\/api\/ISteamUserStats#ELeaderboardDataRequest){ target=\"\\_blank\" }. For a quick overview:\r\n\r\nLeaderboard Data Request Enums \t\t\t\t| Values\t| Descriptions\r\n--- \t\t\t\t\t\t\t\t\t\t| --- \t\t| ---\r\nLEADERBOARD_DATA_REQUEST_GLOBAL\t\t\t\t| 0\t\t\t| Used for a sequential range by leaderboard rank.\r\nLEADERBOARD_DATA_REQUEST_GLOBAL_AROUND_USER\t| 1\t\t\t| Used to get entries relative to the user's entry. You may want to use negatives for the start to get entries before the user's.\r\nLEADERBOARD_DATA_REQUEST_FRIENDS\t\t\t| 2\t\t\t| Used to get all entries for friends for the current user. Start and end arguments are ignored.\r\nLEADERBOARD_DATA_REQUEST_USERS\t\t\t\t| 3\t\t\t| Used internally by Steam, **do not use this**.\r\n\r\nFinally, **leaderboard_handle** is the leaderboard we are getting entries for. Just like uploading, downloading scores does not require a leaderboard handle to be included if you are using the internally-stored one.\r\n\r\nIf you want to just grab the leaderboard entries for a specific group of players, you can use **downloadLeaderboardEntriesForUsers()** instead:\r\n\r\n```gdscript\r\nvar user_array: Array[int] = [\r\n\tsteam_id1,\r\n\tsteam_id2,\r\n\tsteam_id3,\r\n\tsteam_id4\r\n\t]\r\n\r\nSteam.downloadLeaderboardEntriesForUsers( user_array, leaderboard_handle )\r\n```\r\n\r\nWe just need an array of Steam IDs, shown above as **user_array**. Then we just (optionally) pass the **leaderboard_handle** for the leaderboard we are getting entries for.\r\n\r\n### Download Callback\r\n\r\nIn either case, after you request leaderboard entries, you should receive a **leaderboard_scores_downloaded** callback which will trigger our **\\_on_leaderboard_scores_downloaded()** function. That function should look similar to this:\r\n\r\n```gdscript\r\nfunc _on_leaderboard_scores_downloaded(this_handle: int, these_results: Array) -> void:\r\n\tif these_results.size() == 0:\r\n\t\tprint(\"No results found.\")\r\n\t\treturn\r\n\r\n\tif current_handle != this_handle:\r\n\t\tprint(\"Oops, we got entries for a different handle: %s\" % this_handle)\r\n\t\treturn\r\n\r\n\t# Add logic to display results\r\n\tfor this_result in result:\r\n\t\t# Use each entry that is returned\r\n```\r\n\r\n**this_handle** is the handle the accompanying entries are for. This should match the handle we called with.\r\n\r\n**these_results** are all the leaderboard entries. Each entry in the array is actually a dictionary containing the following keys:\r\n\r\n- **score:** this user's score\r\n- **steam_id:** this user's Steam ID; you can use this to get their avatar, name, etc.\r\n- **global_rank:** obviously their global ranking for this leaderboard\r\n- **ugc_handle:** handle for any UGC that is attached to this entry\r\n- **details:** any details you stored with this entry for later use\r\n\r\n### Leaderboard Entry\r\n\r\nFor Skillet, we use a [separate scene which gets instanced for each leaderboard entry](https:\/\/codeberg.org\/godotsteam\/skillet\/src\/branch\/demo\/src\/components\/leaderboards\/leaderboards_leader.tscn) in the for loop above. First we will want to amend our main leaderboard script with the following:\r\n\r\n```gdscript\r\n@onready var leaderboard_entry: Object = preload(\"res:\/\/leaderboard_entry.tscn\")\r\n\r\n\r\nfunc _ready()-> void:\r\n\tSteam.avatar_loaded.connect(_on_avatar_loaded)\r\n\r\n\r\nfunc _on_avatar_loaded(avatar_id: int, avatar_size: int, avatar_data: Array) -> void:\r\n\tvar avatar_image: Image = Image.create_from_data(avatar_size, avatar_size, false, Image.FORMAT_RGBA8, avatar_data)\r\n\r\n\tif avatar_size > 128:\r\n\t\tavatar_image.resize(128, 128, Image.INTERPOLATE_LANCZOS)\r\n\tvar avatar_texture: ImageTexture = ImageTexture.create_from_image(avatar_image)\r\n\r\n\tget_node(\"List\/Leader%s\" % avatar_id).setup_texture(avatar_texture)\r\n```\r\n\r\nNext we will want to amend our **\\_on_leaderboard_scores_downloaded()** function's result for loop with the following:\r\n\r\n```gdscript\r\nfor this_result in these_results:\r\n\tvar this_leader: Object = leaderboard_entry.instantiate()\r\n\tthis_leader.setup_entry(this_result)\r\n\tthis_leader.name = \"Leader%s\" % this_result['steam_id']\r\n\t$List.add_child(this_leader)\r\n```\r\n\r\nOur leaderboard entry object is just a base HBoxContainer with some Labels, a TextureRect, and Button:\r\n\r\n{ loading=lazy }\r\n\r\nIt has a script which we pass the **these_results** dictionary to **setup_entry()**:\r\n\r\n```gdscript\r\nfunc setup_entry(these_details: Dictionary) -> void:\r\n\tSteam.getPlayerAvatar(these_details['steam_id'])\r\n\t%Avatar.texture = null\r\n\t%Name.text = Steam.getFriendPersonaName(these_details['steam_id'])\r\n\t%Rank.text = str(these_details['global_rank'])\r\n\t%Score.text = str(these_details['score'])\r\n\t%Details.text = str(these_details['details'])\r\n\r\n\r\nfunc setup_texture(avatar_texture: ImageTexture) -> void:\r\n\t%Avatar.texture = avatar_texture\r\n```\r\n\r\nWhen the entry is added and updated with details, it will call **getPlayerAvatar()** to pull the user's avatar. As you may have noticed in our first amendment, this just calls the **setup_texture()** function in the appropriate leaderboard entry.\r\n\r\n## Uploading UGC to Leaderboard{.block}\r\n\r\nThere are two methods for adding UGC to your leaderboard entries: through Remote Storage's **fileWriteAsync()** and **fileShare()** methods or UGC's item creation methods.\r\n\r\nFor this tutorial, we will be using the Remote Storage method; the UGC method will be covered in the Workshop tutorial. First you will need to setup signals:\r\n\r\n::: tabs\r\n@tab:active Godot 2.x, 3.x\r\n```gdscript\r\nfunc _ready() -> void:\r\n\tSteam.connect(\"file_share_result\", self, \"_on_file_share_result\")\r\n\tSteam.connect(\"file_write_async_complete\", self, \"on_file_write_async_complete\")\r\n\tSteam.connect(\"leaderboard_ugc_set\", self, \"on_leaderboard_ugc_set\")\r\n```\r\n\r\n@tab Godot 4.x\r\n```gdscript\r\nfunc _ready() -> void:\r\n\tSteam.file_share_result.connect(_on_file_share_result)\r\n\tSteam.file_write_async_complete.connect(_on_file_write_async_complete)\r\n\tSteam.leaderboard_ugc_set.connect(_on_leaderboard_ugc_set)\r\n```\r\n:::\r\n\r\nNext we need to convert our file to a PackedByteArray and pass it to **fileWriteAsync()**:\r\n\r\n```gdscript\r\nvar ugc_file_name: String = \"this_ugc_file.png\"\r\nvar ugc_path: String = \"\/path\/to\/file\/%s\" % ugc_file_name\r\n\r\nvar data: PackedByteArray = FileAccess.get_file_as_bytes( ugc_path )\r\nSteam.fileWriteAsync(ugc_file_name, data, data.size())\r\n```\r\n\r\nFile will be created in location `...\\Steam\\userdata\\
\\
\\remote` and should be pushed to Steam Cloud. Obviously it doesn't have to be a .png, it can be any kind of file you want to share for this. We will check this worked and then share it in the **fileWriteAsync()** callback function:\r\n\r\n```gdscript\r\nfunc on_file_write_async_complete(write_result: int) -> void:\r\n\tif write_result != Steam.RESULT_OK:\r\n\t\tprint(\"Failed to write UGC file: %s\" % write_result)\r\n\t\treturn\r\n\tprint(\"Sharing UGC file\")\r\n\r\n\tif not Steam.fileExists(ugc_file_name):\r\n\t\tprint(\"Failed does not exist in Steam Cloud\")\r\n\t\treturn\r\n\r\n\tSteam.fileShare(ugc_file_name)\r\n```\r\n\r\nWe check if it even exists in Steam Cloud first, with **fileExists()**. If so, then **fileShare()** is used to mark existing files to be shared that are present in remote local directory \/ Steam Cloud.\r\n\r\nIn our callback function from **fileShare()** we will finally attach our UGC to the leaderboard:\r\n\r\n```gdscript\r\nfunc on_file_share_result(share_result: int, this_handle, this_name: String ) -> void:\r\n\tif this_result != Steam.RESULT_OK:\r\n\t\tprint(\"Failed to share UGC file %s: %s\" % [this_name, share_result])\r\n\t\treturn\r\n\r\n\tprint(\"Successfully shared UGC file %s\" % this_name)\r\n\tSteam.attachLeaderboardUGC(this_handle, current_handle)\r\n```\r\n\r\nBe careful here with all the handle names, the first one for **attachLeaderboard()** is the UGC handle we are sharing and the second is the leaderboard handle we are attaching this UGC to. Lastly we will check the callback for **attachLeaderboard()**:\r\n\r\n```gdscript\r\nfunc on_leaderboard_ugc_set(this_handle: int, result: Steam.Result) -> void:\r\n\tif this_result == Steam.RESULT_TIMEOUT:\r\n\t\tprint(\"UGC took too long ot upload; try again.\")\r\n\t\treturn\r\n\tif this_result == Steam.RESULT_INVALID_PARAM:\r\n\t\tprint(\"Leaderboard handle was invalid: %s\" % this_handle)\r\n\t\treturn\r\n\tif this_result != Steam.RESULT_OK:\r\n\t\tprint(\"Random error: %s\" % this_result)\r\n\t\treturn\r\n\r\n\tprint(\"Leaderboard UGC was set: %s\" % this_handle)\r\n```\r\n\r\nThis UGC handle that gets returned should match what you get when you download entries. This can be used with a variety of methods to retrieve the UGC which we will talk about in the [Retrieveing UGC section.](#retrieving-ugc)\r\n\r\n!!! warning Mangled Handles\r\nAt the time of writing, I didn't find a way to correctly represent the UGC handle so it shows up as a negative integer in Godot; most likely something with how Godot handles large numbers. This handle still works perfectly fine.\r\n!!!\r\n\r\n!!! warning Callback Changes\r\nIn GodotSteam 4.15 \/ 3.29 or earlier, the **leaderboard_ugc_set** callback foolishly sends the result back as a String instead of a Steam result enum value. This is fixed in GodotSteam 4.16 \/ 3.30 and later.\r\n!!!\r\n\r\n## Retrieving UGC{.block}\r\n\r\nFor our example, we will add a button to every leaderboard entry that has UGC attached. We will modify our previous **setup_entry()** function in our **leaderboard_entry.gd**:\r\n\r\n```gdscript\r\nfunc setup_entry(these_details: Dictionary) -> void:\r\n\tSteam.getPlayerAvatar(these_details['steam_id'])\r\n\t%Avatar.texture = null\r\n\t%Name.text = Steam.getFriendPersonaName(these_details['steam_id'])\r\n\t%Rank.text = str(these_details['global_rank'])\r\n\t%Score.text = str(these_details['score'])\r\n\t%Details.text = str(these_details['details'])\r\n\t# Used for attached UGC\r\n\t%UGC.visible = true if these_details['ugc_handle'] != 0 else false\r\n\t%UGC.pressed.connect(_on_ugc_pressed.bind(these_details['ugc_hanadle']))\r\n```\r\n\r\nWe will then create the related function **\\_on_ugc_pressed()** which takes our handle and grabs the UGC:\r\n\r\n```gdscript\r\nfunc _on_ugc_pressed(this_ugc_handle: int) -> void:\r\n\tprint(\"UGC Details: %s\" % Steam.getUGCDetails(this_ugc_handle))\r\n\r\n\tvar ugc_absolute_path: String = ProjectSettings.globalize_path(\"user:\/\/leaderboards\/ugc\/%s.png\" % this_ugc_handle)\r\n\r\n\tSteam.ugcDownloadToLocation(this_ugc_handle, ugc_absolute_path, 0)\r\n```\r\n\r\nThe call to **ugcDownload()** works and responds with a successful callback; however, I cannot seem to find where the file downloads. So we will be using **ugcDownloadToLocation()** to grab and place the UGC content where ever you want. We will be putting them in the **user:\/\/** folder.\r\n\r\nOur **ugcDownloadToLocation()** will trigger a **download_ugc_result** callback and our **\\_on_download_ugc_result()** function:\r\n\r\n```gdscript\r\nfunc _on_download_ugc_result(result: Steam.Result, download_data: Dictionary) -> void:\r\n\tif result != Steam.RESULT_OK:\r\n\t\tprint(\"Failed to download: %s\" % result)\r\n\t\treturn\r\n\r\n\tvar new_ugc_image: Image = Image.new()\r\n\tnew_ugc_image.load(ProjectSettings.globalize_path(\"user:\/\/leaderboards\/ugc\/%s.png\" % download_data['handle']))\r\n\tvar new_ugc_texture: ImageTexture = ImageTexture.create_from_image(new_ugc_image)\r\n\t%UGCImage.texture = new_ugc_texture\r\n\t%UGC.visible = true\r\n```\r\n\r\nThis will load in our image and set it as the texture for our UGC modal that displays.\r\n\r\n{ loading=lazy }\r\n\r\nAnd that is our basic setup for leaderboards!\r\n\r\n## Possible Oddities{.block}\r\n\r\nA user in our Discord noted that sometimes **downloadLeaderboardEntriesForUsers()** would trigger a callback but have zero entries. Oddly, they reported that creating a second leaderboard then deleting the first one would fix this. While I don't understand why this would be the case, in the event you come across this, perhaps try this solution!\r\n\r\n## Additional Resources{.block}\r\n\r\n### Video Tutorials\r\n\r\nPrefer video tutorials? Feast your eyes and ears!\r\n\r\n[youtube: How To Build Leaderboards Out](https:\/\/www.youtube.com\/watch?v=VCwNxfYZ8Cw&t=3394s){ data-author=\"FinePointCGI\" }\r\n\r\n[youtube: Godot 4 Steam Leaderboards](https:\/\/www.youtube.com\/watch?v=51qre_hodZI){ data-author=\"Gwizz\" }\r\n\r\n### Example Project\r\n\r\n[Later this year you can see this tutorial in action with more in-depth information by checking out our upcoming free-to-play game Skillet on GitHub.](https:\/\/codeberg.org\/godotsteam\/skillet) There you will be able to view of the code used which can serve as a starting point for you to branch out from."
Comments
Log in to post a comment
Licensed under the CC-BY license
See the full license details
Submitted by
Gramps
Table of contents
Tags
No tags applied
Share this tutorial
Share on Bluesky
Share on X / Twitter
or share this direct link:
https://thegodotbarn.com/contributions/tutorial/341/godotsteam-tutorials-leaderboards
Please wait ...
Okay
Okay
No
Yes
Okay