Making a simple online leaderboard in Godot with SilentWolf


Hello! Right now I'm working on an update for Jump3Match, a little game I made for the Trijam #111 if I recall correctly. Among other things, I'm adding a little online leaderboard to it. I believe a leaderboard is a small touch that can help a game get a little more replay, specially if it is an arcade-like game. Since I'm adding this feature, I thought that printing the steps and taking note of them would help someone, so I made this guide! I hope it helps you.

Here are the sections:

  • 1. Creating an account and game on SilentWolf
  • 2. Setting up SilentWolf on Godot
    • 2.1 [Optional step] Hiding the API Key on a enviroment file (useful for opensource projects)
  • 3. Allowing the user to send their score
  • 4. Loading the player and top 10 scores
    • 4.1 Showing a "Loading" message
    • 4.2 Showing the player's position 
    • 4.3 Resetting the leaderboard
  • 5. Final comments

1. Creating an account and game on SilentWolf

 We are going to use SilentWolf as a backend for our highscore. It is a quite simple and easy to use backend for Godot stuff, allowing you to create and manage leaderboards, player data, and player accounts.

 First of all, we need to create an account there. So, go to SilentWolf and click on "Log In".


Then, create a new account.


With that done, click on "+New game" after you log in.


Insert your game name on the space that appears, and you should get a window like this:


Be sure to save both the API Key and the gameId! They will be used as a way for your game to send and fetch the scores from SilentWolf. Also, be sure to keep the API Key as a secret: it works as the password to the leaderboards we will create.

Finally, download the SilentWolf plugin by clicking on "Download" at the top of the page.


2. Setting up SilentWolf on Godot

After downloading the silent_wolf.zip file, extract its contents on a folder called "addons" on the root of your project folder. It should end up looking something like this:


Then, go to Project > Project Settings > Plugin tab, and enable the Silent Wolf plugin if it isn't already enabled. Also, check on the AutoLoad tab if the SilentWolf.gd file is loaded as a singleton as well.




Now we need to write some code to "log in" our game into SilentWolf, so that scores can be fetched and sent. We only need to run this code at the start of the game, so putting it inside the _ready() function of a Singleton will work fine. I commonly use a global.gd file at the root of the project as this Singleton.

The code we'll run at the start of the game is:

SilentWolf.configure({
    "api_key": "yourApiKey",
    "game_id": "yourGameName",
    "game_version": "1.0",
    "log_level": 1
})

If everything is working well, by running your game now you should see some of SilentWolf's debug messages:


2.1 [Optional step] Hiding the API Key on a enviroment file (useful for opensource projects)

In case you are interested in releasing the source code of your project, that's great! The only problem is that now your API Key won't be a secret anymore. However, there is a quite simple way to fix this.

First, we will create a string variable called apiKey on our global.gd file, and change our SilentWolf.configure() a bit:

SilentWolf.configure({
    "api_key": self.apiKey,
    "game_id": "yourGameName",
    "game_version": "1.0",
    "log_level": 1 
})

Now, create an apiKey.env file on the root of your project, and add your game's apiKey inside. Back to the global.gd file, add the code 

var f=File.new()
f.open('res://apiKey.env',File.READ)
self.apiKey=f.get_line()
f.close()

right before the SilentWolf.configure() call.  With this, we now load the contents of the apiKey.env file onto our apiKey variable! This is quite useful, because by preventing our apiKey.env file from being uploaded, we can prevent other people from knowing our apiKey.

 If you are using git, you can prevent this file from being uplodade quite easily. Create a file called .gitignore on the root of the project, and inside it add only "*.env". This will guarantee that any file with the extension "env" won't be uploaded to whatever remote git repository service you use, and because of this, your apiKey is now once again a secret only you knows!

 Update: as pointed out by the MurphysDad on the comment section, if you follow this step, be sure to configure your exports to include the .env file: otherwise, the leaderboards will work only inside the Godot Engine! This link contains more information on how to do so. Still, if you have any problems, feel free to drop a comment on the comment section and I'll try to help you ๐Ÿ˜‰

3. Allowing the user to send their score

We've already setup the leaderboard and "logged in", so sending some scores now is the next step. On your end game screen, add a MarginContainer node that will be responsible for:

  • Showing the final score and the option to send the player score;
  • Showing a loading message;
  • Showing the leaderboard.

