EXTREMAMENTE Confuso sobre o ciclo de jogo "Constant Game Speed ​​Maximum FPS"

12

Li recentemente este artigo no Game Loops: http://www.koonsolo.com/news/dewitters-gameloop/

E a última implementação recomendada está me confundindo profundamente. Eu não entendo como isso funciona, e parece uma bagunça completa.

Entendo o princípio: atualize o jogo a uma velocidade constante, com o que restar renderize o jogo quantas vezes for possível.

Presumo que você não pode usar um:

  • Obtenha entrada para 25 ticks
  • Render jogo para 975 ticks

Abordagem, já que você receberia informações para a primeira parte da segunda e isso pareceria estranho? Ou é isso que está acontecendo no artigo?


Essencialmente:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

Como isso é válido?

Vamos assumir seus valores.

MAX_FRAMESKIP = 5

Vamos supor next_game_tick, que foi designado momentos após a inicialização, antes que o loop principal do jogo seja ... 500.

Finalmente, como estou usando SDL e OpenGL para o meu jogo, com o OpenGL sendo usado apenas para renderização, vamos assumir que GetTickCount()retorna o tempo desde que SDL_Init foi chamado, o que ocorre.

SDL_GetTicks -- Get the number of milliseconds since the SDL library initialization.

Fonte: http://www.libsdl.org/docs/html/sdlgetticks.html

O autor também assume isso:

DWORD next_game_tick = GetTickCount();
// GetTickCount() returns the current number of milliseconds
// that have elapsed since the system was started

Se expandirmos a whiledeclaração, obtemos:

while( ( 750 > 500 ) && ( 0 < 5 ) )

750 porque o tempo passou desde que next_game_tickfoi atribuído. loopsé zero, como você pode ver no artigo.

Então entramos no loop while, vamos fazer alguma lógica e aceitar alguma entrada.

Yadayadayada.

No final do loop while, que eu lembro que você está dentro do loop principal do jogo, é:

next_game_tick += SKIP_TICKS;
loops++;

Vamos atualizar a aparência da próxima iteração do código while

while( ( 1000 > 540 ) && ( 1 < 5 ) )

1000 porque o tempo passou para obter entrada e fazer coisas antes de atingirmos a próxima ineteração do loop, em que GetTickCount () é chamado.

540 porque, no código 1000/25 = 40, portanto, 500 + 40 = 540

1 porque nosso loop iterou uma vez

5 , você sabe o porquê.


Então, como esse loop While é claramente dependente MAX_FRAMESKIPe não o pretendido, TICKS_PER_SECOND = 25;como o jogo deve funcionar corretamente?

Não foi surpresa para mim que, quando eu implementei isso no meu código, eu adicionasse corretamente, simplesmente renomeei minhas funções para manipular a entrada do usuário e desenhar o jogo com o que o autor do artigo possui em seu código de exemplo, o jogo não fez nada .

Coloquei um fprintf( stderr, "Test\n" );loop while que não é impresso até o jogo terminar.

Como esse loop do jogo é executado 25 vezes por segundo, garantido, enquanto é renderizado o mais rápido possível?

Para mim, a menos que esteja faltando algo ENORME, parece ... nada.

E essa estrutura, desse loop while, supostamente funciona 25 vezes por segundo e atualiza o jogo exatamente o que eu mencionei anteriormente no início do artigo?

Se for esse o caso, por que não podemos fazer algo simples como:

while( loops < 25 )
{
    getInput();
    performLogic();

    loops++;
}

drawGame();

E conte para interpolação de alguma outra maneira.

Perdoe minha pergunta extremamente longa, mas este artigo fez mais mal do que bem para mim. Estou severamente confuso agora - e não tenho idéia de como implementar um loop de jogo adequado por causa de todas essas perguntas que surgiram.

