Struggling to figure out what's wrong with my highscore function

Godot Version

Godot 4.3

Question

My game increases the score by 1 each time the player manages to catch 1 falling object. It’s also supposed to display the current score and the high score in the death scene. After some work, the scores are displaying just not correctly.

My issue is that each time my game restarts, and the player starts catching falling objects, my previous high score is being overwritten by the current score despite the current score not being higher than the high score.

Example:
Game 1 score = 6
high score = 6

Game 2 If the player catches nothing.

           score = 0
           high score = 6

Game 3 If the player catches 1 falling object.

           score = 1
           high score = 1

It seems like my highscore function is struggling to save the previous high score ? I suspect there’s some logical issue in my code that I’m missing.

score.gd

var score = 0
var highscore = 0
@onready var score_label = $Score

func add_point():
	score += 1
	score_label.text = "Score:" + str(score)
	save_score()
	
func add_highscore():
	highscore += 1
	save_highscore()
	update_highscore()
	
	
func save_score():
	var file = FileAccess.open("user://score.save", FileAccess.WRITE)
	file.store_32(score)
	file.close()
	
func save_highscore():
	var highscore_file = FileAccess.open("user://high_score.save", FileAccess.WRITE)
	highscore_file.store_32(highscore)
	highscore_file.close()
	
func update_highscore():
	if score > highscore:
		highscore = score
		save_highscore()

death_scene.gd:

extends Control
@onready var death_score = $Score2
@onready var high_score = $HighScore2

func _ready():
	loading_score()
	loading_highscore()
	
	
# Called when the node enters the scene tree for the first time.
func loading_score():
	if FileAccess.file_exists("user://score.save") :
		var file = FileAccess.open("user://score.save",FileAccess.READ)
		var score = file.get_32()
		file.close()
		death_score.text = str(score)
		
func loading_highscore():
	if FileAccess.file_exists("user://high_score.save"):
		var highscore_file = FileAccess.open("user://high_score.save",FileAccess.READ)
		var highscore = highscore_file.get_32()
		high_score.text = str(highscore)
		
		var currentscore_file = FileAccess.open("user://score.save", FileAccess.READ)
		var current_score = currentscore_file.get_32()
		currentscore_file.close()
		
		if highscore < current_score:
			highscore = current_score
			var new_highscore = FileAccess.open("user://high_score.save", FileAccess.WRITE)
			new_highscore.store_32(current_score)
			high_score.text = str(current_score)
			new_highscore.close()
	

falling objects script:

extends Area2D

func _physics_process(delta: float) -> void:
	position.y += 100 * delta
	
@onready var score = $"../score"

func _on_body_entered(body: Node2D) -> void:
	if body.name == "pink_cookie_man":
		score.add_point()
		score.add_highscore()
	queue_free()

games.gd

extends Node2D
@onready var new_game_score = $score
@onready var new_game_highscore = $score

func _ready():
	$cookie_normal_Timer.wait_time = 0.5
	$cookie_normal_Timer.timeout.connect(_on_timer_timeout)
	newgame_score()
	newgame_highscore()
	
func _on_timer_timeout():
	randomize()
	var cookie_normal = preload("res://scenes/cookie_normal.tscn").instantiate()
	var mint_choco = preload("res://scenes/cookie_mint_choco.tscn").instantiate()
	var strawberry_choco = preload("res://scenes/cookie_strawberry_choco.tscn").instantiate()
	var sprinkles_frosting = preload("res://scenes/cookie_sprinkles.tscn").instantiate()
	var jam_cookie = preload("res://scenes/cookie_jam.tscn").instantiate()
	var dynamite = preload("res://scenes/dynamite.tscn").instantiate()
	
	cookie_normal.position.y = -534
	cookie_normal.position.x = randf_range(-1152,1152)
	
	mint_choco.position.y = -534
	mint_choco.position.x = randf_range(-1152,1152)
	
	strawberry_choco.position.y = -534
	strawberry_choco.position.x = randf_range(-1152,1152)
	
	sprinkles_frosting.position.y = -534
	sprinkles_frosting.position.x = randf_range(-1152,1152)
	
	jam_cookie.position.y = -534
	jam_cookie.position.x = randf_range(-1152,1152)
	
	dynamite.position.y = -534
	dynamite.position.x = randf_range(-1152,1152)
	
	add_child(cookie_normal)
	add_child(mint_choco)
	add_child(strawberry_choco)
	add_child(sprinkles_frosting)
	add_child(jam_cookie)
	add_child(dynamite)

func newgame_score():
	if FileAccess.file_exists("user://score.save"):
		var new_gamescore = FileAccess.open("user://score.save",FileAccess.WRITE)
		new_gamescore.store_32(0)
		new_gamescore.close()
		
		
