Você está movendo o círculo em um pixel por quadro. Não deve ser uma grande surpresa que, se seu loop de renderização for executado a 30 FPS, seu círculo se moverá 30 a pixels por segundo.
Basta escolher uma taxa de quadros e cumpri-la. Foi o que muitos jogos antigos fizeram - eles rodavam a uma taxa fixa de 50 ou 60 FPS, geralmente sincronizados com a taxa de atualização da tela, e apenas projetavam a lógica do jogo para fazer tudo o que era necessário dentro desse intervalo de tempo fixo. Se, por alguma razão, isso não acontecesse, o jogo teria que pular um quadro (ou possivelmente travar), reduzindo efetivamente a física do desenho e do jogo a meia velocidade.
Em particular, jogos que usavam recursos como detecção de colisão de sprites de hardware praticamente tinham que funcionar assim, porque sua lógica de jogo estava inextricavelmente ligada à renderização, que era feita em hardware a uma taxa fixa.
Use um timestep variável para a física do jogo. Basicamente, isso significa reescrever o loop do jogo para algo parecido com isto:
long lastTime = System.currentTimeMillis();
while (isRunning) {
long time = System.currentTimeMillis();
float timestep = 0.001 * (time - lastTime); // in seconds
if (timestep <= 0 || timestep > 1.0) {
timestep = 0.001; // avoid absurd time steps
}
update(timestep);
draw();
// ... sleep until next frame ...
lastTime = time;
}
e, por dentro update()
, ajustando as fórmulas físicas para dar conta da variável timestep, por exemplo:
speed += timestep * acceleration;
position += timestep * (speed - 0.5 * timestep * acceleration);
Um problema com esse método é que pode ser complicado manter a física (principalmente) independente do passo temporal ; você realmente não quer que a distância entre os jogadores dependa da taxa de quadros. A fórmula que eu mostrei acima funciona muito bem para aceleração constante, por exemplo, sob gravidade (e a do post vinculado funciona muito bem, mesmo que a aceleração varie com o tempo), mas mesmo com as fórmulas físicas mais perfeitas possíveis, é provável que trabalhar com flutuadores produz um pouco de "ruído numérico" que, em particular, pode impossibilitar repetições exatas. Se isso é algo que você acha que pode querer, pode preferir os outros métodos.
Desacoplar a atualização e desenhar etapas. Aqui, a idéia é que você atualize o estado do jogo usando um timestap fixo, mas execute um número variável de atualizações entre cada quadro. Ou seja, seu loop de jogo pode ser algo como isto:
long lastTime = System.currentTimeMillis();
while (isRunning) {
long time = System.currentTimeMillis();
if (time - lastTime > 1000) {
lastTime = time; // we're too far behind, catch up
}
int updatesNeeded = (time - lastTime) / updateInterval;
for (int i = 0; i < updatesNeeded; i++) {
update();
lastTime += updateInterval;
}
draw();
// ... sleep until next frame ...
}
Para tornar o movimento percebido mais suave, você também pode desejar que seu draw()
método interpole coisas como posições de objetos sem problemas entre os estados do jogo anterior e do próximo. Isso significa que você precisa passar o deslocamento de interpolação correto para o draw()
método, por exemplo:
int remainder = (time - lastTime) % updateInterval;
draw( (float)remainder / updateInterval ); // scale to 0.0 - 1.0
Você também precisaria que seu update()
método calculasse o estado do jogo um passo à frente (ou possivelmente vários, se você desejar fazer uma interpolação de splines de ordem superior) e salve as posições anteriores dos objetos antes de atualizá-las, para que o draw()
método possa interpolar entre eles. (Também é possível extrapolar as posições previstas com base nas velocidades e acelerações dos objetos, mas isso pode parecer instável, especialmente se os objetos se moverem de maneiras complicadas, causando falhas nas previsões.)
Uma vantagem da interpolação é que, para alguns tipos de jogos, isso permite reduzir significativamente a taxa de atualização da lógica do jogo, mantendo uma ilusão de movimento suave. Por exemplo, você pode atualizar o estado do jogo apenas, digamos, 5 vezes por segundo, enquanto ainda desenha de 30 a 60 quadros interpolados por segundo. Ao fazer isso, você também pode considerar intercalar sua lógica do jogo com o desenho (por exemplo, ter um parâmetro no seu update()
método que diga para executar apenas x % de uma atualização completa antes de retornar) e / ou executar a física do jogo / lógica e o código de renderização em threads separados (cuidado com falhas de sincronização!).
Obviamente, também é possível combinar esses métodos de várias maneiras. Por exemplo, em um jogo multiplayer cliente-servidor, você pode fazer com que o servidor (que não precisa desenhar nada) execute suas atualizações em um intervalo de tempo fixo (para uma física consistente e uma repetibilidade exata), enquanto o cliente faz atualizações preditivas (para ser substituído pelo servidor, em caso de desacordo) em um intervalo de tempo variável para obter melhor desempenho. Também é possível misturar utilidades de interpolação e atualizações de tempo variável; por exemplo, no cenário cliente-servidor descrito, não há muito sentido em fazer com que o cliente use intervalos de tempo de atualização mais curtos que o servidor, para que você possa definir um limite mais baixo no intervalo de tempo do cliente e interpolar no estágio de desenho para permitir maior FPS.
(Edit: Código adicionado para evitar intervalos / contagens absurdas de atualização, caso, digamos, o computador seja temporariamente suspenso ou congelado por mais de um segundo enquanto o loop do jogo estiver em execução. Agradecimentos ao Mooing Duck por me lembrar sobre a necessidade disso .)