Showing Best Scores

Showing current and best scores together.

This is the eleventh tutorial in a series that covers the creation of a simple true top-down 2D game with pixel graphics, using the Godot Engine, version 4. It follows Extra Map Info and always shows the best scores while playing the game.

This tutorial uses Godot 4.5, the regular version, but you could also use the .NET version.

Upgrade to Godot 4.5

For this tutorial I upgraded to Godot 4.5.1. We can go straight from 4.4 to 4.5 without issues. There are no fundamental changes that require attention.

Godot 4.5 did introduce chunked tilemap physics, which means that tile colliders are merged into larger chunks. This reduces the problem of physics objects hitting seams between tiles, but does not fundamentally eliminate it, unless the whole map is a single chunk, because seams are still present at chunk boundaries. TileMapLayer has gained a Physics › Physics Quadrant Size property to control the new chunk size. As our game doesn't suffer from the seam problem we can just leave it at its default value, which is 16. Settings it to 1 makes each tile its own chunk so no tile colliders get merged, as it used to be. By setting Physics › Collider Visibility Mode to Force Show you can see how the collider mesh shapes get merged.

Best Score Functions

We previously introduced best scores for the map completion time and travel distance. We only show new best scores when the player finishes a map, but so far the scores are not visible while still on the map. We're going to change that by always showing the scores of the current map while playing.

To facilitate easy retrieval of these scores add functions to get them to map.gd. Name them get_best_time() and get_best_travel_distance().

func get_best_time() -> int:
	return map_save_data[KEY_BEST_TIME]


func get_best_travel_distance() -> int:
	return map_save_data[KEY_BEST_TRAVEL_DISTANCE]

These function can only return valid values if their relevant best score exists, so also add has_best_time() and has_best_travel_distance() functions that check whether this is the case.

func get_best_time() -> int:
	return map_save_data[KEY_BEST_TIME]


func has_best_time() -> bool:
	return map_save_data.has(KEY_BEST_TIME)


func get_best_travel_distance() -> int:
	return map_save_data[KEY_BEST_TRAVEL_DISTANCE]


func has_best_travel_distance() -> bool:
	return map_save_data.has(KEY_BEST_TRAVEL_DISTANCE)

Let's add code documentation to the get-functions to make clear that the has-functions should be checked before use. We can include a link to the functions by writing them in square brackets and putting the keyword method in front of them. That will make them clickable links in our script's documentation page and popups.

## Only valid if [method has_best_time] returns true.
func get_best_time() -> int:
	return map_save_data[KEY_BEST_TIME]

…

## Only valid if [method has_best_travel_distance] returns true.
func get_best_travel_distance() -> int:

We can use these functions in _update_best_scores() to avoid code duplication.

func _update_best_scores() -> void:
	var new_time := floori(current_map_time)
	if not has_best_time() or new_time < get_best_time():
		map_save_data[KEY_BEST_TIME] = new_time
	else:
		new_time = -1
	
	var new_travel_distance := floori(player_character.travel_distance)
	if (
			not has_best_travel_distance()
			or new_travel_distance < get_best_travel_distance()
	):
		map_save_data[KEY_BEST_TRAVEL_DISTANCE] = new_travel_distance
	else:
		new_travel_distance = -1
	
	HUD.show_new_best_map_scores(new_time, new_travel_distance)

Score Text Variables

Showing the scores is the job of hud.gd. We'll use the same labels that show the current scores to also show the best ones. But because the best scores won't change until after map completion it isn't necessary to create a new string for them every update, like we do for the current scores. This means that we could create strings with the best scores incorporated into them once and then reuse those. This is possible because strings with placeholders in them don't need to be constant, they're just regular strings.

To facilitate adjustment of the strings used to set the labels in show_map_time() and show_travel_distance() we promote them to variables. Let's name them map_time_text and map_travel_distance_text.

var map_time_text := "%ds"
var map_travel_distance_text := "%dpx"

…

func show_map_time(time: float) -> void:
	map_time_label.text = map_time_text % time

…

func show_travel_distance(distance: float) -> void:
	map_travel_distance.text = map_travel_distance_text % distance

Inserting Best Scores

If we're going to change the text we don't need default values for them anymore.

var map_time_text: String
var map_travel_distance_text: String

We'll instead assign the appropriate strings to them in show_map_info(). Let's start with the map time first. If the map has a best time then we'll append the best time to the text, in round brackets, using the same placeholder format, and substitute in the best time. Otherwise we use the old text. We have to do this before the call to show_map_time(), so let's do it at the start of the function.

func show_map_info(map: GameMap) -> void:
	if map.has_best_time():
		map_time_text = "%ds (%ds)" % map.get_best_time()
	else:
		map_time_text = "%ds"

	map_name_label.text = map.map_name
	show_map_time(0.0)
	visible = true
	new_best_map_scores_label.visible = false

Immediately using two placeholders isn't going to work. We want the first placeholder to remain in the string. Only the second placeholder should be replaced with the best time. This can be done by doubling the single % character so it becomes %%. When performing substitution the extra % will be removed.

		map_time_text = "%%ds (%ds)" % map.get_best_time()

Now if we for example have a best time of 5 we will get the string %ds (5s).

Use the same approach to set the text string for the travel distance.

	if map.has_best_time():
		map_time_text = "%%ds (%ds)" % map.get_best_time()
	else:
		map_time_text = "%ds"
	
	if map.has_best_travel_distance():
		map_travel_distance_text = (
				"%%dpx (%dpx)" % map.get_best_travel_distance()
		)
	else:
		map_travel_distance_text = "%dxp"
Showing best scores while playing.

We now see the best scores while playing a map, if they exist. However, the travel distance can be shown with the incorrect best time for a moment just after loading a new map. This happens because show_travel_distance() gets called too early, before the text has been updated with the best score for the next map. We should call it at the same time that we show the initial map time.

	show_map_time(0.0)
	show_travel_distance(0.0)

And we should no longer do this in _ready() in player_character.gd.

func _ready() -> void:
	last_position = position
	#HUD.show_travel_distance(travel_distance)

This completes our simple game HUD. In the future we'll move back to the map itself, adding more features to it.