Uma boa maneira de criar um loop de jogo no OpenGL

31

Atualmente, estou começando a aprender OpenGL na escola e comecei a fazer um jogo simples outro dia (por conta própria, não para a escola). Estou usando freeglut e construindo-o em C; portanto, para o loop do jogo, eu realmente estava usando uma função que fiz passar glutIdleFuncpara atualizar todo o desenho e a física de uma só vez. Isso foi bom para animações simples que eu não me importei muito com a taxa de quadros, mas como o jogo é baseado principalmente na física, eu realmente quero (preciso) amarrar a velocidade da atualização.

Portanto, minha primeira tentativa foi fazer minha função passar para glutIdleFunc( myIdle()) para acompanhar quanto tempo se passou desde a chamada anterior e atualizar a física (e atualmente os gráficos) a cada milissegundos. Eu costumava timeGetTime()fazer isso (usando <windows.h>). E isso me levou a pensar: usar a função ociosa é realmente uma boa maneira de fazer o loop do jogo?

Minha pergunta é: qual é a melhor maneira de implementar o loop do jogo no OpenGL? Devo evitar usar a função ociosa?

Jeff
fonte
gamedev.stackexchange.com/questions/651/… Duplicar?
The Duck Comunista
Sinto-me esta pergunta é mais específico para OpenGL e se utilizando GLUT para o loop é aconselhável
Jeff
1
Aqui , cara , não use GLUT para projetos maiores . É bom para os menores, no entanto.
bobobobo

Respostas:

29

A resposta simples é não, você não deseja usar o retorno de chamada glutIdleFunc em um jogo que tenha algum tipo de simulação. A razão para isso é que essa função separa a animação e desenha o código da manipulação de eventos da janela, mas não de forma assíncrona. Em outras palavras, o recebimento e a entrega de eventos da janela interrompem o código de desenho (ou o que você colocar nesse retorno de chamada), isso é perfeitamente adequado para um aplicativo interativo (interação e resposta), mas não para um jogo em que a física ou o estado do jogo deve progredir independente da interação ou render tempo.

Você deseja desacoplar completamente a manipulação de entrada, o estado do jogo e o código de desenho. Existe uma solução fácil e limpa para isso, que não envolve a biblioteca de gráficos diretamente (ou seja, é portátil e fácil de visualizar); você deseja que todo o ciclo do jogo produza tempo e faça com que a simulação consuma o tempo produzido (em pedaços). A chave, no entanto, é integrar a quantidade de tempo que sua simulação consumiu em sua animação.

A melhor explicação e tutorial que encontrei sobre isso é Fix Your Timestep, de Glenn Fiedler

Este tutorial possui o tratamento completo; no entanto, se você não tiver uma simulação física real , poderá pular a verdadeira integração, mas o loop básico ainda se resume a (no pseudo-código detalhado):

// The amount of time we want to simulate each step, in milliseconds
// (written as implicit frame-rate)
timeDelta = 1000/30
timeAccumulator = 0
while ( game should run )
{
  timeSimulatedThisIteration = 0
  startTime = currentTime()

  while ( timeAccumulator >= timeDelta )
  {
    stepGameState( timeDelta )
    timeAccumulator -= timeDelta
    timeSimulatedThisIteration += timeDelta
  }

  stepAnimation( timeSimulatedThisIteration )

  renderFrame() // OpenGL frame drawing code goes here

  handleUserInput()

  timeAccumulator += currentTime() - startTime 
}

Ao fazer isso dessa maneira, as paradas no código de renderização, no tratamento de entradas ou no sistema operacional não fazem com que o estado do jogo fique para trás. Este método também é portátil e independente da biblioteca de gráficos.

GLUT é uma boa biblioteca, no entanto, é estritamente orientada a eventos. Você registra retornos de chamada e dispara o loop principal. Você sempre entrega o controle do seu loop principal usando GLUT. Existem hacks para contornar isso , você também pode falsificar um loop externo usando temporizadores e outros, mas outra biblioteca é provavelmente o melhor (mais fácil) caminho a percorrer. Existem muitas alternativas, eis algumas (com boa documentação e tutoriais rápidos):

  • GLFW, que permite obter eventos de entrada em linha (em seu próprio loop principal).
  • SDL , no entanto, sua ênfase não é especificamente o OpenGL.
