Programação de jogos
Física independente da taxa de quadros: do passo por quadro ao passo fixo
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
- No Chrome para computador, as ferramentas de desenvolvedor permitem limitar a CPU; isso simula quedas de taxa.
- Em código, torne o laço chamável com um
dtarbitrário e escreva testes que simulam o mesmo intervalo de tempo com quadros de tamanhos diferentes. É o método usado no Super Pinguin GP. - Em aparelho, compare um celular de 60 Hz com um de 120 Hz cronometrando um evento fixo, como a queda de um poder do topo à base da tela.
Lista de verificação
- Toda velocidade e aceleração tem unidade de tempo explícita (por segundo ou por quadro de referência).
- Temporizadores de jogo contam tempo, não quadros.
- O intervalo é limitado, e o limite foi escolhido conscientemente.
- Suavizações usam
1 - exp(-k·dt), não um fator fixo por quadro. - O relógio é reiniciado após pausas e troca de aba.
- Existe pelo menos um teste que compara duas taxas de quadros.