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?



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.

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.

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.