charstar
fonte
Bom artigo. Então, para fazer isso no OpenGL, eu não usaria o glutMainLoop, mas sim o meu? Se fizesse isso, seria capaz de usar o glutMouseFunc e outras funções? Espero que isso faz sentido, eu ainda sou muito novo para tudo isso
Jeff
Infelizmente não. Todos os retornos de chamada registrados no GLUT são disparados de dentro do glutMainLoop à medida que a fila de eventos é drenada e itera. Eu adicionei alguns links para alternativas no corpo da resposta.
charstar
1
+1 para abandonar o GLUT para qualquer outra coisa. Até onde eu sei, o GLUT nunca foi feito para nada além de aplicativos de teste.
Jari Komppa
Você ainda precisa lidar com eventos do sistema, não? Meu entendimento era que, usando glutIdleFunc, ele apenas lida com o loop GetMessage-TranslateMessage-DispatchMessage (no Windows) e chama sua função sempre que não houver nenhuma mensagem a ser recebida. Você faria isso de qualquer maneira ou sofreria a impaciência do sistema operacional ao sinalizar seu aplicativo como não responsivo, certo?
Ricket
@Ricket Tecnicamente, glutMainLoopEvent () lida com eventos recebidos chamando os retornos de chamada registrados. glutMainLoop () é efetivamente {glutMainLoopEvent (); IdleCallback (); } Uma consideração importante aqui é que, o redesenho também é um retorno de chamada tratado de dentro do loop de eventos. Você pode fazer uma comportam biblioteca orientada a eventos como um um-driven tempo, mas Martelo de Maslow e tudo o que ...
charstar
4

Eu acho que glutIdleFuncestá bem; é essencialmente o que você acabaria fazendo manualmente de qualquer maneira, se não o usasse. Não importa o que você terá um loop restrito que não é chamado em um padrão fixo, e você precisa fazer a escolha se deseja convertê-lo em um loop de tempo fixo ou mantê-lo em um loop de tempo variável e fazer Certifique-se de todas as suas contas de matemática para interpolar o tempo. Ou mesmo uma mistura deles, de alguma forma.

Certamente, você pode ter uma myIdlefunção para a qual passa e glutIdleFunc, dentro disso, mede o tempo que passou, dividi-lo pelo seu timestap fixo e chama outra função várias vezes.

Aqui está o que não fazer:

void myFunc() {
    // (calculate timeDifference here)
    int numOfTimes = timeDifference / 10;
    for(int i=0; i<numOfTimes; i++) {
        fixedTimestep();
    }
}

FixedTimestep não seria chamado a cada 10 milissegundos. Na verdade, seria mais lento que o tempo real, e você pode acabar enganando os números para compensar quando realmente a raiz do problema está na perda do restante quando você divide timeDifferencepor 10.

Em vez disso, faça algo como isto:

long queuedMilliseconds; // static or whatever, this must persist outside of your loop

void myFunc() {
    // (calculate timeDifference here)
    queuedMilliseconds += timeDifference;
    while(queuedMilliseconds >= 10) {
        fixedTimestep();
        queuedMilliseconds -= 10;
    }
}

Agora, essa estrutura de loop garante que sua fixedTimestepfunção seja chamada uma vez a cada 10 milissegundos, sem perder nenhum milissegundo como restante ou algo assim.

Devido à natureza multitarefa dos sistemas operacionais, você não pode confiar em uma função chamada exatamente a cada 10 milissegundos; mas o loop acima aconteceria rápido o suficiente para que, esperançosamente, estivesse próximo o suficiente, e você pode assumir que cada chamada fixedTimestepincrementaria sua simulação em 10 milissegundos.

Leia também esta resposta e especialmente os links fornecidos na resposta.

Ricket
fonte