Programação de jogos

Física independente da taxa de quadros: do passo por quadro ao passo fixo

Por Fernando Capanema, SpiderWorks · Publicado em

Celulares atuais atualizam a tela a 60, 90 ou 120 vezes por segundo, e o navegador chama requestAnimationFrame nesse ritmo. Se o jogo move objetos uma quantidade fixa por quadro, ele roda mais rápido nos aparelhos melhores. Três jogos do catálogo SpiderWorks mostram estágios diferentes desse problema, e a revisão de outubro de 2026 levou dois deles ao modelo mais rigoroso.

O problema

Em um laço de jogo típico, cada chamada de requestAnimationFrame atualiza posições e desenha a cena. A 60 Hz isso acontece a cada 16,7 ms; a 120 Hz, a cada 8,3 ms. Se uma bola anda 4 pixels por chamada, ela percorre 240 pixels por segundo em uma tela e 480 em outra. O mesmo vale para gravidade, temporizadores contados em quadros e velocidade de rolagem.

A taxa também varia durante a partida: o navegador pode reduzir o ritmo em economia de energia, um coletor de lixo pode atrasar um quadro, e a aba em segundo plano para de receber quadros. Um jogo robusto precisa responder a duas perguntas: quanto tempo passou desde o último quadro, e o que fazer quando passou tempo demais.

Brick Breaker: um passo por quadro

A versão 1.0.0 do Brick Breaker Neon, testada em setembro de 2026, segue o modelo mais simples:

ball.x += ball.dx;
ball.y += ball.dy;
// ...
drop.y += 2.2;
if (powerTimer > 0 && --powerTimer === 0) { /* fim do poder */ }

A velocidade da bola é expressa em pixels por quadro (até 9), os poderes caem 2,2 pixels por quadro e o efeito WIDE dura 600 quadros, que são 10 segundos a 60 Hz e 5 segundos a 120 Hz. Pela leitura do código, em uma tela de 120 Hz o jogo tende a ficar duas vezes mais rápido e os poderes duram metade do tempo. A medição em aparelho de 120 Hz não chegou a ser feita: o problema foi identificado na leitura do código e corrigido antes de qualquer envio à loja.

Há um segundo efeito, menos óbvio: com passos grandes, a bola pode atravessar a borda de um tijolo e colidir com dois no mesmo quadro, invertendo a direção duas vezes. Passos menores (taxa mais alta) reduzem isso; passos maiores (quedas de desempenho) aumentam.

Cyber Bird: escala de tempo limitada

A versão 1.0.0 do Cyber Bird mede o tempo real entre quadros e o converte em uma escala relativa a 60 Hz:

const deltaScale = Math.max(.25, Math.min(2, elapsed / (1000 / 60)));
bird.vy = Math.min(bird.maxFall, bird.vy + bird.gravity * deltaScale);
bird.y += bird.vy * deltaScale;
p.x -= speed * deltaScale;

A vantagem é prática: todas as constantes já ajustadas em “unidades por quadro a 60 Hz” continuam valendo. A 120 Hz, deltaScale fica perto de 0,5 e cada quadro aplica metade do movimento. Os limites de 0,25 e 2 são uma decisão de projeto. O limite superior impede que uma pausa de meio segundo (por exemplo, uma notificação) faça o pássaro cair através do chão em um único passo; nesse caso o jogo desacelera momentaneamente em vez de pular. O inferior evita escalas próximas de zero em quadros duplicados.

A criação de canos usa setTimeout em milissegundos, então também independe da taxa de quadros. Uma limitação: a integração da gravidade é de Euler simples, e com passos de tamanhos diferentes a altura máxima de um pulo varia ligeiramente entre 60 e 120 Hz. Para um jogo de um toque, a diferença é pequena; para um jogo de plataforma com saltos exatos, não seria aceitável.

Super Pinguin GP: delta e suavização

O Super Pinguin GP trabalha em segundos e limita o intervalo a 50 ms:

function loop(ts) {
  const dt = Math.min(.05, (ts - (state.lastTs || ts)) / 1000);
  state.lastTs = ts;
  // ...
  update(dt); draw();
  requestAnimationFrame(loop);
}

