Voltar para o Blog
Quest Log

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

Carro azul correndo ao lado de um fantasma translúcido em uma pista top-down na Godot 4

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.

Próximo nível
Quer aprender isso na prática?

No CursoGame.Dev você sai dos tutoriais soltos e constrói jogos publicáveis, com trilha progressiva, quests práticas e feedback real.

Conhecer a plataforma
+500 alunos4.9/5Garantia 7 dias

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.