Search code examples
3dgame-enginegdscriptgodot4

Multi-Checkpoint System in Godot 4 (3D)


I am very new to Godot, but have some experience in programming in Python. I am making a 3D simple movement game to learn about the engine, and I want to add checkpoints throughout the level. I followed this tutorial (https://youtu.be/AEdEUeK_g4A) to get most of the code for the checkpoints, but it didn't work. I have spent a few more hours researching and changing the code, and now I am nearly there, I just can't figure out how to get the checkpoint to know where its position is.

I have moved things around from the original tutorial, and now the checkpoint script (extends Area3D) is autoloaded, so I can access it from other scripts. I have tested (with the print function) and it knows when the area has been entered, but when I press to load checkpoint, it takes me back to 0,0,0. I have set the initial check_point_pos vector3 to be 10,10,10, and it brings me there. So it seems the issue is that the vector3 is not being updated. What do I need to add to make the vector3 update with the correct position for each checkpoint? Ive tried a few things, but nothing is working so far.

extends Area3D

var check_point_pos = Vector3.ZERO

func save_check_point(pos_x, pos_y, pos_z):
    check_point_pos.x = pos_x
    check_point_pos.y = pos_y
    check_point_pos.z = pos_z

func _on_area_entered(_body):
    print("checkpoint")
    save_check_point(self.position.x, self.position.y, self.position.z)
if Input.is_action_pressed("load_checkpoint"):
    set_position(checkpt.check_point_pos)

[enter image description here](https://i.sstatic.net/an3cO.png)


Solution

  • The basic check point system needs two parts.

    • A place to store the last reached check point. Which - following your approach - I will do with an Autoload.
    • A check point you place in the world that will store the position when triggered by the player character. This will be an Area3D.

    This is the part of your code that stores the last reached check point (and hence what would be in an Autoload):

    extends Area3D
    
    var check_point_pos = Vector3.ZERO
    
    func save_check_point(pos_x, pos_y, pos_z):
        check_point_pos.x = pos_x
        check_point_pos.y = pos_y
        check_point_pos.z = pos_z
    

    These are the modifications I will do:

    • It does not need to be an Area3D, it fact it does not need a position in the world. So I'll change it Node.
    • It does not need a method to save the check point position because we can set the variable directly. So I'll remove save_check_point.
    • And while I'm at it, I'll set the type of the variable so it is no longer a Variant.

    This is the result (which, again, is what you will make into an Autoload):

    extends Node
    
    var check_point_pos:Vector3
    

    Make sure you give it a name when making it an Autoload. I believe you are using the name checkpt, so I'll use that for this answer.


    And the Area3D you place in the world, has this part of the code:

    extends Area3D
    
    func _on_area_entered(_body):
        print("checkpoint")
        save_check_point(self.position.x, self.position.y, self.position.z)
    

    These are the modifications I'll make:

    • We need to use the Autoload to set the variable. And since I have removed save_check_point, we will set the variable directly.
    • We do not want to use position because that is relative to the parent Node3D. Instead we will use global_position which is relative to the game world origin.
    • And while I'm at it, I'll give it a class name, so it shows up in the dialog to add a new node.
    • And I'll have it connect the area entered (¿?) to itself.

    This is the result:

    class_name CheckPoint
    extends Area3D
    
    
    func _ready() -> void:
        if not area_entered.is_connected(_on_area_entered):
            area_entered.connect(_on_area_entered)
    
    
    func _on_area_entered(_area:Area3D) -> void:
        print("checkpoint")
        checkpt.check_point_pos = global_position
    

    I would have expected that you would use body_entered instead of area_entered. Double check that area_entered is what you want. If you actually want body_entered the code would be like this:

    class_name CheckPoint
    extends Area3D
    
    
    func _ready() -> void:
        if not body_entered.is_connected(_on_body_entered):
            body_entered.connect(_on_body_entered)
    
    
    func _on_body_entered(_body:Node3D) -> void:
        print("checkpoint")
        checkpt.check_point_pos = global_position
    

    I recommend to change the collision mask so that only the player character will trigger it, and nothing else.


    Of course, recalling the check point looks like this (in your player character):

    global_position = checkpt.check_point_pos
    

    I also want to note that using an static var is a viable alternative, this way you do not need an Autoload and everything will be in a single script:

    class_name CheckPoint
    extends Area3D
    
    
    static var check_point_pos:Vector3
    
    
    func _ready() -> void:
        if not area_entered.is_connected(_on_area_entered):
            area_entered.connect(_on_area_entered)
    
    
    func _on_area_entered(_area:Area3D) -> void:
        print("checkpoint")
        CheckPoint.check_point_pos = global_position
    

    This time recalling the check point looks like this (in your player character):

    global_position = CheckPoint.check_point_pos