Delta Time: Por Que Multiplicar Movimento por Delta no Seu Jogo

Entenda delta time no jogo: por que multiplicar movimento por delta deixa a velocidade independente do FPS e evita o bug clássico de física inconsistente.
Delta Time: Por Que Multiplicar Movimento por Delta no Seu Jogo
Você testa o jogo na sua máquina e o personagem anda na velocidade certa. Manda pra um amigo com um monitor de 144 Hz e o personagem sai voando, mais que o dobro da velocidade. Mesmo código, mesmo jogo, resultado completamente diferente. Esse é o bug que o delta time resolve, e entender delta time no jogo é uma daquelas coisas que separam quem copia tutorial de quem sabe o que está escrevendo.
A regra que todo mundo decora é "multiplica por delta". A parte que quase ninguém explica é o porquê, quando a regra vale e, principalmente, quando ela não vale. É isso que esse artigo cobre, com código GDScript do Godot 4 que roda como está.
O que é delta time, afinal
Todo jogo roda em loop: processa input, atualiza o mundo, desenha na tela. Cada volta desse loop é um frame. O problema é que ninguém garante quanto tempo cada volta demora.
Numa máquina potente rodando uma cena leve, o loop gira 144 vezes por segundo ou mais. Num notebook fraco com a cena pesada, gira 30. E dentro da mesma máquina a taxa varia: uma explosão cheia de partículas entra na tela e o frame que custava 7 ms passa a custar 20.
Delta time é simplesmente isso: o tempo, em segundos, que passou desde o frame anterior. A 60 FPS, o delta fica em torno de 0,0166 segundos. A 30 FPS, em torno de 0,033. É a engine medindo o relógio e te entregando o valor pronto. No Godot, ele chega como parâmetro:
func _process(delta):
# delta = segundos desde o último frame (float, ex: 0.016)
pass
Em Unity é Time.deltaTime, em Unreal é o DeltaSeconds do Tick, em GameMaker é delta_time (em microssegundos, detalhe que já pegou muita gente). O conceito é idêntico em qualquer engine: o tempo do último frame, pra você usar nas contas.
O erro comum: somar um valor fixo por frame
Aqui está o código que parece certo e está errado:
extends Sprite2D
func _process(delta):
position.x += 5 # 5 pixels POR FRAME, não por segundo
O que esse código diz de verdade é "ande 5 pixels a cada volta do loop". Só que a quantidade de voltas por segundo depende da máquina:
- A 60 FPS: 5 × 60 = 300 pixels por segundo
- A 144 FPS: 5 × 144 = 720 pixels por segundo
- A 30 FPS: 5 × 30 = 150 pixels por segundo
A velocidade do seu personagem virou função do hardware do jogador. No monitor gamer ele corre, no notebook ele se arrasta. E não é só estética: se o pulo do player sobe mais alto a 144 FPS, você tem fases que só fecham em certas máquinas. Jogos antigos de PC sofriam exatamente disso, foram feitos assumindo uma taxa fixa e ficaram injogáveis em hardware mais rápido.
O mesmo erro aparece disfarçado em outros lugares: timer que desconta 1 por frame, vida que regenera 0.5 por frame, câmera que persegue o player somando um passo fixo. Qualquer valor somado por frame sem delta é velocidade amarrada no FPS.
Multiplicar por delta: a correção
A solução é parar de pensar em "pixels por frame" e pensar em pixels por segundo. Aí você multiplica essa velocidade pelo tempo que o frame durou:
extends Sprite2D
const SPEED = 300.0 # pixels por segundo
func _process(delta):
position.x += SPEED * delta
A conta fecha em qualquer FPS:
- A 60 FPS: 300 × 0,0166 ≈ 5 pixels por frame, 60 frames = 300 px/s
- A 144 FPS: 300 × 0,0069 ≈ 2,1 pixels por frame, 144 frames = 300 px/s
- A 30 FPS: 300 × 0,033 ≈ 10 pixels por frame, 30 frames = 300 px/s
Frames mais frequentes dão passos menores, frames mais raros dão passos maiores, e a distância percorrida em um segundo é sempre a mesma. Isso é movimento frame independente: o jogo se comporta igual no PC do desenvolvedor e no PC do jogador.
A intuição física ajuda a fixar: distância = velocidade × tempo. O delta é o tempo. Sem ele, você está somando velocidade direto na posição, o que dimensionalmente nem faz sentido. Quando bater a dúvida "multiplico por delta aqui?", pergunte: esse valor é uma taxa por segundo? Se sim, multiplica.
E o mesmo vale pra qualquer coisa que muda continuamente ao longo do tempo, não só posição:
var fuel = 100.0
const BURN_RATE = 2.5 # unidades por segundo
const TURN_SPEED = PI # radianos por segundo (meia volta/s)
func _process(delta):
fuel -= BURN_RATE * delta
rotation += TURN_SPEED * delta
Delta time no jogo real: _process vs _physics_process
No Godot existem dois loops, e o papel do delta muda um pouco entre eles.
O _process(delta) roda uma vez por frame renderizado, com delta variável. O _physics_process(delta) roda em passo fixo, por padrão 60 vezes por segundo, e o delta dele é praticamente constante (0,0166...). É aqui que mora uma dúvida clássica: "se o delta do physics é fixo, pra que multiplicar?".
Dois motivos. Primeiro, o tick de física é configurável: se você (ou um port pra mobile) mudar de 60 pra 30 ticks em Project Settings, todo código que assumiu o delta fixo quebra junto. Segundo, manter as unidades em "por segundo" deixa o código legível: SPEED = 300.0 significa 300 pixels por segundo, um valor que você consegue raciocinar e ajustar. Multiplicar por delta no physics não custa nada e protege o código de mudança de configuração.
Agora, o detalhe que mais confunde iniciante no Godot 4: o move_and_slide() já aplica o delta por dentro.
extends CharacterBody2D
const SPEED = 300.0
const GRAVITY = 980.0
func _physics_process(delta):
# Aceleração muda a velocidade: multiplica por delta.
if not is_on_floor():
velocity.y += GRAVITY * delta
# Velocity já está em pixels/segundo: NÃO multiplica.
velocity.x = Input.get_axis("ui_left", "ui_right") * SPEED
# move_and_slide() aplica velocity * delta internamente.
move_and_slide()
A regra de bolso pra CharacterBody: velocity é em pixels por segundo e o move_and_slide() cuida do delta da posição. Mas qualquer coisa que altera a velocity ao longo do tempo (gravidade, aceleração, fricção) é uma taxa de mudança, então essa parte multiplica por delta sim. Se você multiplicar a velocity inteira por delta antes do move_and_slide(), o personagem vai se mover a 1/60 da velocidade esperada, parado na prática. É um bug comum e silencioso.
O que NÃO multiplicar por delta
A regra "tudo multiplica por delta" também erra, só que pro outro lado. Três casos onde delta não entra:
Eventos instantâneos. Pulo, tiro, dano de um golpe, knockback de explosão. São mudanças que acontecem uma vez, não taxas contínuas:
if Input.is_action_just_pressed("jump") and is_on_floor():
velocity.y = -400.0 # valor direto, sem delta
Multiplicar o impulso do pulo por delta transformaria o pulo em algo minúsculo e dependente do frame em que aconteceu. Se o valor é aplicado uma única vez, ele entra inteiro.
Valores que já são posição ou estado final. Teleporte, snap em grid, setar posição de spawn. Você está atribuindo, não integrando ao longo do tempo.
O terceiro argumento do lerp. Esse merece seção própria.
O caso do lerp por frame
Esse padrão aparece em todo lugar pra suavizar câmera e movimento:
func _process(delta):
position = position.lerp(target, 0.1) # frame dependente!
Parece inocente, mas o 0.1 significa "cubra 10% da distância restante a cada frame". A 144 FPS isso acontece 144 vezes por segundo e a câmera gruda no alvo quase instantaneamente; a 30 FPS ela se arrasta. É o mesmo bug do movimento sem delta, só que escondido dentro de uma curva.
E a correção ingênua, trocar 0.1 por velocidade * delta, melhora mas não resolve de verdade, porque o lerp é exponencial e não linear. A forma matematicamente correta de deixar esse decaimento independente de frame usa exponencial:
const SMOOTHING = 8.0 # maior = converge mais rápido
func _process(delta):
var t = 1.0 - exp(-SMOOTHING * delta)
position = position.lerp(target, t)
O exp(-SMOOTHING * delta) garante que a fração coberta num intervalo de tempo seja a mesma, não importa em quantos frames esse intervalo foi fatiado. Pra suavização de câmera num projeto pequeno, o lerp com velocidade * delta já fica visualmente aceitável. Mas se a suavização afeta gameplay (mira assistida, perseguição de inimigo), use a versão com exponencial.
Física consistente: por que existe o passo fixo
Se delta resolve tudo, por que as engines têm um loop de física separado com passo fixo?
Porque multiplicar por delta torna o movimento independente de frame, mas não torna a simulação determinística. Com delta variável, a trajetória de um pulo é calculada em fatias de tamanho diferente a cada execução, e erros de arredondamento se acumulam de formas diferentes. Detecção de colisão sofre ainda mais: um frame longo (delta grande) faz um projétil dar um passo enorme e atravessar uma parede fina sem nunca tocar nela, o famoso tunneling.
O passo fixo ataca isso na raiz: a física sempre avança em fatias do mesmo tamanho, independente de quão rápido a tela renderiza. Por isso a regra prática no Godot é direta: movimento, gravidade e tudo que toca colisão vai em _physics_process; efeito visual, HUD e câmera podem ficar em _process. E nos dois lugares, taxas por segundo multiplicam por delta.
Resumo pra colar na parede
- Delta time é o tempo em segundos desde o último frame.
- Valor que muda continuamente é taxa por segundo: multiplica por delta. Vale pra posição, rotação, vida regenerando, timer descontando.
- Evento instantâneo (pulo, impulso, teleporte) aplica o valor inteiro, sem delta.
- No Godot 4,
move_and_slide()já aplica o delta na velocity; só as mudanças de velocity (gravidade, aceleração) multiplicam por delta. - Lerp por frame com fator fixo é frame dependente; use
1.0 - exp(-k * delta)quando a suavização importa. - Física e colisão ficam no
_physics_process, que roda em passo fixo justamente pra simulação ser consistente.
Delta time é fundamento, não detalhe. Depois que a ideia de "taxa por segundo vezes tempo do frame" assenta, você para de decorar regra e passa a enxergar na hora o que multiplica e o que não multiplica. Pra testar se assentou: abra um projeto seu, limite o FPS a 30 em Project Settings (Application > Run > Max FPS) e veja se o jogo se comporta igual. Se algo ficou mais lento, você acabou de achar um lugar sem delta.