We will start by the first one, allowing the player to send their score. For now, inside it add a VBoxContainer with:

  • A message showing the player score;
  • A LineEdit node;
  • A button.

If you want, you may add some HSeparators between the nodes to make the box more visually pleasing, along with theming your nodes in the style of your game. After customizing my nodes, I ended up with this:


And my node tree is organized like this:


Now, we need to do some coding. Add a script to the MarginContainer you created, connect the pressed() signal of the button you created to it, and there write:

func _on_button_pressed():
    if $vbox1/lineEdit.text!='':
    var score_id = yield(SilentWolf.Scores.persist_score(
                   $vbox1/lineEdit.text, global.score),
                   "sw_score_posted")
    global.scoreId=score_id
    print_debug("Score persisted successfully: " + str(score_id))

The global.scoreId variable will be used later, when retrieving the player's position. Don't forget to create it!

First, we check if there is actually any text on the LineEdit. If there is, we send the score to the leaderboard using the SilentWolf plugin, which returns a score_id that will be useful to us as a confirmation that everything went OK. Notice that the yield() will "lock" the code there, and code lines below it will work only after SilentWolf emits a sw_score_posted signal.

You can try to send some dummy scores to the leaderboard now. If everything it working, you should get a message more or less like this on the debugger:


Hope everything is working well on your side! Now that the game can send scores, let's display the top 10 highscores.

4. Loading the player and top 10 scores

If you think about it, a leaderboard is just a bunch of lines of text containing the position of a certain score, that score owner's name, and the score. For instance, on Planet Waves (another one of our games), the leaderboard is like this:


As you can see not many people played it, but that's not the point! The important thing to see is the pattern I mentioned. We can reproduce it quite easily by instancing a bunch of "scorelines" in a VBoxContainer node.

Let's start by creating a new scene, which root will be a HBoxContainer. Name it scoreline, and add some nodes:

  • One label, for the score position;
  • Another label, for the player name;
  • One VSeparator, with the horizontal size flag set to "Expand";
  • And the last label, for the score.

Don't forget to theme it! I ended up with:


Save this scene, and go back to the MarginContainer script. Create a const that will hold a preload of the scoreline scene, and on the button pressed function we will add a loop that will populate our VBoxContainer with the top scores.

First, we should clean the VBoxContainer. We can do that by deleting all of its children:

After that, now we should create the loop I mentioned. We can get to top 10 scores from SilentWolf by calling

SilentWolf.Scores.get_high_scores(10)

Once again we will add a yield() instead of the direct call, so that the code only proceeds to create the scorelines after they've been loaded.

yield(SilentWolf.Scores.get_high_scores(10), "sw_scores_received")

With the scores loaded, create a variable that will hold the current position, and iterate over SilentWolf.Scores.scores, finally creating the scoreline instances. Notice that by iterating over SilentWolf.Scores.scores, we have access to the player's name and score:

var idx=1
for score in SilentWolf.Scores.scores:
    var i=scoreline.instance()
    i.get_node("strPlayerPosition").text='#'+str(idx)
    i.get_node('strPlayerName').text=score.player_name
    i.get_node('strPlayerScore').text=str(score.score)
    $vbox1.add_child(i)
    idx+=1

And if you test the game now, you'll see it seems to work fine! However, we can still do somethings to make stuff work better. Compared to what we did until now they are mostly straightfoward, so I'll explain how to do them a bit more briefly. If you have any trouble, fell free to leave a comment and I can help you. Also, the game is open source, so you can go check its source code in case you want to see how I coded these parts.

4.1 Showing a "Loading" message

This one is quite easy. To do it, create another scene with the root being a Label node. Customize it, and set the text as "Loading..." for instance.  Also, set the horizontal alignment to "Center" Save it, and load the scene inside the MarginContainer. Now, to make a loading option appear, simply instance this scene inside the VBoxContainer, and then call its queue_free() after the

yield(SilentWolf.Scores.get_high_scores(10), "sw_scores_received")

4.2 Showing the player's position

Your player might end up not being among the top 10 players once they stop playing. In that case, our current code will not show the position the current player was able to get.

