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.

Ferramenta:Godot

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.

Curso de Godot
Quer fazer seu primeiro jogo na Godot?

No curso de Godot da CursoGame.Dev você vai do zero ao primeiro jogo, com um desafio prático em cada level e suporte individual no WhatsApp quando travar.

Conhecer o curso de Godot
Suporte no WhatsAppAcesso vitalícioGarantia de 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.