Estou experimentando construir um mecanismo de jogo do zero em Java e tenho algumas perguntas. Meu loop principal do jogo é assim:
int FPS = 60;
while(isRunning){
/* Current time, before frame update */
long time = System.currentTimeMillis();
update();
draw();
/* How long each frame should last - time it took for one frame */
long delay = (1000 / FPS) - (System.currentTimeMillis() - time);
if(delay > 0){
try{
Thread.sleep(delay);
}catch(Exception e){};
}
}
Como você pode ver, defini a taxa de quadros em 60FPS, usada no delay
cálculo. O atraso garante que cada quadro leve a mesma quantidade de tempo antes de renderizar o próximo. Na minha update()
função, faço o x++
que aumenta o valor horizontal de um objeto gráfico que eu desenho com o seguinte:
bbg.drawOval(x,40,20,20);
O que me confunde é a velocidade. quando defino FPS
para 150, o círculo renderizado percorre a velocidade muito rapidamente, enquanto define os FPS
30 movimentos na tela na metade da velocidade. A taxa de quadros não afeta apenas a "suavidade" da renderização e não a velocidade dos objetos que estão sendo renderizados? Acho que estou perdendo uma grande parte, eu adoraria alguns esclarecimentos.
fonte
1000 / FPS
divisão pode ser feita e o resultado atribuído a uma variável antes do seuwhile(isRunning)
loop. Isso ajuda a economizar algumas instruções da CPU para fazer algo mais de uma vez inutilmente.Respostas:
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.
Você basicamente tem três maneiras possíveis de lidar com esse problema:
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:
e, por dentro
update()
, ajustando as fórmulas físicas para dar conta da variável timestep, por exemplo: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:
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 odraw()
método, por exemplo: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 odraw()
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 .)
fonte
updateInterval
é apenas o número de milissegundos que você deseja entre as atualizações de estado do jogo. Por exemplo, 10 atualizações por segundo, você definiriaupdateInterval = (1000 / 10) = 100
.currentTimeMillis
não é um relógio monotônico. Use emnanoTime
vez disso, a menos que você queira que a sincronização do tempo da rede mexa com a velocidade das coisas no seu jogo.while(lastTime+=updateInterval <= time)
. Isso é apenas um pensamento, não uma correção.No momento, seu código está sendo executado sempre que um quadro é renderizado. Se a taxa de quadros for maior ou menor que a taxa de quadros especificada, seus resultados serão alterados, pois as atualizações não terão o mesmo tempo.
Para resolver isso, consulte o Delta Timing .
Para fazer isso:
Você precisaria então multiplicar o tempo delta pelo valor que deseja alterar pelo tempo. Por exemplo:
fonte
time()
retornar o mesmo duas vezes, você não deseja erros div / 0 e processamento desperdiçado.Isso ocorre porque você limita sua taxa de quadros, mas faz apenas uma atualização por quadro. Então, vamos supor que o jogo seja executado a 60 fps, você recebe 60 atualizações lógicas por segundo. Se a taxa de quadros cair para 15 qps, você terá apenas 15 atualizações lógicas por segundo.
Em vez disso, tente acumular o tempo de quadro passado até agora e atualize sua lógica do jogo uma vez para cada período de tempo que passou, por exemplo, para executar sua lógica a 100 fps, você executaria a atualização uma vez a cada 10 ms acumulados (e subtrairá os do contador).
Adicione uma alternativa (melhor para recursos visuais) atualize sua lógica com base no tempo passado.
fonte