Qual é o objetivo da renderização independente da atualização em um loop do jogo?

74

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?

Sam
fonte
6
Pessoalmente, achei gameprogrammingpatterns.com/game-loop.html uma explicação útil
Niels
12
Nem todas as alterações na renderização são refletidas no estado do jogo. E eu suspeito que você não entendeu o ponto desse trecho de código - ele permite que você atualize várias vezes por renderização, e não renderize várias vezes por atualização.
Luaan
13
Observe que o código lê while (isTimeForUpdate), não if (isTimeForUpdate). O objetivo principal não é render()quando não há um update(), mas update()repetidamente entre render()s. Independentemente, ambas as situações têm usos válidos. O primeiro seria válido se o estado pudesse mudar fora da sua updatefunçã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.
Thierry
Uma pergunta mais lógica seria "o que é o ponto de update- dependente loop do jogo rendering"
user1306322
1
Com que frequência você tem uma atualização que não faz nada? Nem mesmo atualizando animações em segundo plano ou relógios na tela?
Pjc50

Respostas:

113

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 deltaTimetrabalhos 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 deltaTimedimensionamento é 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.

DMGregory
fonte
2
Nos casos em que tudo estará usando a mesma taxa de quadros, a sincronização de tudo pode minimizar o atraso entre a amostragem do controlador e a reação do estado do jogo. Para muitos jogos em algumas máquinas mais antigas, o pior caso seria abaixo de 17ms (os controles são lidos no início do espaço em branco vertical, as alterações no estado do jogo são aplicadas e o próximo quadro é renderizado enquanto o feixe está se movendo pela tela) . A dissociação das coisas geralmente resulta em um aumento significativo no pior dos casos.
Supercat
3
Embora seja verdade que os pipelines de atualização mais complicados facilitam a introdução involuntária de latência, não é uma consequência necessária da abordagem dissociada quando implementada corretamente. De fato, podemos até reduzir a latência. Vamos fazer um jogo que renderiza a 60 fps. Com um passo em read-update-renderfalso, 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)-renderloop 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. :)
DMGregory
3
Em uma observação interessante de jogos antigos que não são responsáveis ​​pelo tempo real decorrido, o arcade Space Invaders original teve uma falha causada pelo poder de renderização, onde o jogador destruía as naves inimigas, a renderização e, portanto, as atualizações do jogo, acelerariam, resultando acidentalmente , na curva de dificuldade icônica do jogo.
Oskuro
1
@DMGregory: Se as coisas forem feitas de forma assíncrona, será possível que uma alteração de controle ocorra logo após o loop do jogo pesquisar os controles, o ciclo de eventos do jogo após o atual terminar após o loop de renderização capturar o estado do jogo e para que o loop de renderização após o atual termine logo após o sistema de saída de vídeo pegar o buffer de quadro atual, o pior dos casos acaba sendo apenas dois tempos de loop de jogo mais dois tempos de renderização e dois períodos de quadro. Sincronizar as coisas corretamente pode reduzir isso pela metade.
Supercat
1
@Oskuro: Isso não foi uma falha. A velocidade do loop de atualização permanece constante, independentemente de quantos invasores estejam na tela, mas o jogo não atrai todos os invasores em cada loop de atualização.
Supercat
8

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.

Waddles
fonte
5
Há uma distinção a ser feita aqui entre renderizar (tudo) apenas quando algo mudou (sobre o que a pergunta pergunta) e renderizar apenas as partes que foram alteradas (como sua resposta descreve).
DMGregory
Isso é verdade, e tentei fazer essa distinção entre o primeiro e o segundo parágrafos. É raro um quadro não mudar, por isso pensei que era importante levar essa visão ao lado de sua resposta abrangente.
Waddles
Além disso, eu observaria que não há razão para não renderizar. Você sabe que sempre tem tempo para suas chamadas de renderização em todos os quadros (é melhor saber que você sempre tem tempo para suas chamadas de renderização em todos os quadros!); Portanto, há muito pouco dano em fazer uma renderização 'desnecessária' - especialmente porque esse caso essencialmente nunca surgem na prática.
Steven Stadnicki 11/11/19
@StevenStadnicki É verdade que não há um grande tempo custo de tornar tudo cada frame (desde que você precisa para ter tempo no seu orçamento para fazer isso de qualquer maneira, sempre que muitas mudanças de estado ao mesmo tempo). Mas para dispositivos portáteis como laptops, tablets, telefones ou sistemas de jogos portáteis, pode ser considerado evitar renderização redundante para fazer uso eficiente da bateria do jogador . Isso se aplica principalmente a jogos com muita interface do usuário, onde grandes partes da tela podem permanecer inalteradas para os quadros entre as ações dos jogadores e nem sempre será prático de implementar, dependendo da arquitetura do jogo.
DMGregory
6

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.

tyjkenn
fonte
1
Ou, por exemplo, meça o tempo entre os ticks e calcule até onde o personagem deve ir nesse período? Os dias de animações fixas de sprites já passaram!
Graham
2
Os seres humanos não conseguem perceber as coisas mais rápido que 60 qps sem aliasing temporal, mas a taxa de quadros necessária para obter movimento suave na ausência de desfoque de movimento pode ser muito maior que isso. Além de uma certa velocidade, uma roda giratória deve aparecer como um borrão, mas se o software não aplicar borrão de movimento e a roda girar mais de meio raio por quadro, a roda poderá parecer ruim, mesmo se a taxa de quadros for de 1000 fps.
Supercat
1
@ Graham, então você encontra o problema do meu primeiro parágrafo, onde coisas como a física se comportam mal com a maior mudança por tick. Se a taxa de quadros cair suficientemente baixo, compensar mudanças maiores pode fazer com que um jogador corra direto pela parede, perdendo completamente sua caixa de acerto.
tyjkenn
1
Normalmente, o ser humano não consegue perceber mais de 60 fps, por isso é inútil desperdiçar recursos ao renderizar mais rápido do que isso. Eu discordo dessa afirmação. VR HMDs renderizam em 90Hz, e está subindo. Acredite em mim quando digo que você CERTAMENTE pode perceber a diferença entre 90Hz e 60Hz em um fone de ouvido. Além disso, tenho visto tantos jogos ultimamente vinculados à CPU quanto à GPU. Dizer "a renderização geralmente é o processo mais lento" não é necessariamente verdade.
3Dave
@ DavidDiva, então "inútil" pode ter sido um exagero demais. O que eu quis dizer foi que renderização tende a ser um gargalo, e a maioria dos jogos parece bem a 60 qps. Certamente, existem efeitos que são importantes em alguns tipos de jogos como o VR, que você só pode obter com taxas de quadros mais rápidas, mas esses parecem ser a exceção e não a norma. Se eu estivesse executando um jogo em uma máquina mais lenta, preferiria ter física de trabalho a uma taxa de quadros rápida quase imperceptível.
tyjkenn
4

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 no updatemétodo, mas também pode projetá-lo para que a mudança de movimento ocorra durante update. Com a última abordagem, mesmo que o estado do seu jogo não mude até a próxima update, umrenderA 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.

Thomas
fonte
4

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.

Graham
fonte