Velocidades são expressas em unidades por segundo e multiplicadas por dt. Respostas que deveriam se aproximar de um alvo, como o ângulo do volante e a velocidade lateral, usam suavização exponencial:

state.wheelAngle = lerp(state.wheelAngle, steering * .62, 1 - Math.exp(-dt * 12));

A forma 1 - e-k·dt é o que torna a suavização correta para qualquer taxa: dois quadros de 8 ms produzem exatamente o mesmo resultado que um de 16 ms. Um lerp(a, b, 0.2) fixo por quadro, comum em tutoriais, converge duas vezes mais rápido a 120 Hz.

Dois cuidados complementam o laço. A simulação dos rivais subdivide qualquer intervalo maior que 50 ms em passos menores; isso importa sobretudo depois da chegada do jogador, quando a mesma IA é executada para calcular os tempos finais dos demais competidores. E o carimbo de tempo do último quadro é zerado depois da contagem regressiva e ao sair da pausa; sem isso, o primeiro quadro após a pausa receberia todo o tempo pausado.

Os testes automatizados do projeto rodam a mesma situação simulando 30 e 120 quadros por segundo e conferem que a direção responde de forma semelhante e que o prazo de eliminação por posição, de oito segundos, é cumprido igualmente nas duas taxas.

A revisão: passo fixo com acumulador

A solução mais rigorosa é o passo fixo: a simulação sempre avança em intervalos iguais, e cada quadro executa quantos passos couberem no tempo real decorrido. A revisão em andamento desde 22/10/2026 adotou esse modelo no Brick Breaker e no Cyber Bird, com passo de 1/60 s:

const TICK = 1000 / 60;
let lastTime = null, acc = 0;
function frame(now) {
  if (lastTime === null) lastTime = now;
  acc += Math.min(250, now - lastTime);   // ignora pausas longas
  lastTime = now;
  let steps = 0;
  while (acc >= TICK && steps < 6) { step(); acc -= TICK; steps++; }
  if (steps === 6) acc = 0;               // não tenta "alcançar" o atraso
  render();
  requestAnimationFrame(frame);
}

As vantagens aparecem nos detalhes. Todas as constantes já ajustadas em “por quadro a 60 Hz” continuam valendo sem conversão. Temporizadores contados em passos, como a duração de um poder, passam a durar o mesmo tempo em qualquer tela. O resultado fica determinístico: a mesma sequência de entradas produz o mesmo jogo em um celular de 60 Hz e em outro de 120 Hz, o que facilita reproduzir relatos de erro.

Os dois limites do laço são decisões de projeto. Somar no máximo 250 ms por quadro evita que a volta de uma pausa ou da troca de aplicativo dispare uma rajada de passos. Executar no máximo seis passos por quadro, e descartar o restante, evita a “espiral da morte”, em que um aparelho lento gasta tanto tempo simulando que atrasa ainda mais o quadro seguinte; nesse caso o jogo desacelera, em vez de travar.

No Brick Breaker, a revisão também resolve o problema de túnel: dentro de cada passo, o movimento da bola é dividido em subpassos menores que metade do raio, e a colisão com tijolos devolve a bola pelo eixo de menor penetração, o que impede que um único contato inverta a direção duas vezes.

const steps = Math.max(1, Math.ceil(speed / (ball.r * .5)));
for (let s = 0; s < steps; s++) {
  ball.x += ball.vx / steps;
  ball.y += ball.vy / steps;
  // colisões com paredes, raquete e tijolos a cada subpasso
}

O desenho, por enquanto, mostra o estado do último passo, sem interpolação entre passos. Em telas de 120 Hz isso significa que, em média, um quadro em cada dois repete a imagem anterior; para jogos com objetos lentos a diferença é pouco perceptível, mas é o próximo refinamento possível. O Super Pinguin GP continua com delta limitado e suavização exponencial, abordagem que se mostrou suficiente nos testes em duas taxas de quadros.

Como testar

Lista de verificação