tsujp
fonte
1
Os discursos são mais direcionados ao autor do artigo. Qual parte é a sua pergunta objetiva ?
Anko
3
Esse loop do jogo é válido mesmo, alguém explica. Dos meus testes, ele não tem a estrutura correta para executar 25 vezes por segundo. Explique ao meu por que isso acontece. Além disso, isso não é um discurso retórico, é uma série de perguntas. Devo usar emoticons, pareço zangado?
18713 tsujp
2
Como sua pergunta se resume a "O que eu não estou entendendo sobre esse loop de jogo" e você tem muitas palavras em negrito, ela aparece pelo menos exasperada.
Kirbinator
@ Kirbinator Eu posso entender isso, mas eu estava tentando fundamentar tudo o que acho incomum neste artigo, por isso não é uma pergunta superficial e vazia. Eu não acho que seja por excelência que, de qualquer maneira, gostaria de pensar que tenho alguns pontos válidos - afinal, é um artigo tentando ensinar, mas não fazendo um trabalho tão bom.
tsujp
7
Não me interpretem mal, é uma boa pergunta, poderia ser 80% menor.
22613 Kirbinator

Respostas:

8

Eu acho que o autor cometeu um pequeno erro:

while( GetTickCount() > next_game_tick && loops < MAX_FRAMESKIP)

deveria estar

while( GetTickCount() < next_game_tick && loops < MAX_FRAMESKIP)

Ou seja: contanto que ainda não seja hora de desenhar o próximo quadro e, embora não tenhamos pulado tantos quadros quanto MAX_FRAMESKIP, devemos esperar.

Eu também não entendo por que ele atualiza next_game_tick no loop e presumo que seja outro erro. Como no início de um quadro, você pode determinar quando o próximo quadro deve ser (ao usar uma taxa de quadros fixa). A next game ticknão depende de quanto tempo temos que sobraram após a actualização e renderização.

O autor também comete outro erro comum

com o que resta, renderize o jogo quantas vezes for possível.

Isso significa renderizar o mesmo quadro várias vezes. O autor está ciente disso:

O jogo será atualizado regularmente 50 vezes por segundo, e a renderização é feita o mais rápido possível. Observe que, quando a renderização for feita mais de 50 vezes por segundo, alguns quadros subsequentes serão os mesmos; portanto, os quadros visuais reais serão exibidos com no máximo 50 quadros por segundo.

Isso apenas faz com que a GPU faça um trabalho desnecessário e, se a renderização demorar mais do que o esperado, poderá fazer com que você comece a trabalhar no seu próximo quadro mais tarde do que o pretendido, para que seja melhor render ao sistema operacional e aguardar.

Roy T.
fonte
+ Votar. Mmmm. Isso realmente me deixa confuso sobre o que fazer então. Agradeço a sua compreensão. Provavelmente terei uma brincadeira, o problema é realmente o conhecimento de como limitar o FPS ou definir o FPS dinamicamente e como fazer isso. Na minha opinião, o manuseio fixo de entradas é necessário para que o jogo corra no mesmo ritmo para todos. Este é apenas um simples 2D Platformer MMO (a muito longo prazo)
tsujp
Enquanto o loop parece correto e o incremento de next_game_tick existe para isso. Ele existe para manter a simulação em velocidade constante em hardware mais lento e rápido. A execução do código de renderização "o mais rápido possível" (ou mais rápido do que a física) só faz sentido quando houver interpolação no código de renderização para torná-lo mais suave para objetos rápidos etc. (final do artigo), mas isso é apenas desperdício de energia se está renderizando mais do que aquilo que qualquer dispositivo de saída (tela) é capaz de mostrar.
Dotti 18/02
Portanto, o código dele é uma implementação válida, agora é isso. E nós apenas temos que viver com esse desperdício? Ou existem métodos que eu possa procurar por isso.
tsujp
Ceder ao sistema operacional e aguardar é apenas uma boa idéia, se você tiver uma boa idéia de quando o sistema operacional retornará o controle para você - o que pode não acontecer assim que você desejar.
Kylotan
Não posso votar nesta resposta. Ele está completamente ausente do material de interpolação, o que invalida a segunda metade da resposta.
AlbeyAmakiir
4

Talvez seja melhor simplificar um pouco:

while( game_is_running ) {

    current = GetTickCount();
    while(current > next_game_tick) {
        update_game();

        next_game_tick += SKIP_TICKS;
    }
    display_game();
}

whileO loop dentro do mainloop é usado para executar etapas de simulação de onde quer que estivesse, para onde deveria estar agora. update_game()A função deve sempre assumir que apenasSKIP_TICKS quantidade de tempo passou desde a última chamada. Isso manterá a física do jogo rodando em velocidade constante em hardware lento e rápido.

