Checkpoint does not load correctly - godot 4

I’m developing a game in Godot 4, and I’m trying to create a “save and load” system. Each scene is named following a pattern like “world_01”, “world_02”, and so on. The character starts in the scene “world_01”. Upon reaching the end, they advance to “world_02”, and the location where the player starts is the checkpoint location. That means, after completing scene 01 (world_01) and closing the application, when reopening it, the player should start directly in scene 02 (world_02) at the main node “Stage-02”. The code appears to save the checkpoint correctly and seems to load the checkpoint when closing and reopening the application. However, upon starting, the player begins in the first scene and not in the second scene where the checkpoint is located. What could be the reason for this? Here is my code:

Message returned when saving:

Globals script ready
Checkpoint saved: /root/Stage-02(331, 270)

Message returned when closing and reopening the game:

Globals script ready
Checkpoint loaded: /root/Stage-02(331, 270)

Archives:

checkpoint.gd:

extends Area2D

var is_active = false

func _on_body_entered(body):
	if body.name == "player" and not is_active:
		activate_checkpoint()

func activate_checkpoint():
	Globals.current_checkpoint = self
	Globals.current_checkpoint_scene = get_tree().current_scene.get_path()
	is_active = true
	Globals.save_checkpoint()

globals.gd (global):

extends Node

var player = null
var current_checkpoint = null
var current_checkpoint_scene = ""
var checkpoint_save_path = "user://checkpoint.sav"

func _ready():
	print("Globals script ready")  # Adicione uma mensagem para depuração
	load_checkpoint_from_file()

func _exit_tree():
	print("Tree exiting, saving checkpoint")  # Adicione uma mensagem para depuração
	save_checkpoint()

func respawn_player():
	if current_checkpoint_position != null:
		player.position = current_checkpoint_position


func load_checkpoint():
	if current_checkpoint != null:
		return {
			"position": current_checkpoint.global_position,
			"scene": current_checkpoint_scene
		}
	return null

func save_checkpoint():
	if current_checkpoint != null:
		var file = FileAccess.open(checkpoint_save_path, FileAccess.WRITE)
		if file:
			file.store_pascal_string(current_checkpoint_scene)
			file.store_var(current_checkpoint.global_position)
			file.close()
			print("Checkpoint saved: ", current_checkpoint_scene, current_checkpoint.global_position)
		else:
			print("Error opening file for writing")
	else:
		print("No current checkpoint to save")

var current_checkpoint_position = null

func load_checkpoint_from_file():
	if not FileAccess.file_exists(checkpoint_save_path):
		print("Checkpoint file does not exist")
		return
	
	var file = FileAccess.open(checkpoint_save_path, FileAccess.READ)
	if file:
		current_checkpoint_scene = file.get_pascal_string()
		var pos := file.get_var() as Vector2
		file.close()
		
		current_checkpoint_position = pos
		print("Checkpoint loaded: ", current_checkpoint_scene, pos)
	else:
		print("Error opening file for reading")

title_screen.gd (main scene):

extends Control

func _ready():
	pass # Não é necessário implementar nada aqui

func _on_start_btn_pressed():
	var checkpoint_data = Globals.load_checkpoint()
	if checkpoint_data != null:
		Globals.player.position = checkpoint_data.position
		get_tree().change_scene_to_file(checkpoint_data.scene)
	else:
		get_tree().change_scene_to_file("res://levels/world_01.tscn")

func _on_new_btn_pressed():
	get_tree().change_scene_to_file("res://levels/world_01.tscn")

func _on_credits_btn_pressed():
	# Implemente conforme necessário
	pass

func _on_quit_btn_pressed():
	Globals.save_checkpoint()
	get_tree().quit()

world.gd (script base/general):

extends Node2D

@onready var player := $player as CharacterBody2D
@onready var player_scene = preload("res://levels/player.tscn")

@onready var camera := $camera as Camera2D
# Called when the node enters the scene tree for the first time.
func _ready():
	Globals.player = player
	Globals.player.follow_camera(camera)
	Globals.player.player_has_died.connect(reload_game)


# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