We can fix this by calling

SilentWolf.Scores.get_score_position(global.scoreId)

This will fetch the player's position, and such value will be in the variable SilentWolf.Scores.position. Then, we can check if the player is outside the top 10, and if they are, we add their score to the bottom of the list. Don't forget to make use of yield(), since the get_score_position() takes some time to work (that is, it is an asynchronous method).

4.3 Resetting the leaderboard

By now, you might have a leaderboard full of debug entries you used while testing it. In order to reset the leaderboard, just call 

SilentWolf.Scores.wipe_leaderboard()

from somewhere in your code. I usually add it in the _ready() of the global singleton.

However, remember to comment this line out right after! Otherwise, no data will be saved on your leaderboard, since it will keep being reseted everytime someone plays the game.

5. Final comments

 That's it! By now you should have a nice little leaderboard at the end of your game. In case you want to level up your project, you can also make weekly leaderboards, or show who is close to the player's score (so that they have a kind of rival), or even create custom leaderboards, so that you can rank the players as the ones who survived the longest, or made more points, etc. Be creative! Those three examples have some nice introduction at SilentWolf's documentation, so that's a place to start learning more.

 Also, in case you use this tutorial, feel free to post your game below. I'll be sure to give it a try, but I'm not sure if I'll get a highscore! Also, feel free to leave me some feedback. I'm not used to writing tutorials, but if this one is helpful, I might make another ones. Thanks for the attention!

Get Jump3Match

Download NowName your own price

Comments

Log in with itch.io to leave a comment.

(+3)

I think so far this is the best and most direct tutorial on silentwolf plugin. With this a lot of godot beginners would be able to get their leaderboard up and running. Thanks for the good work

 Thanks a lot, Alani! It's always great to receive positive feedback ๐Ÿ˜‰

(+3)

Thank you for writing up this tutorial - it was very helpful!

One thing I found: after following the tip about importing the API key via the `api_key.env` file, I found communication with SilentWolf wasn't working in my exported version of the game. The solution, I found, was to ensure that the Project > Export > Resources included `*.env` files under the "Filters to export non-resource files/folders" section (see https://godotengine.org/qa/91714/file-read-and-write-worked-from-godot-console-w...)

Anyway, just wanted to share that. Thanks again!

 Thanks for reading! Great to know that you found it helpful!

 And thanks for the observation too, I'll mention this information, it's quite important. Thanks for pointing it out! Feel free to link your game here when you finish it ๐Ÿ™‚

(+1)

No problem!

Haha, well, here's the game: https://murphysdad.itch.io/damage-the-walls  (It's just a silly little thing I did for a jam)

(+1)

Great tutorial! And good advice about managing your API Key. I don't know why it took me so long to find out about this page. I'm off to play some of your games. Cheers, Brass Harpooner (creator/maintainer of the SilentWolf plugin)

(+1)

Wow, thanks for the compliment! Thanks for making SilentWolf, it really helps!

(+1)

Thanks for this great example! I'm currently developing a really simple endless runner and this helped me implement leaderboards into it. Much love!

(+1)

Awesome! Glad I could help! Feel free to link your game here when you finish it :)

(+1)

Thanks for the help! SilentWolf's documentation is really great, but your specific example helped me get it working!

Their documentation really is great!  IMO they should be more used as a backend. Anyway, glad to know I helped you!

(+1)

I want to set up Leaderboard, But sumbiting Score is kinda too confusing

Hey, thanks for the feedback!

The gist for submitting a score is just to call

SilentWolf.Scores.persist_score(PLAYER_NAME, PLAYER_SCORE)

The yield I used on the tutorial helps us to continue running the code only after this function finishes what it has to do, and the rest of the things (the nodes, for instance) can be altered to suit the way you like to use Godot.

The important thing is to call SilentWolf.Scores.persist_score and pass the player name and the player score. For the player name, I suggest using a LineEdit node, and for the score, a variable on the global singleton should do the trick.

Sorry if my guide was a bit confusing, but I hope now you understand better how to send scores :)

(+1)

Thanks you. you help a lot, now i understand and successfully Implemented Leader board for my Game, all thanks to you.  3 Red hearts

(+1)

Great!!! Glad to be helpful ๐Ÿ˜‰