Incrementar next_game_tickpela quantidade de SKIP_TICKSmovimentos aproxima-o da hora atual. Quando isso fica maior que o tempo atual, ele quebra ( current > next_game_tick) e o mainloop continua a renderizar o quadro atual.

Após a renderização, a próxima chamada para GetTickCount()retornará um novo horário atual. Se esse tempo for maior quenext_game_tick que significa que já estamos atrás das etapas 1-N na simulação e devemos nos atualizar, executando cada etapa na simulação na mesma velocidade constante. Nesse caso, se for menor, apenas renderizará o mesmo quadro novamente (a menos que haja interpolação).

O código original limitou o número de loops se ficássemos muito distantes ( MAX_FRAMESKIP). Isso faz com que ele realmente mostre algo e não pareça estar bloqueado se, por exemplo, retomar a suspensão ou o jogo ficar em pausa no depurador por um longo tempo (supondo GetTickCount()que não pare durante esse tempo) até que ele esteja atualizado.

Para se livrar da renderização inútil do mesmo quadro, se você não estiver usando interpolação interna display_game(), você pode envolvê-la dentro da instrução if, como:

while (game_is_running) {
    current = GetTickCount();
    if (current > next_game_tick) {
        while(current > next_game_tick) {
            update_game();

            next_game_tick += SKIP_TICKS;
        }
    display_game();
    }
    else {
    // could even sleep here
    }
}

Este também é um bom artigo sobre isso: http://gafferongames.com/game-physics/fix-your-timestep/

Além disso, talvez o motivo pelo qual suas fprintfsaídas no final do jogo seja o fato de não ter sido liberado.

Desculpe meu ingles.

Dotti
fonte
4

Seu código parece totalmente válido.

Considere o whileloop do último conjunto:

// JS / pseudocode
var current_time = function () { return Date.now(); }, // in ms
    framerate = 1000/30, // 30fps
    next_frame = current_time(),

    max_updates_per_draw = 5,

    iterations;

while (game_running) {

    iterations = 0;

    while (current_time() > next_frame && iterations < max_updates_per_draw) {
        update_game(); // input, physics, audio, etc

        next_frame += framerate;
        iterations += 1;
    }

    draw();
}

Eu tenho um sistema em funcionamento que diz "enquanto o jogo está rodando, verifique a hora atual - se é maior do que a contagem de quadros em execução e pulamos o desenho com menos de 5 quadros, então pule o desenho e apenas atualize a entrada e física: desenhe a cena e inicie a próxima iteração de atualização "

Quando cada atualização acontece, você aumenta o tempo de "next_frame" pela taxa de quadros ideal. Então você verifica seu tempo novamente. Se o seu horário atual agora é menor do que quando o next_frame deve ser atualizado, pule a atualização e desenhe o que você tem.

Se o seu current_time for maior (imagine que o último processo de desenho demorou muito tempo, porque houve algum soluço em algum lugar ou um monte de coleta de lixo em uma linguagem gerenciada ou uma implementação de memória gerenciada em C ++ ou o que for), então o draw é ignorado e o next_frameatualizado com outro quadro extra, até que as atualizações ao ponto em que deveríamos estar no relógio ou saltamos para desenhar quadros suficientes que DEVEMOS absolutamente desenhar um, para que o jogador possa ver o que eles estão fazendo

Se sua máquina é super rápida ou seu jogo é super simples, current_timepode ser com menos next_framefrequência, o que significa que você não está atualizando durante esses pontos.

É aí que entra a interpolação. Da mesma forma, você pode ter um bool separado, declarado fora dos loops, para declarar espaço "sujo".

Dentro do loop de atualização, você definiria dirty = true, significando que realmente realizou uma atualização.

Então, em vez de apenas ligar draw(), você diria:

if (is_dirty) {
    draw(); 
    is_dirty = false;
}

Então você está perdendo qualquer interpolação para um movimento suave, mas garante que só atualize quando houver atualizações realmente acontecendo (em vez de interpolações entre estados).

Se você é corajoso, há um artigo chamado "Corrija seu Timestep!" por GafferOnGames.
Ele aborda o problema de maneira um pouco diferente, mas considero uma solução mais bonita que faz basicamente a mesma coisa (dependendo dos recursos do seu idioma e do quanto você se importa com os cálculos de física do seu jogo).

Norguard
fonte