Existem dezenas de artigos, livros e discussões sobre loops de jogos. No entanto, muitas vezes me deparo com algo assim:
while(running)
{
processInput();
while(isTimeForUpdate)
{
update();
}
render();
}
O que basicamente está me incomodando nessa abordagem é a renderização "independente de atualização", por exemplo, renderiza um quadro quando não há nenhuma alteração. Então, minha pergunta é por que essa abordagem é frequentemente ensinada?
while (isTimeForUpdate)
, nãoif (isTimeForUpdate)
. O objetivo principal não érender()
quando não há umupdate()
, masupdate()
repetidamente entrerender()
s. Independentemente, ambas as situações têm usos válidos. O primeiro seria válido se o estado pudesse mudar fora da suaupdate
função, por exemplo, alterar o que é renderizado com base no estado implícito, como o horário atual. O último é válido porque oferece ao seu mecanismo de física a possibilidade de fazer muitas atualizações pequenas e precisas, o que reduz a chance de "distorcer" os obstáculos.Respostas:
Há uma longa história de como chegamos a esta convenção comum, com muitos desafios fascinantes ao longo do caminho, então tentarei motivá-la em etapas:
1. Problema: os dispositivos funcionam em velocidades diferentes
Você já tentou jogar um jogo DOS antigo em um PC moderno, e ele é executado de maneira incrivelmente rápida - apenas um borrão?
Muitos jogos antigos tinham um loop de atualização muito ingênuo - eles coletavam entradas, atualizavam o estado do jogo e renderizavam o mais rápido que o hardware permitia, sem levar em conta quanto tempo havia passado. O que significa que, assim que o hardware muda, a jogabilidade muda.
Geralmente, queremos que nossos jogadores tenham uma experiência consistente e uma sensação de jogo em vários dispositivos (desde que atendam a algumas especificações mínimas), estejam usando o telefone do ano passado ou o modelo mais novo, um desktop de jogo de última geração ou um laptop de nível intermediário.
Em particular, para jogos competitivos (para vários jogadores ou através de tabelas de classificação), não queremos que jogadores rodando em um dispositivo específico tenham vantagem sobre os outros, porque podem correr mais rápido ou ter mais tempo para reagir.
A solução infalível aqui é bloquear a taxa na qual fazemos atualizações de estado de jogo. Dessa forma, podemos garantir que os resultados serão sempre os mesmos.
2. Então, por que não bloquear a taxa de quadros (por exemplo, usando o VSync) e ainda executar as atualizações e a renderização do estado da jogabilidade no lockstep?
Isso pode funcionar, mas nem sempre é agradável ao público. Houve muito tempo em que correr a 30 fps sólidos era considerado o padrão-ouro para jogos. Agora, os jogadores esperam rotineiramente 60 fps como a barra mínima, especialmente em jogos de ação multiplayer, e alguns títulos mais antigos agora parecem visivelmente agitados conforme nossas expectativas mudam. Há também um grupo vocal de jogadores de PC, em particular, que se opõem a framerate bloqueios. Eles pagaram muito pelo seu hardware de ponta e querem poder usar esse músculo computacional para obter a renderização mais suave e com maior fidelidade possível.
Em VR, em particular, a taxa de quadros é o rei, e o padrão continua subindo. No início do recente ressurgimento da RV, os jogos costumavam rodar em torno de 60 fps. Agora 90 é mais padrão, e harware como o PSVR está começando a suportar 120. Isso pode continuar a aumentar ainda. Portanto, se um jogo de VR limitar sua taxa de quadros ao que é possível e aceito hoje, é provável que seja deixado para trás à medida que o hardware e as expectativas se desenvolverem ainda mais.
(Como regra geral, tenha cuidado ao saber que "os jogadores não conseguem perceber nada mais rápido que XXX", pois geralmente é baseado em um tipo específico de "percepção", como reconhecer um quadro em sequência. A percepção da continuidade do movimento geralmente é muito mais sensível.)
A última questão aqui é que um jogo usando uma taxa de quadros bloqueada também precisa ser conservador - se você chegar a um momento no jogo em que está atualizando e exibindo um número incomumente alto de objetos, não quer perder seu quadro prazo e causar uma gagueira perceptível ou um engate. Portanto, você precisa definir seu orçamento de conteúdo baixo o suficiente para deixar espaço livre ou investir em recursos de ajuste dinâmico de qualidade mais complicados para evitar atrelar toda a experiência de jogo ao pior desempenho possível em hardware com especificações mínimas.
Isso pode ser especialmente problemático se os problemas de desempenho aparecerem mais tarde no desenvolvimento, quando todos os seus sistemas existentes forem construídos e ajustados, assumindo uma taxa de quadros de renderização em etapas que agora você nem sempre consegue. A dissociação das taxas de atualização e renderização oferece mais flexibilidade para lidar com a variabilidade de desempenho.
3. A atualização em um intervalo de tempo fixo não tem os mesmos problemas que (2)?
Eu acho que essa é a essência da pergunta original: se dissociarmos nossas atualizações e, às vezes, renderizarmos dois quadros sem atualizações de estado do jogo, então não será o mesmo que renderização de passo a passo em uma taxa de quadros mais baixa, já que não há alterações visíveis a tela?
Na verdade, existem várias maneiras diferentes de os jogos usarem o desacoplamento dessas atualizações:
a) A taxa de atualização pode ser mais rápida que a taxa de quadros renderizada
Como tyjkenn observa em outra resposta, a física em particular é frequentemente aumentada com uma frequência maior do que a renderização, o que ajuda a minimizar erros de integração e fornece colisões mais precisas. Portanto, em vez de ter 0 ou 1 atualizações entre os quadros renderizados, você pode ter 5, 10 ou 50.
Agora, o jogador que renderiza a 120 fps pode obter 2 atualizações por quadro, enquanto o jogador com renderização de hardware de especificação mais baixa a 30 fps recebe 8 atualizações por quadro, e ambos os jogos rodam na mesma velocidade de jogo-ticks-per-realtime-second speed. O melhor hardware faz com que pareça mais suave, mas não altera radicalmente o funcionamento da jogabilidade.
Há um risco aqui de que, se a taxa de atualização for incompatível com a taxa de quadros, você poderá obter uma "frequência de batida" entre os dois . Por exemplo. na maioria dos quadros, temos tempo suficiente para 4 atualizações de estado do jogo e um pouco de sobra; então, de vez em quando, economizamos o suficiente para fazer 5 atualizações em um quadro, dando um pequeno salto ou gaguejando no movimento. Isso pode ser resolvido por ...
b) Interpolar (ou extrapolar) o estado do jogo entre as atualizações
Aqui, geralmente deixamos o estado do jogo viver um timestamp fixo no futuro e armazenamos informações suficientes dos 2 estados mais recentes para que possamos render um ponto arbitrário entre eles. Então, quando estivermos prontos para mostrar um novo quadro na tela, combinamos com o momento apropriado apenas para fins de exibição (por exemplo, não modificamos o estado de jogo subjacente aqui)
Quando bem feito, o movimento fica suave e até ajuda a mascarar alguma flutuação na taxa de quadros, desde que não caiamos muito .
c) Adicionando suavidade às alterações de estado que não são de jogo
Mesmo sem interpolar o estado da jogabilidade, ainda podemos obter vitórias de suavidade.
Alterações puramente visuais, como animação de personagens, sistemas de partículas ou efeitos visuais e elementos da interface do usuário como o HUD, geralmente são atualizados separadamente do tempo fixo do estado da jogabilidade. Isso significa que, se estamos marcando nosso estado de jogo várias vezes por quadro, não estamos pagando o custo com cada tick - apenas no passe de renderização final. Em vez disso, escalamos a velocidade de reprodução desses efeitos para coincidir com o comprimento do quadro, para que sejam reproduzidos da maneira mais suave que a taxa de quadros de renderização permitir, sem afetar a velocidade ou a equidade do jogo, conforme discutido em (1).
O movimento da câmera também pode fazer isso - especialmente em VR, às vezes, mostramos o mesmo quadro mais de uma vez, mas o reprojetamos para levar em conta o movimento da cabeça do jogador no meio , para que possamos melhorar a latência e o conforto percebidos, mesmo que possamos nativamente processa tudo tão rápido. Alguns sistemas de streaming de jogos (nos quais o jogo está rodando em um servidor e o jogador executa apenas um thin client) também usam uma versão disso.
4. Por que não usar esse estilo (c) para tudo? Se funcionar para animação e interface do usuário, não podemos simplesmente escalar nossas atualizações de estado de jogo para corresponder à taxa de quadros atual?
Sim * isso é possível, mas não, não é simples.
Essa resposta já é um pouco longa, então não vou entrar em todos os detalhes sangrentos, apenas um resumo rápido:
Multiplicar por
deltaTime
trabalhos para ajustar as atualizações de comprimento variável para alterações lineares (por exemplo, movimento com velocidade constante, contagem regressiva de um temporizador ou progresso ao longo de uma linha do tempo de animação)Infelizmente, muitos aspectos dos jogos não são lineares . Mesmo algo tão simples quanto a gravidade exige técnicas de integração mais sofisticadas ou sub-passos de alta resolução para evitar resultados divergentes em diferentes taxas de quadros. A entrada e o controle do player são, por si só, uma enorme fonte de não linearidade.
Em particular, os resultados da detecção e resolução de colisões discretas dependem da taxa de atualização, levando a erros de tunelamento e tremulação se os quadros ficarem muito longos. Portanto, uma taxa de quadros variável nos obriga a usar métodos de detecção de colisão contínua mais complexos / caros em mais de nosso conteúdo ou tolerar a variabilidade em nossa física. Até a detecção contínua de colisões enfrenta desafios quando objetos se movem em arcos, exigindo intervalos de tempo mais curtos ...
Portanto, no caso geral de um jogo de média complexidade, manter um comportamento consistente e equitativo por meio de
deltaTime
dimensionamento é algo entre muito difícil e intensivo em manutenção, até totalmente inviável.A padronização de uma taxa de atualização permite garantir um comportamento mais consistente em várias condições , geralmente com código mais simples.
Manter essa taxa de atualização separada da renderização nos dá flexibilidade para controlar a suavidade e o desempenho da experiência sem alterar a lógica do jogo .
Mesmo assim , nunca obtemos uma independência de quadros verdadeiramente "perfeita", mas, como muitas abordagens nos jogos, nos fornece um método controlável para discarmos em direção ao "suficientemente bom" para as necessidades de um determinado jogo. É por isso que é comumente ensinado como um ponto de partida útil.
fonte
read-update-render
falso, nossa pior latência é de 17ms (ignorando o pipeline de gráficos e a latência de exibição por enquanto). Com um(read-update)x(n>1)-render
loop desacoplado na mesma taxa de quadros, nossa latência de pior caso só pode ser a mesma ou melhor, porque verificamos e agimos na entrada com tanta frequência ou mais. :)As outras respostas são boas e falam sobre por que o loop do jogo existe e deve ser separado do loop de renderização. No entanto, como no exemplo específico de "Por que renderizar um quadro quando não houve alterações?" Realmente se resume apenas a hardware e complexidade.
As placas de vídeo são máquinas de estado e são realmente boas em fazer a mesma coisa repetidamente. Se você renderizar apenas coisas que mudaram, na verdade é mais caro, não menos. Na maioria dos cenários, não há muita coisa estática; se você mover um pouco para a esquerda em um jogo de FPS, alterou os dados de pixel de 98% das coisas na tela; é possível renderizar todo o quadro.
Mas principalmente, complexidade. Manter o controle de tudo o que foi alterado durante a atualização é muito mais caro, pois é necessário refazer tudo ou acompanhar o resultado antigo de algum algoritmo, compará-lo com o novo resultado e renderizar esse pixel apenas se a alteração for diferente. Depende do sistema.
O design do hardware etc. é amplamente otimizado para as convenções atuais, e uma máquina de estado tem sido um bom modelo para começar.
fonte
A renderização geralmente é o processo mais lento no ciclo do jogo. Os seres humanos não percebem facilmente uma diferença na taxa de quadros acima de 60, por isso é menos importante perder tempo processando mais rápido do que isso. No entanto, existem outros processos que se beneficiariam mais com uma taxa mais rápida. A física é uma. Uma mudança muito grande em um loop pode fazer com que os objetos passem pelas paredes. Pode haver maneiras de contornar erros simples de colisão em incrementos maiores, mas para muitas interações físicas complexas, você simplesmente não terá a mesma precisão. Porém, se o loop físico for executado com mais frequência, há menos chances de falhas, pois os objetos podem ser movidos em incrementos menores sem serem renderizados sempre. Mais recursos vão para o mecanismo físico sensível e menos são desperdiçados ao desenhar mais quadros que o usuário não pode ver.
Isso é especialmente importante em jogos com mais gráficos. Se houvesse uma renderização para cada loop do jogo e um jogador não tivesse a máquina mais poderosa, pode haver pontos no jogo em que o fps cai para 30 ou 40. Embora essa ainda seja uma taxa de quadros não totalmente horrível, o o jogo começaria a ficar bastante lento se tentássemos manter cada mudança física razoavelmente pequena para evitar falhas. O jogador ficaria irritado por seu personagem andar apenas metade da velocidade normal. Se a taxa de renderização fosse independente do restante do loop, no entanto, o jogador seria capaz de permanecer em uma velocidade de caminhada fixa, apesar da queda na taxa de quadros.
fonte
Uma construção como a da sua pergunta pode fazer sentido se o subsistema de renderização tiver alguma noção de "tempo decorrido desde a última renderização" .
Considere, por exemplo, uma abordagem na qual a posição de um objeto no mundo do jogo seja representada através de
(x,y,z)
coordenadas fixas com uma abordagem que armazena adicionalmente o vetor de movimento atual(dx,dy,dz)
. Agora, você pode escrever seu loop de jogo para que a mudança de posição ocorra noupdate
método, mas também pode projetá-lo para que a mudança de movimento ocorra duranteupdate
. Com a última abordagem, mesmo que o estado do seu jogo não mude até a próximaupdate
, umrender
A função chamada em uma frequência mais alta já pode desenhar o objeto em uma posição ligeiramente atualizada. Enquanto isso tecnicamente leva a uma discrepância entre o que você vê e o que é representado internamente, a diferença é pequena o suficiente para não importar nos aspectos mais práticos, mas permite que as animações pareçam muito mais suaves.Prever o "futuro" do seu estado de jogo (apesar do risco de estar errado) pode ser uma boa idéia quando você considera, por exemplo, as latências da entrada de rede.
fonte
Além de outras respostas ...
A verificação de alterações de estado requer um processamento significativo. Se for necessário um tempo de processamento semelhante (ou mais!) Para verificar alterações, em comparação com o processamento real, você realmente não melhorou a situação. No caso de renderizar uma imagem, como o @Waddles diz, uma placa de vídeo é realmente boa em fazer a mesma coisa idiota repetidas vezes, e é mais caro verificar cada pedaço de dados em busca de alterações do que simplesmente transferi-lo para a placa de vídeo para processamento. Além disso, se a renderização for de jogabilidade, é muito improvável que a tela não tenha mudado no último tick.
Você também está assumindo que a renderização leva um tempo significativo do processador. Isso depende muito do seu processador e placa gráfica. Por muitos anos, o foco está em transferir progressivamente mais sofisticados trabalhos de renderização para a placa gráfica e reduzir a entrada de renderização necessária ao processador. Idealmente, a
render()
chamada do processador deve simplesmente configurar uma transferência DMA e é isso. A obtenção de dados na placa gráfica é então delegada ao controlador de memória e a produção da imagem é delegada na placa gráfica. Eles podem fazer isso em seu próprio tempo, enquanto o processador em paralelocontinua com a física, o mecanismo de jogabilidade e todas as outras coisas que um processador faz melhor. Obviamente, a realidade é muito mais complicada do que isso, mas ser capaz de descarregar o trabalho para outras partes do sistema também é um fator significativo.fonte