func newgame_highscore():
	if FileAccess.file_exists("user://high_score.save"):
		var score_file = FileAccess.open("user://high_score.save", FileAccess.READ)
		score_file.get_32()
		new_game_highscore.highscore = score_file.get_32()
	

score.gd is a script inside score which is under the game node. The death scene is on a separate scene that isn’t under the game node.

I tried adding the newgame_highscore function in the games.gd hoping it would set the highscore to the previous game’s score preventing it from being overwritten by the current score, but the same issue remained.

you are over writing the high score if the current score is lower. Making it a lowscore instead. Use > instead of <

so current_score > highscore ? I still have the same issue after changing the < symbol to > and I’m unsure how to fix it.

is the issue with add_highscore()?

I think your problem will be there, you’re simple updating the highscore as if was a normal score every time you collect a falling object, is because that if you don’t collect anything the highscore will be correct, because you won’t override the value.

Also this scoring structure is bad, you’re saving in a file for every score change, IO operations are expensive, if you game make a faster scoring update you game will stall, you should only rely on files after the finish of each match to save the highscore.


score.gd

var score = 0
var highscore = 0
@onready var score_label = $Score


func _ready() -> void:
	# Makes easier to access this node from everywhere
	add_to_group("score") 

	# You don't need to read this file all the time, just read 
	# once when you open the game to get the current highscore.
	if FileAccess.file_exists("user://high_score.save"):
		var highscore_file = FileAccess.open("user://high_score.save", FileAccess.READ)
		highscore = highscore_file.get_32()
		highscore_file.close()


func add_point():
	score += 1
	score_label.text = "Score:" + str(score)
	#save_score() Unecessary, remove


func get_scores() -> Array:
	return [score, highscore]


func start_score() -> void:
	score = 0
	score_label.text = "Score:" + str(score)

func save_highscore():
	# Update the highscore file only if necessary, otherwise don't touch it
	if score > highscore:
		highscore = score
		var highscore_file = FileAccess.open("user://high_score.save", FileAccess.WRITE)
		highscore_file.store_32(highscore)
		highscore_file.close()

# Everything bottom this commentary is unecessary, remove

func add_highscore():
	highscore += 1
	save_highscore()
	update_highscore()
	

func save_score():
	var file = FileAccess.open("user://score.save", FileAccess.WRITE)
	file.store_32(score)
	file.close()
	

func update_highscore():
	if score > highscore:
		highscore = score
		save_highscore()

death_scene.gd:

extends Control
@onready var death_score = $Score2
@onready var high_score = $HighScore2

func _ready():
	# Im guessing this is only instanciated when the game finishes
	# because i didn't find a game over function

	# Get the score node using groups
	var score_node = get_tree().get_first_node_in_group("score")
	score_node.save_highscore()

	var scores = score_node.get_scores()
	death_score.text = str(scores[0])
	high_score.text = str(scores[1])

	#loading_score() Unecessary, remove
	#loading_highscore() Unecessary, remove
	
# Everything bottom this commentary is unecessary, remove

# Called when the node enters the scene tree for the first time.
func loading_score():
	if FileAccess.file_exists("user://score.save") :
		var file = FileAccess.open("user://score.save",FileAccess.READ)
		var score = file.get_32()
		file.close()
		death_score.text = str(score)
		
func loading_highscore():
	if FileAccess.file_exists("user://high_score.save"):
		var highscore_file = FileAccess.open("user://high_score.save",FileAccess.READ)
		var highscore = highscore_file.get_32()
		high_score.text = str(highscore)
		
		var currentscore_file = FileAccess.open("user://score.save", FileAccess.READ)
		var current_score = currentscore_file.get_32()
		currentscore_file.close()
		
		if highscore < current_score:
			highscore = current_score
			var new_highscore = FileAccess.open("user://high_score.save", FileAccess.WRITE)
			new_highscore.store_32(current_score)
			high_score.text = str(current_score)
			new_highscore.close()

falling objects script:

extends Area2D

func _physics_process(delta: float) -> void:
	position.y += 100 * delta
	
@onready var score = $"../score"

func _on_body_entered(body: Node2D) -> void:
	if body.name == "pink_cookie_man":
		score.add_point()
		#score.add_highscore() Unecessary, remove
	queue_free()

games.gd

extends Node2D

# Why two variables that point to the same node?
#@onready var new_game_score = $score Unecessary, remove
#@onready var new_game_highscore = $score Unecessary, remove

