This tutorial will show you how to add a locally saved high score table to your game. (The next tutorial will show you how to add an online leaderboard to your game.)
Before we start adding anything we will look at what the simple game already has and how it works.
Note
This section is for information; you don’t have to do anything until the next section.
Our game only has 2 scenes:
The code for this is very simple:
func _on_QuitButton_pressed():
get_tree().quit()
func _on_PlayButton_pressed():
get_tree().change_scene("res://game.tscn")
These functions are connected to the buttons via signals. Note there is no function for the high score button yet.
The game scene has only 4 nodes:
The code simply counts down the timer every second, and increases the score every time a key is pressed:
var time = 10
var score = 0
func _on_Timer_timeout():
-= 1
time $TimeLabel.text = "TIME: "+str(time)
if time <=0:
get_tree().change_scene("res://title_screen.tscn")
func _unhandled_input(event):
if event is InputEventKey and not event.echo:
+= 1
score $ScoreLabel.text = "SCORE: "+str(score)
$icon.position.y = score * 5
We need somewhere for the player to enter his name, so let’s make a ‘Game Over’ screen that will be displayed when the game ends.
gameover
.gameover.tscn
.GAMEOVER
Your score is
score
."res://title_screen.tscn"
to "res://gameover.tscn"
so that the game goes to the gameover screen at the end.We have a problem: we want to display the score on the Game Over screen, but the score is only stored in the game.gd script, not the gameover.gd one.
In Python (and Godot) we saw global variables that can be used from any function in one script. In Python if we want to use a variable from another script we have to import it.
In Godot we can do something similar but it’s easier to create variables that can be used by any script in any scene by creating a singleton object. Let’s do this.
globals.gd
as the name of the script and press create.var score=0
Save the script. (ctrl-S)
To make this accessible from anywhere:
Now go back the game.gd script and delete the line containing the score variable (line 5). Then change all the other references from score
to Globals.score
.
The end result should look like this:
extends Node2D
var time = 10
func _on_Timer_timeout():
-= 1
time $TimeLabel.text = "TIME: "+str(time)
if time <=0:
get_tree().change_scene("res://gameover.tscn")
func _unhandled_input(event):
if event is InputEventKey and not event.echo:
+= 1
Globals.score $ScoreLabel.text = "SCORE: "+str(Globals.score)
$icon.position.y = Globals.score * 5
You don’t need to type all that, you only need to make 4 edits. But that’s the complete file you should have after your changes.
pass
) so that it looks like this:func _ready():
$score.text = str(Globals.score)
Before we can display the table we need somewhere to store the scores and the names, so let’s add two lists to the end of the globals.gd script:
var scores = []
var names = []
Go back to gameover.tscn scene and click on the LineEdit node. This is where the name is entered.
Click on Node to the right of the Inspector to view the Signals. Double click on text_entered. Press connect.
A function will be created for you that is called when the player enters his name and presses return. Edit the function to look like this:
func _on_LineEdit_text_entered(new_text):
append(Globals.score)
Globals.scores.append(new_text)
Globals.names.get_tree().change_scene("res://score_table.tscn")
Create a new scene.
Select User Interface for the root node.
Rename the root node to ScoreTable
.
Save the scene as score_table.tscn
.
Add a Label child node to the root node.
Names
Add a Label child node to the root node.
Scores
Position the two labels side by side like this:
Right click on the root node and Attach script. Press create. Edit the ready function so that it looks like this:
func _ready():
for name in Globals.names:
$Names.text += name + "\n"
for score in Globals.scores:
$Scores.text += str(score)+"\n"
You should be able to enter your score and see the score table. However, you will then be stuck because there is no menu navigation.
Rename it to BackButton
In the Inspector set the Text to Back
.
In the Inspector, click Custom Fonts and then drag the font.tres file from the FileSystem (bottom left of screen) into the [empty] font field.
func _on_BackButton_pressed():
get_tree().change_scene("res://title_screen.tscn")
func _on_HighScoresButton_pressed():
get_tree().change_scene("res://score_table.tscn")
There a couple of big problems with this score table. The first one is that it loses the scores every time you quit game.
To fix this, we can store the scores in a file on the computer’s disk. We will create separate functions for loading and saving the scores. Edit globals.gd and add this code to the bottom:
func _init():
load_scores()
func save_scores():
var file = File.new()
open("user://game.dat", File.WRITE)
file.store_var(names)
file.store_var(scores)
file.close()
file.
func load_scores():
var file = File.new()
var err = file.open("user://game.dat", File.READ)
if err != OK:
print("error loading scores")
else:
= file.get_var()
names = file.get_var()
scores close() file.
The first time we run the game there will be no score file, so we will we print an error, but this is OK, because it will be created when we save the scores. To do this, edit gameover.gd, and insert the one new line highlighted below:
func _on_LineEdit_text_entered(new_text):
append(Globals.score)
Globals.scores.append(new_text)
Globals.names.save_scores()
Globals.get_tree().change_scene("res://score_table.tscn")
Run the game and check your scores load and save.
Currently, the scores are not displayed in the correct order. We need to sort them.
Godot has a built-in sort function, so we could call scores.sort()
, but this would only sort the scores and not the names. The way a professional coder would deal with this would probably be to store the name and score in an object and write a comparator function. However, it’s more educational (and simpler) for us to just write our own sort function. (Not to mention that Godot’s support for object-oriented programming is frustratingly rudimentary!)
This is a famous algorithm called Bubble Sort.
Add this to the bottom of globals.gd:
func bubble_sort():
for passnum in range(len(scores)-1,0,-1):
for i in range(passnum):
if scores[i]<scores[i+1]:
var temp = scores[i]
= scores[i+1]
scores[i] +1] = temp
scores[i= names[i]
temp = names[i+1]
names[i] +1] = temp names[i
Edit the save_scores function so that it sorts every time it saves (new line highlighted)
func save_scores():
bubble_sort()
var file = File.new()
file.open("user://game.dat", File.WRITE)
file.store_var(names)
file.store_var(scores)
file.close()
Add an ‘OK’ button on the gameover screen.
Display ranking number 1, 2, 3, etc next to the names.
What do you do when there are too many scores to fit on the screen? Delete the lowest ones? Or provide buttons to scroll up and down?
Saving to a local file is very useful, but if you want to compare your scores with your friends? You can’t read files saved to your friends’ computers, so instead you need to store all the scores on a computer on the Internet. This is called a server. Then as well as saving your score locally, you also send it to the server, like this:
The server saves your score along with all the scores of everybody else. Then when you want to display the scores, you send a request to the server to retrieve them:
Usually I would not suggest relying on third party servers for your game.
However the dreamlo server is very simple, so if it does stop running it will not be difficult for us to create our own replacement. (That would would be the topic for another tutorial. For now we will use dreamlo).
In your web browser, go to the website dreamlo.com.
Click Get Yours Now button.
In Godot, open globals.gd. Add these two variables, but rather than using my values, copy and paste the codes given to you on the left side of the web page.
var public_code = "60d206118f40bb114c4ca743"
var private_code = "iRJrbvqSmkykd5aQBcXlAgm6EWSo3SekmWhWF5W-zfkA"
Copy this URL into a new web browser window and press enter, but replace the code with your private code. (You can see this example on your private dreamlo page with the correct code already filled in)
http://dreamlo.com/lb/Sv3NeBzS0016IwMfZjGudTESQhkHwEpQ/add/Carmine/100
You should get a response that says OK or similar. You have submitted the score of 100 for player Carmine. Go ahead and submit a few more scores for other players.
To test if it worked, copy this URL and press enter but replace the code with your private code. (You can see this example on the dreamlo page with the correct code already filled in.)
http://dreamlo.com/lb/60d341098f40bb114c4e34b2/json
You will get a response that looks something like this:
Here it is with nicer indentation:
{"dreamlo":
{"leaderboard":
{"entry":
[
{"name":"Carmine","score":"100","seconds":"0"},
{"name":"Bob","score":"10","seconds":"0"}
]
}
}
}
This is just plain text, but it is formatted in a format called JSON which makes it easy for us to write a program that processes. The names of the objects are important and we will need them later. Also note that curly brackets mean objects and square brackets mean lists/arrays.
func _on_LineEdit_text_entered(new_text):
Globals.scores.append(Globals.score)
Globals.names.append(new_text)
Globals.save_scores()
var url = "http://dreamlo.com/lb/"+Globals.private_code+"/add/"
url += new_text.percent_encode()+"/"+str(Globals.score)
$HTTPRequest.request(url)
get_tree().change_scene("res://score_table.tscn")
If you run this, play the game and submit a score, it will appear to work. However networking coding is tricksy.
In your web browser, open the URL that you used previously to get the high s core table in JSON format. (For me this is http://dreamlo.com/lb/60d206118b114c4ca743/json but your public code will be different.)
You will probably find the score was not added. Why not? Because we changed the scene without waiting for the network request to finish. How long do we have to wait? It depends on the network speed. So we will next use a callback function that is called for us by Godot when the request is completed.
DELETE this line from the on_LineEdit_text_entered function.
get_tree().change_scene("res://score_table.tscn")
Click on the HTTPRequest node. Click Node next to Inspector on the right to view the Signals. Double click the request_complated signal. Press connect.
Edit the function it generates to look like this:
func _on_HTTPRequest_request_completed(result, response_code, headers, body):
get_tree().change_scene("res://score_table.tscn")
OnlineScoreTable
.online_score_table.tscn
.Names
font.tres
file from the FileSystem (bottom left of screen) into the [empty]
font field.Scores
func _ready():
$HTTPRequest.request("http://dreamlo.com/lb/"+Globals.public_code+"/json")
Click on the HTTPRequest node. Click Node next to Inspector on the right to view the Signals. Double click the request_complated signal. Press connect.
Edit the function it generates to look like this:
func _on_HTTPRequest_request_completed(result, response_code, headers, body):
var json = JSON.parse(body.get_string_from_utf8())
var scores = json.result["dreamlo"]["leaderboard"]["entry"]
for i in scores:
$Names.text += i["name"] + '\n'
$Scores.text += i["score"] + '\n'
Note how we needed the field names from the JSON output in order to tell Godot how to pull out the data from the text and put it in a list for us.
Go to the title_screen.tscn scene.
Right click on the VBoxContainer node and add a Button child node.
OnlineHighScoreButton
.font.tres
file from the FileSystem (bottom left of screen) into the [empty]
font field.Click on Node to the right of the Inspector to view the Signals. Double click on pressed. Press connect.
Edit the function that is created to look like this:
func _on_OnlineHighScoresButton_pressed():
get_tree().change_scene("res://online_score_table.tscn")
Note
When you run this code it may work, but it may also crash.
Why? Because there are several possible responses the server could send you, and you don’t know which you are going to get.
Ideally we would write code to handle all of these possibilities, so that our game doesn’t crash unexpectedly.
For now, we are just going to do three basic error checks and return
if there is an error. Note that we consider there being one single score to be an error, so you must submit two or more scores before this will display anything on the screen.
Edit the function so that it looks like this:
func _on_HTTPRequest_request_completed(result, response_code, headers, body):
if result != HTTPRequest.RESULT_SUCCESS:
return
var json = JSON.parse(body.get_string_from_utf8())
if json.error != OK:
return
var scores = json.result["dreamlo"]["leaderboard"]["entry"]
if not scores is Array:
return
for i in scores:
$Names.text += i["name"] + '\n'
$Scores.text += i["score"] + '\n'