Replay e carro-fantasma na Godot 4: grave e reproduza

Como montar um sistema de replay e carro-fantasma na Godot 4: grave amostras em passo fixo, reproduza interpolando e salve o recorde do jogador.
Bater o próprio recorde é um dos loops de gameplay mais viciantes que existem, e um sistema de replay com carro-fantasma (o ghost translúcido que corre a sua melhor volta ao seu lado) é o que fecha esse loop. Neste guia a gente monta um replay e um fantasma na Godot 4 do zero, gravando a jogada durante um time-trial e reproduzindo depois. Vou focar na abordagem mais robusta pra quem está começando: gravar amostras do estado do jogador em passo fixo e reproduzir interpolando entre elas. No fim cito a alternativa de gravar inputs, que é mais leve, mas exige cuidado.
Antes de qualquer coisa, um aviso pra não confundir conceitos. Isto aqui não tem nada a ver com configurar controles. Se o que você quer é mapear teclas e botões pra ações como acelerar e frear, isso é outra camada, e eu explico com calma no post sobre configurar o Input Map e os controles na Godot. O sistema de replay vive uma camada acima: ele grava o resultado do que o jogador fez ao longo do tempo, não a tecla em si.
Duas formas de gravar um replay
Existem dois caminhos clássicos pra guardar uma jogada, e vale entender os dois antes de escolher.
O primeiro é gravar o estado. A cada intervalo fixo você tira uma foto da posição e da rotação do jogador e joga num array. Pra reproduzir, você percorre essas fotos na mesma cadência e coloca o fantasma exatamente onde o jogador esteve. É robusto porque não depende de a física se comportar igual duas vezes: você está reproduzindo o que aconteceu, não recalculando. O custo é o espaço, já que cada amostra carrega alguns floats.
O segundo é gravar inputs. Em vez da posição, você salva só quais ações estavam pressionadas em cada frame de física. Pra reproduzir, você roda a simulação de novo alimentando essas ações no lugar do teclado. É determinístico e muito mais leve, porque um input cabe em poucos bits. O problema é a fragilidade: se a física da Godot produzir um resultado minimamente diferente entre a gravação e a reprodução (mudança de versão da engine, ordem de colisões, delta variável, qualquer aleatoriedade sem seed fixa), o fantasma sai da pista e o replay desanda. Gravar inputs é a técnica dos speedruns e dos jogos de luta, mas exige uma simulação 100% determinística, o que é trabalho de gente grande.
Pra iniciante intermediário, gravar estado é a escolha certa. Ela perdoa. É o que vamos construir.
A estrutura de dados do replay
Uma amostra precisa de três coisas: onde o jogador estava, pra onde estava apontando, e em que momento da corrida isso foi. Como estamos num contexto 2D top-down, posição é Vector2 e rotação é um float em radianos.
Em vez de trabalhar com dicionários soltos, vale criar uma classe tipada pequena pra amostra. Fica legível e o autocomplete te ajuda.
class_name ReplayFrame
extends RefCounted
var time: float = 0.0
var position: Vector2 = Vector2.ZERO
var rotation: float = 0.0
func _init(t: float = 0.0, pos: Vector2 = Vector2.ZERO, rot: float = 0.0) -> void:
time = t
position = pos
rotation = rot
O replay inteiro é só um Array[ReplayFrame]. Simples assim. Um recorde de um minuto gravado a cada frame de física dá alguns milhares de amostras, o que é perfeitamente confortável em memória.
Gravando em passo fixo com _physics_process
Aqui está a regra de ouro: grave dentro do _physics_process, não do _process. O _physics_process roda com um delta estável (o passo de física da Godot), então cada amostra cobre exatamente o mesmo intervalo de tempo. Se você gravasse no _process, o intervalo variaria conforme o FPS da máquina, e na hora de reproduzir o fantasma ficaria rápido demais numa máquina e lento demais em outra.
Vou colocar o gravador num nó separado, um Node que fica pendurado no jogador. Ele recebe a referência do corpo do jogador e vai enfileirando amostras enquanto a corrida acontece.
class_name ReplayRecorder
extends Node
@export var target: Node2D
@export var sample_interval: float = 0.05
var _frames: Array[ReplayFrame] = []
var _elapsed: float = 0.0
var _since_last: float = 0.0
var _recording: bool = false
func start() -> void:
_frames.clear()
_elapsed = 0.0
_since_last = sample_interval
_recording = true
func stop() -> Array[ReplayFrame]:
_recording = false
return _frames
func _physics_process(delta: float) -> void:
if not _recording or target == null:
return
_elapsed += delta
_since_last += delta
if _since_last >= sample_interval:
_since_last = 0.0
var frame: ReplayFrame = ReplayFrame.new(
_elapsed, target.global_position, target.rotation
)
_frames.append(frame)
Repare no sample_interval. Você não precisa gravar todo frame de física. Uma amostra a cada 0.05 segundos (vinte por segundo) costuma ser mais que suficiente pra um carro, porque a interpolação na reprodução preenche os buracos. Quanto maior o intervalo, menor o arquivo, e o segredo pra isso não ficar travado é justamente a interpolação, que vem agora.
Se quiser ver o carro que esse gravador acompanha sendo montado do zero, com aceleração e derrapagem, eu detalho num tutorial dedicado sobre controlar um carro top-down 2D na Godot. O gravador aqui só lê global_position e rotation de qualquer Node2D, então ele acopla em qualquer corpo que você já tenha.
Perguntas frequentes
Qual a diferença entre gravar estado e gravar inputs no replay?
Gravar estado salva posição e rotação por amostra, é robusto e funciona mesmo com física não determinística, mas ocupa mais espaço. Gravar inputs salva só as teclas por frame e reproduz a simulação de novo, é leve e determinístico, porém quebra se qualquer parte da física variar entre execuções.
Por que gravar no _physics_process e não no _process?
O _physics_process roda em passo fixo, com delta estável, então cada amostra representa o mesmo intervalo de tempo. No _process o intervalo varia com o FPS, o que faz o fantasma reproduzido acelerar ou engasgar dependendo da máquina.
Como o fantasma fica suave se eu gravo poucas amostras por segundo?
Você interpola. Guarda o tempo de reprodução, descobre entre quais duas amostras ele cai e usa lerp na posição e slerp ou lerp_angle na rotação. Assim o movimento entre amostras vira uma linha contínua, mesmo com taxa de amostragem baixa.
Isso é a mesma coisa que configurar o Input Map?
Não. O Input Map só mapeia teclas e botões para nomes de ação, como acelerar ou frear. O sistema de replay grava o resultado dessas ações ao longo do tempo para reproduzir a jogada depois. São camadas diferentes.
Como salvo e carrego o replay em disco?
Serialize o array de amostras para um formato simples, como JSON, e grave com FileAccess em user://. Para carregar, leia o arquivo, faça o parse e reconstrua o array tipado convertendo os valores de volta para Vector2 e float.