func _ready():
	$cookie_normal_Timer.wait_time = 0.5
	$cookie_normal_Timer.timeout.connect(_on_timer_timeout)
	
	# Call the function start_score in the score node to 
	# make sure the score starts as zero
	get_tree.call_group("score", "start_score")

	#newgame_score() Unecessary, remove
	#newgame_highscore() Unecessary, remove
	
func _on_timer_timeout():
	randomize()
	var cookie_normal = preload("res://scenes/cookie_normal.tscn").instantiate()
	var mint_choco = preload("res://scenes/cookie_mint_choco.tscn").instantiate()
	var strawberry_choco = preload("res://scenes/cookie_strawberry_choco.tscn").instantiate()
	var sprinkles_frosting = preload("res://scenes/cookie_sprinkles.tscn").instantiate()
	var jam_cookie = preload("res://scenes/cookie_jam.tscn").instantiate()
	var dynamite = preload("res://scenes/dynamite.tscn").instantiate()
	
	cookie_normal.position.y = -534
	cookie_normal.position.x = randf_range(-1152,1152)
	
	mint_choco.position.y = -534
	mint_choco.position.x = randf_range(-1152,1152)
	
	strawberry_choco.position.y = -534
	strawberry_choco.position.x = randf_range(-1152,1152)
	
	sprinkles_frosting.position.y = -534
	sprinkles_frosting.position.x = randf_range(-1152,1152)
	
	jam_cookie.position.y = -534
	jam_cookie.position.x = randf_range(-1152,1152)
	
	dynamite.position.y = -534
	dynamite.position.x = randf_range(-1152,1152)
	
	add_child(cookie_normal)
	add_child(mint_choco)
	add_child(strawberry_choco)
	add_child(sprinkles_frosting)
	add_child(jam_cookie)
	add_child(dynamite)


# Everything bottom this commentary is unecessary, remove

func newgame_score():
	if FileAccess.file_exists("user://score.save"):
		var new_gamescore = FileAccess.open("user://score.save",FileAccess.WRITE)
		new_gamescore.store_32(0)
		new_gamescore.close()
		
		
func newgame_highscore():
	if FileAccess.file_exists("user://high_score.save"):
		var score_file = FileAccess.open("user://high_score.save", FileAccess.READ)
		score_file.get_32()
		new_game_highscore.highscore = score_file.get_32()

Try theses changes and see if works, maybe can fail because i don’t know the exact organization of your scene, but with adjusts will work.

1 Like

In games.gd, I get this error message :

‘Cannot find property “call_group” on Base “Callable”.’