func reload_game():
	await get_tree().create_timer(1.0).timeout
	var player = player_scene.instantiate()
	add_child(player)
	Globals.player = player
	Globals.player.follow_camera(camera)
	Globals.player.player_has_died.connect(reload_game)
	Globals.respawn_player()
	#get_tree().reload_current_scene()

Are you sure you want to be mixing .position and .global_position?

Globals.player.position = checkpoint_data.position
"position": current_checkpoint.global_position,

Are you sure that the correct scene is being changed to correctly here?

get_tree().change_scene_to_file(checkpoint_data.scene)

In addition, it seems strange to store the position of the checkpoint in the save file.
It seems like a property you could just store in the scene.

@export var player_spawn_position

func respawn_player():
  player.global_position = player_spawn_position

What I would do is to create a standard world_base scene with the world related methods, instead of putting those in Globals.

Then, when creating world_1, world_2, … use “New inherited scene” and all the properties that should be shared between the scenes will be in world_base

2 Likes

The location where the player starts is the same location as the checkpoint, meaning it should save every time the character reaches a new scene (level). I created multiple scenes because they have different elements. I also don’t have much knowledge in GODOT 4. Can you provide me with the corrected code? Or will it only work if I completely change it with the ‘base scene’?

I think I see a few issues. When pressing start, the load_checkpoint function does not check file data at all, it should call load_checkpoint_from_file somewhere along the way. Furthermore load_checkpoint_from_file does not alter current_checkpoint so the load_checkpoint function will always return null when the game is opened.

I propose make load_checkpoing_from_file return a bool so you can track it’s success

func load_checkpoint_from_file() -> bool:
	if not FileAccess.file_exists(checkpoint_save_path):
		print("Checkpoint file does not exist")
		return false # file not found
	
	var file = FileAccess.open(checkpoint_save_path, FileAccess.READ)
	if file:
		current_checkpoint_scene = file.get_pascal_string()
		var pos := file.get_var() as Vector2
		file.close()
		
		current_checkpoint_position = pos
		print("Checkpoint loaded: ", current_checkpoint_scene, pos)
		return true # got all data!
	else:
		print("Error opening file for reading")
		return false # failure to load

Now we can use this either as part of load_checkpoint or directly in the main scene

func load_checkpoint():
	if load_checkpoint_from_file():
		return {
			"position": current_checkpoint.global_position,
			"scene": current_checkpoint_scene
		}
	else:
		return null

This should make your program actually load the requested data. The next issue is about what data is being stored.


this stores the node’s path in the scene tree, that is why you are seeing “/root/Stage-02” instead of something like “Stage02.tscn”. You want to use scene_file_path

func activate_checkpoint():
	Globals.current_checkpoint = self
	Globals.current_checkpoint_scene = get_tree().current_scene.scene_file_path
	is_active = true
	Globals.save_checkpoint()
1 Like

Impressive how it actually correctly returned the scene file (folder path).

Message returned when saving:

Checkpoint saved: res://levels/world_02.tscn(331, 270)
--- Debugging process stopped ---

Message returned when closing and reopening the game:

Globals script ready
Checkpoint loaded: res://levels/world_02.tscn(331, 270)

But for some reason, it returned the error and did not start the game: Invalid get index ‘global_position’ (on base: ‘Nil’).

Could you show the script surrounding that error?

The error revolves around the following functions:

func load_checkpoint():
	if load_checkpoint_from_file():
		return { # specifically in this return
			"position": current_checkpoint.global_position,
			"scene": current_checkpoint_scene
		}
	else:
		return null
func _on_start_btn_pressed():
	var checkpoint_data = Globals.load_checkpoint()
	if checkpoint_data != null:
		Globals.player.position = checkpoint_data.position
		get_tree().change_scene_to_file(checkpoint_data.scene)
	else:
		get_tree().change_scene_to_file("res://levels/world_01.tscn")

Right in this function we want to return the stored position, rather than what is from current_checkpoint.

return {
	"position": current_checkpoint_position,
	"scene": current_checkpoint_scene
}
1 Like

I made the change and now it returned the error:

Invalid set index 'position' (on base: 'Nil') with value of type 'Vector2'.

Specifying the line:

Globals.player.position = checkpoint_data.position

I see, you should remove this variable

var current_checkpoint

and attempt to replace any instance of it with current_checkpoint_position. You have a lot of fragmented code, some referring to current_checkpoint, but most to current_checkpoint_position.

2 Likes

Deleted the reported variable and any other instance, however it returned:

Invalid set index 'position' (on base: 'Nil') with value of type 'Vector2'.

To be more precise, in the following line of the function:

func _on_start_btn_pressed():
	var checkpoint_data = Globals.load_checkpoint()
	if checkpoint_data != null:
		Globals.player.position = checkpoint_data.position #In this line
		get_tree().change_scene_to_file(checkpoint_data.scene)
	else:
		get_tree().change_scene_to_file("res://levels/world_01.tscn")

Sorry, my previous post is still good advice. This issue is Globals.player is nil, do you mean to set this variable after the scene is chaged? You may have to wait a frame.

get_tree().change_scene_to_file(checkpoint_data.scene)

# assuming Globals.player will be initialized after changing scene
await get_tree().process_frame
Globals.player.position = checkpoint_data.position

If this isn’t the solution could you share how Globals.player is set?

1 Like

I made the mentioned change, but it returned:

"Invalid cast: could not convert value to 'Vector2'."
E 0:00:00:0462   globals.gd:51 @ load_checkpoint_from_file(): Condition "len < 4" is true. Returning: ERR_INVALID_DATA
  <Origem C++>   core/io/marshalls.cpp:102 @ decode_variant()
  <Rastreamento de Pilha>globals.gd:51 @ load_checkpoint_from_file()
                 globals.gd:10 @ _ready()

E 0:00:00:0462   globals.gd:51 @ load_checkpoint_from_file(): Error when trying to encode Variant.
  <Erro C++>     Condition "err != OK" is true. Returning: Variant()
  <Origem C++>   core/io/file_access.cpp:301 @ get_var()
  <Rastreamento de Pilha>globals.gd:51 @ load_checkpoint_from_file()
                 globals.gd:10 @ _ready()

Location:

var pos := file.get_var() as Vector2

I’m not sure how to address the “Global.player” issue exactly; I’m trying to deepen my understanding of Godot, but the “player” variable is defined as “null” in the globals.gd file.

Sorry to take up your time!

I’ll also mention below the code from the “player.gd” file, maybe it helps:

extends CharacterBody2D


const SPEED = 130.0
const JUMP_VELOCITY = -300.0
var direction
# Get the gravity from the project settings to be synced with RigidBody nodes.
var gravity = ProjectSettings.get_setting("physics/2d/default_gravity")
var is_jumping := false

signal player_has_died()

@onready var animation := $anim as AnimatedSprite2D
@onready var remote_transform := $remote as RemoteTransform2D
@onready var rip_effect = $rip_effect as AudioStreamPlayer
@onready var timer = $Timer

func _physics_process(delta):
	# Add the gravity.
	if not is_on_floor():
		velocity.y += gravity * delta

	# Handle jump.
	if Input.is_action_just_pressed("ui_up") and is_on_floor():
		velocity.y = JUMP_VELOCITY
		is_jumping = true
	elif is_on_floor():
		is_jumping = false

	# Get the input direction and handle the movement/deceleration.
	# As good practice, you should replace UI actions with custom gameplay actions.
	direction = Input.get_axis("ui_left", "ui_right")
	if direction != 0:
		velocity.x = direction * SPEED
		animation.scale.x = direction
	else:
		velocity.x = move_toward(velocity.x, 0, SPEED)

	_set_state()
	move_and_slide()


func _on_hurtbox_body_entered(body):
	if body.is_in_group("enemies"):
		timer.wait_time = 0.5
		timer.one_shot = true
		rip_effect.play()
		timer.start()
		# Aguarda o Timer completar
		await timer.timeout
		emit_signal("player_has_died")
		get_tree().reload_current_scene()
		
		
func follow_camera(camera):
	var camera_path = camera.get_path()
	remote_transform.remote_path = camera_path
	
