Checkpoint e Respawn no Godot 4: Salvar a Posicao do Player

Aprenda a montar checkpoint godot com Area2D, autoload guardando a posicao do player, sinal de morte e respawn certo. Tutorial pratico em GDScript.
Quase todo jogo de plataforma ou acao precisa de um lugar pra onde o player volta quando morre. Sem isso, qualquer queda joga o jogador de volta pro comeco da fase, e isso cansa rapido. Montar um sistema de checkpoint godot nao e dificil, mas tem alguns detalhes que decidem se ele vai funcionar de verdade ou te dar dor de cabeca: onde guardar a posicao salva, quando salvar, e como reposicionar o player sem quebrar o estado dele.
Checkpoint e Respawn no Godot 4: Salvar a Posicao do Player
Neste tutorial a gente vai montar um checkpoint com Area2D, guardar a ultima posicao num autoload (singleton), disparar um sinal quando o player morre e reposicionar ele usando global_position. No fim, voce vai ter um sistema que aguenta varios checkpoints na mesma fase. Tudo em GDScript de Godot 4.x, sem pseudo-codigo.
Por que um autoload guarda a posicao do checkpoint godot
A primeira pergunta e: onde a posicao salva fica? Se voce guardar ela no proprio player, perde tudo quando o player e destruido (e quando ele morre, normalmente some da cena). Se guardar no node do checkpoint, fica espalhado e dificil de consultar na hora do respawn.
A solucao mais limpa e um autoload. Um autoload e um script que o Godot carrega uma vez e mantem vivo durante toda a execucao do jogo, acessivel de qualquer lugar pelo nome. Ele sobrevive a troca de cena e a morte do player, entao e o lugar natural pra guardar "onde o player deve reaparecer".
Crie um script chamado CheckpointManager.gd:
extends Node
# Posicao onde o player vai reaparecer.
var respawn_position: Vector2 = Vector2.ZERO
# Guarda se ja existe um checkpoint ativado nesta fase.
var has_checkpoint: bool = false
func set_checkpoint(pos: Vector2) -> void:
respawn_position = pos
has_checkpoint = true
func get_respawn_position(fallback: Vector2) -> Vector2:
# Se nenhum checkpoint foi tocado, volta pro ponto inicial.
if has_checkpoint:
return respawn_position
return fallback
func reset() -> void:
# Chame ao reiniciar a fase do zero.
respawn_position = Vector2.ZERO
has_checkpoint = false
Pra registrar como autoload, va em Project > Project Settings > Globals (aba Autoload), aponte pro arquivo CheckpointManager.gd e de o nome CheckpointManager. A partir dai voce chama CheckpointManager.set_checkpoint(...) de qualquer script. O fallback existe porque, no comeco da fase, o jogador ainda nao tocou em nada, e voce nao quer mandar ele pro Vector2.ZERO (canto da tela).
O checkpoint em si: um Area2D que detecta o player
O checkpoint precisa saber quando o player passa por ele. Area2D e ideal pra isso porque ele detecta sobreposicao sem empurrar nada, diferente de um StaticBody2D. O player atravessa a area normalmente e a area so avisa "alguem entrou".
Monte uma cena com esta estrutura:
Checkpoint (Area2D)
├── CollisionShape2D
└── Sprite2D (a bandeira, opcional)
No script do Area2D, conecte o sinal body_entered. Esse sinal dispara quando um PhysicsBody2D (seu player, por exemplo) encosta na area:
extends Area2D
# Marca se este checkpoint ja foi ativado, pra nao salvar toda hora.
var activated: bool = false
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
# So reage ao player. Use grupos pra identificar.
if not body.is_in_group("player"):
return
if activated:
return
activated = true
CheckpointManager.set_checkpoint(global_position)
_play_feedback()
func _play_feedback() -> void:
# Feedback visual simples: muda a cor da bandeira.
var sprite := get_node_or_null("Sprite2D") as Sprite2D
if sprite:
sprite.modulate = Color(0.4, 1.0, 0.4)
Repare em dois detalhes. Primeiro, eu uso body.is_in_group("player") em vez de checar o nome do node. Nomes mudam, grupos sao estaveis. No player, adicione o node ao grupo "player" pelo editor (aba Node > Groups) ou por codigo com add_to_group("player"). Segundo, a flag activated evita que o checkpoint salve a posicao varias vezes enquanto o player fica em cima dele. Salvar uma vez basta.
Uma pergunta comum: salvar global_position do checkpoint ou do player? Use o do checkpoint. Assim o player sempre reaparece num ponto previsivel e seguro (em cima da bandeira), e nao num lugar aleatorio onde ele estava quando cruzou a borda da area.
O sinal de morte do player
Agora o lado do player. Quando ele morre, alguem precisa ser avisado pra cuidar do respawn. Em vez de o player tentar se reposicionar sozinho (ele vai estar no meio de uma animacao de morte, ou ate queue_free), o jeito mais organizado e o player emitir um sinal e deixar a fase decidir o que fazer.
No script do player, declare um sinal e emita ele quando a vida chega a zero. Se voce ja tem um sistema de vida e dano montado, e provavel que ja exista um ponto certo pra disparar isso.
extends CharacterBody2D
signal died
@export var max_health: int = 3
var health: int = max_health
func _ready() -> void:
add_to_group("player")
health = max_health
func take_damage(amount: int) -> void:
health -= amount
if health <= 0:
die()
func die() -> void:
# Evita emitir o sinal duas vezes em quadros seguidos.
set_physics_process(false)
died.emit()
O set_physics_process(false) para a fisica do player no instante da morte. Sem isso, o player pode continuar caindo ou recebendo dano enquanto a animacao de morte roda, o que gera bugs estranhos como morrer duas vezes no mesmo frame.
Reposicionando o player com global_position
Quem escuta o sinal died e a cena da fase (ou um node de controle do nivel). Ela decide se vai respawnar o player ou, dependendo do seu jogo, mostrar a tela de game over quando as vidas acabam. Aqui vamos focar no respawn direto.
No script da fase, conecte o sinal do player e mande ele de volta pro checkpoint:
extends Node2D
@onready var player: CharacterBody2D = $Player
# Ponto inicial caso nenhum checkpoint tenha sido tocado.
@export var start_position: Vector2 = Vector2(100, 100)
func _ready() -> void:
player.died.connect(_on_player_died)
func _on_player_died() -> void:
# Espera um instante pra animacao de morte aparecer.
await get_tree().create_timer(0.8).timeout
respawn_player()
func respawn_player() -> void:
var pos := CheckpointManager.get_respawn_position(start_position)
player.global_position = pos
player.velocity = Vector2.ZERO
player.health = player.max_health
player.set_physics_process(true)
A linha que faz o trabalho e player.global_position = pos. Use global_position, nao position. A diferenca importa: position e relativo ao node pai, entao se o player estiver dentro de algum container ou nodo com offset, definir position coloca ele no lugar errado. global_position e a coordenada no mundo, exatamente o que voce salvou do checkpoint (que tambem usou global_position). Misturar os dois e a causa numero um de "meu player respawna no lugar errado".
Depois de mover, eu zero velocity. Sem isso, o player carrega a velocidade que tinha no momento da morte (geralmente caindo) e desliza ou cai de novo assim que reaparece. Tambem restauro a vida e religo set_physics_process(true), que tinha sido desligado no die().
Se voce quiser uma animacao de morte completa antes de sumir, mantenha o player na cena e so esconda/mostre o sprite, em vez de dar queue_free. Como a posicao salva mora no autoload, o respawn funciona dos dois jeitos, mas manter o mesmo node vivo e mais simples pra comecar.
Lidando com varios checkpoints na fase
O bom da abordagem com autoload e que multiplos checkpoints saem de graca. Cada Area2D de checkpoint, ao ser tocado, chama CheckpointManager.set_checkpoint(global_position) e sobrescreve a posicao anterior. O player sempre reaparece no ultimo que tocou. Voce nao precisa de codigo extra pra isso.
Tem um cuidado pratico, porem: e comum o jogador voltar e cruzar um checkpoint anterior ao ja ter ativado um mais a frente. Se cada area so guarda seu proprio estado, voltar pra tras nao deveria "regredir" o respawn. Como o nosso set_checkpoint simplesmente sobrescreve, andar pra tras e tocar um checkpoint antigo move o respawn pra tras tambem, o que pode nao ser o que voce quer.
Uma forma de evitar isso e dar uma ordem a cada checkpoint e so aceitar se for mais avancado que o atual:
extends Area2D
@export var order: int = 0
func _ready() -> void:
body_entered.connect(_on_body_entered)
func _on_body_entered(body: Node2D) -> void:
if not body.is_in_group("player"):
return
if order <= CheckpointManager.current_order:
return
CheckpointManager.set_checkpoint(global_position, order)
E no autoload, guarde a ordem junto:
extends Node
var respawn_position: Vector2 = Vector2.ZERO
var has_checkpoint: bool = false
var current_order: int = -1
func set_checkpoint(pos: Vector2, order: int) -> void:
respawn_position = pos
current_order = order
has_checkpoint = true
func get_respawn_position(fallback: Vector2) -> Vector2:
if has_checkpoint:
return respawn_position
return fallback
func reset() -> void:
respawn_position = Vector2.ZERO
has_checkpoint = false
current_order = -1
Assim, voltar pra tras nunca diminui o progresso. Defina order em cada checkpoint pelo inspector (1, 2, 3...) e pronto. Se a sua fase nao tem backtracking, pode ignorar isso e usar a versao simples; nem todo jogo precisa.
Quando o jogo fecha: persistir entre sessoes
O autoload resolve a posicao dentro de uma sessao, mas ele zera quando o jogo fecha. Se voce quer que o jogador feche o jogo e volte no mesmo checkpoint depois, precisa gravar respawn_position e current_order num arquivo. Esse e um problema separado, de save de progresso, e da pra resolver com o mesmo autoload escrevendo num ConfigFile ou JSON. Se voce ainda nao tem essa base, o tutorial de salvar e carregar no Godot cobre exatamente como persistir esse tipo de estado entre sessoes.
Pra integrar, bastaria o CheckpointManager chamar uma funcao de save sempre que set_checkpoint rodar, e carregar esses valores no _ready. A logica de respawn que voce ja montou continua igual.
Fechamento
O esquema todo se resume a tres pecas que conversam por mensagem, nao por acoplamento direto. O Area2D detecta o player e avisa o autoload. O autoload guarda a posicao e sobrevive a morte e a troca de cena. O player emite died e deixa a fase reposicionar ele com global_position. Cada peca faz uma coisa so, e por isso fica facil de estender: adicionar mais checkpoints, ligar com a tela de game over, ou persistir em disco depois.
Comece pela versao simples (um autoload, um Area2D, um sinal) e so adicione ordem de checkpoint ou save em arquivo quando o seu jogo realmente pedir. Um sistema de checkpoint godot bom e aquele que o jogador nem percebe: ele morre, reaparece no lugar certo, e volta a jogar.


