A taxa de quadros está afetando a velocidade do objeto

9

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 delaycá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 FPSpara 150, o círculo renderizado percorre a velocidade muito rapidamente, enquanto define os FPS30 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.

Carpetfizz
fonte
4
Aqui está um bom artigo sobre loop do jogo: corrigir o seu timestep
Kostya Regent
2
Como uma observação lateral, geralmente tentamos colocar coisas que não devem ser executadas a cada loop fora dos loops. No seu código, sua 1000 / FPSdivisão pode ser feita e o resultado atribuído a uma variável antes do seu while(isRunning)loop. Isso ajuda a economizar algumas instruções da CPU para fazer algo mais de uma vez inutilmente.
Vaillancourt

Respostas:

21

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:

  1. 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.

  2. 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.

  3. 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 .)

Ilmari Karonen
fonte
11
Muito obrigado por responder a minha pergunta, eu realmente aprecio isso. Eu realmente gosto da abordagem do # 3, faz mais sentido para mim. Duas perguntas, qual é o updateInterval definido por e por que você o divide?
Carpetfizz
11
@Carpetfizz: 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ê definiria updateInterval = (1000 / 10) = 100.
Ilmari Karonen
11
currentTimeMillisnão é um relógio monotônico. Use em nanoTimevez disso, a menos que você queira que a sincronização do tempo da rede mexa com a velocidade das coisas no seu jogo.
user253751
@MooingDuck: Bem avistado. Eu consertei agora, eu acho. Obrigado!
Ilmari Karonen
@IlmariKaronen: Na verdade, olhando o código, pode ser mais simples while(lastTime+=updateInterval <= time). Isso é apenas um pensamento, não uma correção.
Mooing Duck
7

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 .

O objetivo do Delta Timing é eliminar os efeitos do atraso nos computadores que tentam manipular gráficos complexos ou muito código, adicionando velocidade aos objetos, para que eles se movam na mesma velocidade, independentemente do atraso.

Para fazer isso:

Isso é feito chamando um timer a cada quadro por segundo, que mantém o tempo entre agora e a última chamada em milissegundos.

Você precisaria então multiplicar o tempo delta pelo valor que deseja alterar pelo tempo. Por exemplo:

distanceTravelledSinceLastFrame = Speed * DeltaTime
Estático
fonte
3
Além disso, coloque limites nas deltatimes mínima e máxima. Se o computador hibernar e continuar, você não quer que as coisas sejam iniciadas fora da tela. Se um milagre aparecer e time()retornar o mesmo duas vezes, você não deseja erros div / 0 e processamento desperdiçado.
quer
@MooingDuck: Esse é um ponto muito bom. Eu editei minha própria resposta para refleti-la. (Geralmente, você não deve dividir nada pelo timestep em uma atualização típica do estado do jogo; portanto, um timestamp zero deve ser seguro, mas permitir que ele adicione uma fonte extra de erros em potencial para obter pouco ou nenhum ganho e, portanto, deve ser evitado.)
Ilmari Karonen 07/04
5

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.

Mario
fonte
11
ou seja, atualização (decorridos segundos);
Jon
2
E por dentro, posição + = velocidade * decorridosSegundos;
6134 Jon