func _set_state():
	var state = "idle"
	
	if !is_on_floor():
		state = "jump"
	elif direction != 0:
		state = "run"
	
	if animation.name != state:
		animation.play(state)
		
func _input(event):
	if event is InputEventScreenTouch:
		if Input.is_action_just_pressed("ui_up") and is_on_floor():
			velocity.y = JUMP_VELOCITY
			is_jumping = true
		elif is_on_floor():
			is_jumping = false

interesting, I would’ve expected this error to pop up sooner. Are you sure the saved checkpoint file is in our new format? Try starting a new game and/or saving a checkpoint then try again. We can also split up that line to print it for debugging and better save error handling

var pos = file.get_var()

print(pos)
if pos is Vector2:
    current_checkpoint_position = pos
else:
    return false # invalid position
1 Like

Here are the translations:

Before starting a new game:

Globals script ready
<null>
Checkpoint saved: res://levels/world_02.tscn(331, 270)

Upon opening the game again:

Globals script ready
(331, 270)
Checkpoint loaded: res://levels/world_02.tscn(331, 270)

After opening the game:

Invalid get index 'process_frame' (on base: 'null instance').
E 0:00:14:0876   title_screen.gd:11 @ _on_start_btn_pressed(): Parameter "data.tree" is null.
  <Origem C++>   scene/main/node.h:413 @ get_tree()
  <Rastreamento de Pilha>title_screen.gd:11 @ _on_start_btn_pressed()

Regarding the checkpoint file (checkpoint.gd), it is structured as follows:

extends Area2D

var is_active = false

func _on_body_entered(body):
	if body.name == "player" and not is_active:
		activate_checkpoint()

func activate_checkpoint():
	Globals.current_checkpoint_position = self
	Globals.current_checkpoint_scene = get_tree().current_scene.scene_file_path
	is_active = true
	Globals.save_checkpoint()
func activate_checkpoint():
	Globals.current_checkpoint_position = global_position

self isn’t a position, use global_position instead.

I guess get_tree() is invalid after chaging scene, nvm on that await thing.

Still the only thing I can see is the world.gd has Globals.player = player which may be too late for this function, maybe add the checkpoint/respawn to the _ready on world.gd

1 Like

Please.
You are going about this in an extremely awkward way.

In Godot, like is common in object oriented programming, you can inherit methods and variables.

What you want to do is to create a world.tscn with an attached world.gd

This is what you should write in the world.gd at the top:

class_name World extends Node2D

Now you have a World class which inherits from Node2D.

So, you can start using that as the base for your worlds.

Everything that is going to be common in every world, you put in the. All the required variables and methods that will be the same in each.

Let me suggest that the next thing you do is to add a Marker and call it “PlayerSpawnPosition”.

In world.gd, you can now add this:

var player_scene = preload("res://path/to/player.tscn")

func _ready():
    spawn_player($PlayerSpawnPosition.global_position)

func spawn_player(spawn_position):
    var player_instance = player_scene.instantiate()
    player_instance.global_position = spawn_position
    add_child(player_instance)

Now, if you want to create world_1_tscn you simply right click this world.tscn and choose to create an inherited scene. Now, you will have a new scene that will inherit everything in your base world scene. So you simply change the location of $PlayerSpawnPoint…
Do the same for world_2, world_3, etc…

1 Like

This doesn’t solve anything if they have multiple spawn positions in a world.

1 Like

Then make it an array of spawn points and only store the index.

He’s going to want to create an entire game, not just solve this specific issue.

There is a fundamental complexity in the approach which is simply awkward architecture.

In my opinion, the best help here is to point to a more solid foundation.

Helping someone to learn how to do bad architecture is, in my opinion, not helpful.

If I’m doing things in an awkward way, I appreciate when someone shows me something that is more maintainable.

2 Likes

I really don’t think spawning into a previous position is such a bad thing. It’s how any save-on-the-spot system will work.

As for productive foundations I do think there are flaws in the code, but we need to take it one step at a time and point out those flaws; what good is a new foundation if you haven’t learned from the faulty one?

2 Likes