For the line, get_tree.call_group("score, “start_score”)

I think you forgot to give a bracket, can you try this?

get_tree().call_group("score, “start_score”)
1 Like

Thanks for catching the missing brackets.

I fixed it that error message is gone, but now I have an issue in my death_scene.gd.

extends Control
@onready var death_score = $Score2
@onready var high_score = $HighScore2


func _ready():
	var score_node = get_tree().get_first_node_in_group("score")
	score_node.save_highscore()

	var scores = score_node.get_scores()
	death_score.text = str(scores[0])
	high_score.text = str(scores[1])

Error Message : Attempt to call function ‘save_highscore’ in base ‘null instance’ on a null instance.

Does this mean that my score node isn’t being created?

1 Like

Yeah, please can you make sure of it?

1 Like

It should be added by now. But I’m still having the same error.

I’ve even tried converting it to a global group yet I still encounter the same issue.

I’m not sure if this is necessary info, but I wonder if my issues with accessing the score node has to do with how I’m loading the death scene.

So in my game, each time the player hits a falling dynamite I use get.tree().change_scene_to_packed() to get the death_scene .

Yeah, will be that the problem, the score script will be deleted before you have acess to it, generally you don’t change scenes for death screen, just halt the scenario scene and show the death screen without change everything, but anyways, this is easy to solve:

Create an autoload called ScoreManager (or whatever name you prefer) and create two variables in this autoload (how create an autoload: Singletons (Autoload) — Godot Engine (stable) documentation in English)

# ScoreManager.gd

extends Node

var score := 0
var highscore := 0

func _ready() -> void:
	# You don't need to read this file all the time, just read 
	# once when you open the game to get the current highscore.
	if FileAccess.file_exists("user://high_score.save"):
		var highscore_file = FileAccess.open("user://high_score.save", FileAccess.READ)
		highscore = highscore_file.get_32()
		highscore_file.close()


func save_highscore():
	# Update the highscore file only if necessary, otherwise don't touch it
	if score > highscore:
		highscore = score
		var highscore_file = FileAccess.open("user://high_score.save", FileAccess.WRITE)
		highscore_file.store_32(highscore)
		highscore_file.close()


func get_scores() -> Array:
	return [score, highscore]


func start_score() -> void:
	score = 0

# score.gd

@onready var score_label = $Score


func add_point():
	ScoreManager.score += 1 
	score_label.text = "Score:" + str(ScoreManager.score)

# death_scene.gd

extends Control

@onready var death_score = $Score2
@onready var high_score = $HighScore2

func _ready():
	ScoreManager.save_highscore()

	var scores = ScoreManager.get_scores()
	death_score.text = str(scores[0])
	high_score.text = str(scores[1])

# Falling objects script

extends Area2D

@onready var score = $"../score"

func _physics_process(delta: float) -> void:
	position.y += 100 * delta

func _on_body_entered(body: Node2D) -> void:
	if body.name == "pink_cookie_man":
		score.add_point()

	queue_free()

extends Node2D

func _ready():
	$cookie_normal_Timer.wait_time = 0.5
	$cookie_normal_Timer.timeout.connect(_on_timer_timeout)
	
	ScoreManager.start_score()


func _on_timer_timeout():
	randomize()
	var cookie_normal = preload("res://scenes/cookie_normal.tscn").instantiate()
	var mint_choco = preload("res://scenes/cookie_mint_choco.tscn").instantiate()
	var strawberry_choco = preload("res://scenes/cookie_strawberry_choco.tscn").instantiate()
	var sprinkles_frosting = preload("res://scenes/cookie_sprinkles.tscn").instantiate()
	var jam_cookie = preload("res://scenes/cookie_jam.tscn").instantiate()
	var dynamite = preload("res://scenes/dynamite.tscn").instantiate()
	
	cookie_normal.position.y = -534
	cookie_normal.position.x = randf_range(-1152,1152)
	
	mint_choco.position.y = -534
	mint_choco.position.x = randf_range(-1152,1152)
	
	strawberry_choco.position.y = -534
	strawberry_choco.position.x = randf_range(-1152,1152)
	
	sprinkles_frosting.position.y = -534
	sprinkles_frosting.position.x = randf_range(-1152,1152)
	
	jam_cookie.position.y = -534
	jam_cookie.position.x = randf_range(-1152,1152)
	
	dynamite.position.y = -534
	dynamite.position.x = randf_range(-1152,1152)
	
	add_child(cookie_normal)
	add_child(mint_choco)
	add_child(strawberry_choco)
	add_child(sprinkles_frosting)
	add_child(jam_cookie)
	add_child(dynamite)

1 Like

Thanks for the detailed solution. But how do I ensure that this will make the death_scene load out when the player hits a dynamite?

This is dynamite.gd

extends Area2D

func _physics_process(delta: float) -> void:
	position.y += 90 * delta

@onready var timer = $Timer
@onready var death_scene = preload("res://scenes/death_scene.tscn") as PackedScene

func _on_body_entered(body: Node2D) -> void:
	if body.name == "pink_cookie_man":
		timer.start()
	else:
		queue_free()

func _on_timer_timeout() -> void:
	get_tree().change_scene_to_packed(death_scene)

You don’t need to change anything in your dynamite code, if death scene was being called before, that will keep working now

1 Like

Thanks for your help! Everything is working now.

You are wonderful! I’ve been stuck for nearly a week. :smiling_face_with_tear:

1 Like

If my previous high score was getting deleted because of get_tree().change_to_packed(), than how come my current score upon game over was loading correctly?

Why was my high score the only thing affected?

No, your previous highscore was being deleted by your code logic when you updated the score:

The code above is your first code, every time you added a normal point you also was adding a highscore point


var score = 0
var highscore = 0
@onready var score_label = $Score

func add_point():
	score += 1
	score_label.text = "Score:" + str(score)
	save_score()

# For every point you did, you call this, highscore on the start of the 
# game will be zero, so from your old way highscore would always
# be the same value of score
func add_highscore():
	highscore += 1

	# And you was saving that on the file every time you did a score, so 
	# you was always overwriting your highscore, even if you score wasn't 
	# the bigger score
	save_highscore()

	# And a bonus you was calling this function that would do nothing
	# for two reasons, first, as said before, highscore would be always the
	# value of score, so you don't have anything to update here and second,
	# This also calls save_highscore so you're just doing duplication calls
	update_highscore()
	
	
func save_score():
	var file = FileAccess.open("user://score.save", FileAccess.WRITE)
	file.store_32(score)
	file.close()

func save_highscore():
	var highscore_file = FileAccess.open("user://high_score.save", FileAccess.WRITE)
	highscore_file.store_32(highscore)
	highscore_file.close()
	
func update_highscore():
	if score > highscore:
		highscore = score
		save_highscore()

Is because of that you never update statistics while the match is running, you always update these statistics when the match ends.

2 Likes

Thank you