Um loop de jogo deve ser baseado em etapas de tempo fixo ou variável? Uma pessoa é sempre superior ou a escolha certa varia de acordo com o jogo?
Etapa de tempo variável
As atualizações de física são passadas para um argumento de "tempo decorrido desde a última atualização" e, portanto, dependem da taxa de quadros. Isso pode significar fazer cálculos como position += distancePerSecond * timeElapsed
.
Prós : suave, mais fácil de codificar
Contras : não determinístico, imprevisível em etapas muito pequenas ou grandes
exemplo deWiTTERS :
while( game_is_running ) {
prev_frame_tick = curr_frame_tick;
curr_frame_tick = GetTickCount();
update( curr_frame_tick - prev_frame_tick );
render();
}
Etapa de tempo fixo
As atualizações podem nem aceitar um "tempo decorrido", pois pressupõem que cada atualização é por um período fixo. Os cálculos podem ser feitos como position += distancePerUpdate
. O exemplo inclui uma interpolação durante a renderização.
Prós : previsível, determinístico (mais fácil de sincronizar na rede?), Código de cálculo mais claro
Contras : não sincronizado para monitorar o v-sync (causa gráficos instáveis a menos que você interpole), taxa de quadros máxima limitada (a menos que você interpole), difícil de trabalhar em estruturas que assuma etapas de tempo variável (como Pyglet ou Flixel )
exemplo deWiTTERS :
while( game_is_running ) {
while( GetTickCount() > next_game_tick ) {
update();
next_game_tick += SKIP_TICKS;
}
interpolation = float( GetTickCount() + SKIP_TICKS - next_game_tick )
/ float( SKIP_TICKS );
render( interpolation );
}
Alguns recursos
- Gaffer em jogos: corrija seu timestep!
- Artigo do loop de jogo do deWitter
- O FPS do Quake 3 afeta a física do salto - supostamente um motivo pelo qual o Doom 3 está bloqueado a 60fps?
- Flixel requer um timestep variável (acho que isso é determinado pelo Flash) enquanto o Flashpunk permite os dois tipos.
- Manual do Box2D § Simulando o mundo do Box2D sugere que ele use etapas de tempo constantes.
fonte
Respostas:
Existem dois problemas relacionados à questão.
No passo Fix your time, de Glen fielder, ele diz "Liberte a Física". Isso significa que sua taxa de atualização física não deve estar vinculada à sua taxa de quadros.
Nas recomendações de Erin Catto para o Box2D, ele defende isso também.
A taxa de degraus da Física deve estar vinculada à sua taxa de quadros? Não.
Os pensamentos de Erin sobre passo fixo vs passo variável:
Pensamentos de Glen sobre passo fixo vs variável:
A física deve ser escalada com deltas constantes? Sim.
A maneira de escalar a física com deltas constantes e não vincular sua taxa de atualização física à taxa de quadros ainda é usar um acumulador de tempo. No meu jogo, dou um passo adiante. Aplico uma função de suavização ao tempo recebido. Dessa forma, grandes picos de FPS não fazem com que a física salte muito longe; eles são simulados mais rapidamente para um quadro ou dois.
Você menciona que, com uma taxa fixa, a física não sincronizaria com a tela. Isso é verdade se a taxa de física alvo estiver próxima da taxa de quadros alvo. É pior que a taxa de quadros seja maior que a taxa de física. Em geral, é melhor segmentar uma taxa de atualização física de duas vezes o FPS desejado, se você puder pagar.
Se você não puder pagar uma grande taxa de atualização da física, considere interpolar as posições dos gráficos entre os quadros para fazer com que os gráficos desenhados pareçam se mover mais suavemente do que a física realmente se move.
fonte
Eu acho que existem realmente três opções, mas você as listou como apenas 2:
Opção 1
Fazer nada. Tente atualizar e renderizar em um determinado intervalo, por exemplo, 60 vezes por segundo. Se ficar para trás, deixe-o e não se preocupe. Os jogos ficarão mais devagar em câmera lenta se a CPU não conseguir acompanhar o seu jogo. Essa opção não funciona em jogos multiusuário em tempo real, mas é adequada para jogos para um jogador e foi usada com sucesso em muitos jogos.
opção 2
Use o tempo delta entre cada atualização para variar o movimento dos objetos. Ótimo em teoria, especialmente se nada no seu jogo acelera ou desacelera, mas apenas se move a uma velocidade constante. Na prática, muitos desenvolvedores implementam isso mal, e isso pode levar à detecção e à física inconsistentes de colisões. Parece que alguns desenvolvedores acham que esse método é mais fácil do que é. Se você quiser usar esta opção, precisará intensificar seu jogo consideravelmente e apresentar alguns algoritmos e matemáticos importantes, por exemplo, usando um integrador de física Verlet (em vez do Euler padrão que a maioria das pessoas usa) e usando raios para detecção de colisão em vez de simples verificações de distância de Pitágoras. Eu fiz uma pergunta sobre isso no Stack Overflow há um tempo e recebi ótimas respostas:
https://stackoverflow.com/questions/153507/calculate-the-position-of-an-accelerating-body-after-a-fficient-time
Opção 3
Use a abordagem "fix your time step" de Gaffer. Atualize o jogo em etapas fixas como na opção 1, mas faça-o várias vezes por quadro renderizado - com base em quanto tempo decorreu - para que a lógica do jogo acompanhe o tempo real, permanecendo em etapas discretas. Dessa forma, fácil de implementar a lógica do jogo, como os integradores Euler, e a simples detecção de colisão ainda funcionam. Você também tem a opção de interpolar animações gráficas com base no tempo delta, mas isso é apenas para efeitos visuais e nada que afete sua lógica principal do jogo. Você pode ter problemas se suas atualizações forem muito intensas - se as atualizações ficarem para trás, você precisará de mais e mais para acompanhar, o que tornará seu jogo ainda menos responsivo.
Pessoalmente, gosto da opção 1 quando posso fugir dela e da opção 3 quando preciso sincronizar com o tempo real. Respeito que a Opção 2 possa ser uma boa opção quando você souber o que está fazendo, mas conheço minhas limitações o suficiente para ficar longe dela.
fonte
Eu realmente gosto da maneira como o XNA Framework implementa uma etapa de tempo fixo. Se uma determinada chamada de desenho demorar um pouco demais, ela atualizará repetidamente até "recuperar o atraso". Shawn Hargreaves descreve aqui:
http://blogs.msdn.com/b/shawnhar/archive/2007/11/23/game-timing-in-xna-game-studio-2-0.aspx
O maior profissional, na minha opinião, é o que você mencionou, que torna todos os seus cálculos de código do jogo muito mais simples, porque você não precisa incluir essa variável de tempo em todo o lugar.
nota: o xna também suporta timestep variável, é apenas uma configuração.
fonte
Há outra opção - desacoplar a atualização do jogo e a atualização da física. Tentar adaptar o mecanismo de física ao timestep do jogo leva a um problema se você corrigir o timestep (o problema de sair do controle porque a integração precisa de mais timestaps, que levam mais tempo e precisam de mais timestaps), ou torná-lo variável e obter uma física instável.
A solução que eu vejo muito é fazer a física rodar em um timestep fixo, em um thread diferente (em um núcleo diferente). O jogo interpola ou extrapola, considerando os dois quadros válidos mais recentes que ele pode obter. A interpolação adiciona algum atraso, a extrapolação acrescenta alguma incerteza, mas sua física será estável e não deixará seu timestop fora de controle.
Isso não é trivial de implementar, mas pode ser uma prova futura.
fonte
Pessoalmente, eu uso uma variação da variável tempo-passo (que é uma espécie de híbrido de fixo e variável, eu acho). Enfatizo o teste desse sistema de tempo de várias maneiras e me vejo usando-o em muitos projetos. Eu recomendo para tudo? Provavelmente não.
Meus loops de jogo calculam a quantidade de quadros a serem atualizados (vamos chamar de F) e depois executamos F atualizações lógicas discretas. Toda atualização lógica assume uma unidade de tempo constante (que geralmente é 1/100 de segundo nos meus jogos). Cada atualização é executada em sequência até que todas as atualizações lógicas discretas F sejam executadas.
Por que atualizações discretas em etapas lógicas? Bem, se você tentar usar etapas contínuas e, de repente, tiver falhas físicas, porque as velocidades e distâncias calculadas para viajar são multiplicadas por um enorme valor de F.
Uma implementação ruim disso faria apenas F = hora atual - atualizações do último período. Mas se os cálculos ficarem muito atrasados (às vezes devido a circunstâncias fora de seu controle, como outro processo que rouba o tempo da CPU), você verá rapidamente pulos horríveis. Rapidamente, o FPS estável que você tentou manter se torna SPF.
No meu jogo, permito que a desaceleração "suave" (tipo de) restrinja a quantidade de recuperação lógica que deve ser possível entre dois empates. Eu faço isso fixando: F = min (F, MAX_FRAME_DELTA), que geralmente tem MAX_FRAME_DELTA = 2/100 * s ou 3/100 * s. Portanto, em vez de pular os quadros quando estiver muito atrasado na lógica do jogo, descarte qualquer perda maciça de quadros (o que atrasa as coisas), recupere alguns quadros, desenhe e tente novamente.
Ao fazer isso, também asseguro que os controles do player permaneçam sincronizados com o que é realmente mostrado na tela.
O pseudocódigo do produto final é algo assim (delta é F mencionado anteriormente):
Esse tipo de atualização não é adequado para tudo, mas para jogos no estilo arcade, prefiro ver o jogo diminuir, porque há muita coisa acontecendo além de perder quadros e perder o controle do jogador. Também prefiro isso a outras abordagens de etapas de tempo variável que acabam tendo falhas irreproduzíveis causadas pela perda de quadros.
fonte
Essa solução não se aplica a tudo, mas há outro nível de variável timestep - variável timestep para cada objeto no mundo.
Isso parece complicado, e pode ser, mas pense nisso como modelar seu jogo como uma simulação de evento discreto. Cada movimento de jogador pode ser representado como um evento que começa quando o movimento começa e termina quando o movimento termina. Se houver alguma interação que exija que o evento seja dividido (uma colisão, por exemplo), o evento será cancelado e outro evento empurrado para a fila de eventos (que provavelmente é uma fila prioritária classificada pelo horário de término do evento).
A renderização é totalmente desanexada da fila de eventos. O mecanismo de exibição interpola pontos entre os horários de início / término do evento, conforme necessário, e pode ser tão preciso ou tão desleixado nessa estimativa quanto necessário.
Para ver uma implementação complexa desse modelo, consulte o simulador espacial EXOFLIGHT . Ele usa um modelo de execução diferente da maioria dos simuladores de vôo - um modelo baseado em eventos, em vez do modelo tradicional de intervalo de tempo fixo. O loop principal básico desse tipo de simulação se parece com esse, no pseudo-código:
O principal motivo para usar um em um simulador espacial é a necessidade de fornecer aceleração arbitrária no tempo sem perda de precisão. Algumas missões no EXOFLIGHT podem levar anos para terminar, e mesmo uma opção de aceleração de 32x seria insuficiente. Você precisaria de mais de 1.000.000x de aceleração para um sim utilizável, o que é difícil de fazer em um modelo de intervalo de tempo. Com o modelo baseado em eventos, obtemos taxas de tempo arbitrárias, de 1 s = 7 ms a 1 s = 1 ano.
Alterar a taxa de tempo não altera o comportamento do sim, que é um recurso crítico. Se não houver energia suficiente da CPU disponível para executar o simulador na taxa desejada, os eventos serão empilhados e poderemos limitar a atualização da interface do usuário até que a fila de eventos seja limpa. Da mesma forma, podemos avançar o sim o quanto quisermos e ter certeza de que não estamos desperdiçando CPU nem sacrificando a precisão.
Resumindo: podemos modelar um veículo em uma órbita longa e tranquila (usando a integração Runge-Kutta) e outro veículo saltando simultaneamente ao longo do solo - ambos os veículos serão simulados com a precisão apropriada, pois não temos um timestap global.
Contras: Complexidade e falta de mecanismos físicos disponíveis no mercado que suportam este modelo :)
fonte
A etapa de tempo fixo é útil quando se considera a precisão do ponto flutuante e torna as atualizações consistentes.
É um pedaço de código simples, portanto, seria útil testá-lo e ver se funciona para o seu jogo.
O principal problema com o uso de uma etapa de tempo fixo é que os jogadores com um computador rápido não serão capazes de usar a velocidade. Renderizar a 100fps quando o jogo é atualizado apenas a 30fps é o mesmo que renderizar a 30fps.
Dito isto, pode ser possível usar mais de uma etapa de tempo fixo. 60fps podem ser usados para atualizar objetos triviais (como UI ou sprites animados) e 30fps para atualizar sistemas não triviais (como física e) e cronômetros ainda mais lentos para fazer o gerenciamento dos bastidores, como excluir objetos, recursos não utilizados, etc.
fonte
Além do que você já declarou, pode ser a sensação que você deseja que seu jogo tenha. A menos que você possa garantir que sempre terá uma taxa de quadros constante, é provável que haja uma desaceleração em algum lugar e as etapas de tempo fixo e variável parecerão muito diferentes. Corrigido terá o efeito de seu jogo entrar em câmera lenta por um tempo, que às vezes pode ser o efeito pretendido (veja um jogo de tiro à moda antiga como Ikaruga, onde explosões maciças causam lentidão após derrotar um chefe). Passos de tempo variáveis manterão as coisas em movimento na velocidade correta em termos de tempo, mas você poderá observar mudanças repentinas na posição etc., o que pode dificultar o desempenho da ação do jogador.
Realmente não consigo ver que uma etapa de tempo fixo tornará as coisas mais fáceis em uma rede; todas elas estariam um pouco fora de sincronia e desacelerariam em uma máquina, mas nenhuma outra colocaria as coisas mais fora de sincronia.
Eu sempre me inclinei pessoalmente para a abordagem variável, mas esses artigos têm algumas coisas interessantes em que pensar. Ainda encontrei etapas fixas bastante comuns, especialmente em consoles onde as pessoas pensam que a taxa de quadros é de 60fps constante em comparação com as taxas muito altas alcançáveis no PC.
fonte
Use a abordagem "fix your time step" de Gaffer. Atualize o jogo em etapas fixas como na opção 1, mas faça-o várias vezes por quadro renderizado - com base em quanto tempo decorreu - para que a lógica do jogo acompanhe o tempo real, permanecendo em etapas discretas. Dessa forma, fácil de implementar a lógica do jogo, como os integradores Euler, e a simples detecção de colisão ainda funcionam. Você também tem a opção de interpolar animações gráficas com base no tempo delta, mas isso é apenas para efeitos visuais e nada que afete sua lógica principal do jogo. Você pode ter problemas se suas atualizações forem muito intensas - se as atualizações ficarem para trás, você precisará de mais e mais para acompanhar, o que tornará seu jogo ainda menos responsivo.
Pessoalmente, gosto da opção 1 quando posso fugir dela e da opção 3 quando preciso sincronizar com o tempo real. Respeito que a Opção 2 possa ser uma boa opção quando você souber o que está fazendo, mas conheço minhas limitações o suficiente para ficar longe dela
fonte
Eu descobri que os timestados fixos sincronizados a 60fps fornecem uma animação suave ao espelho. Isso é especialmente importante para aplicativos de RV. Qualquer outra coisa é fisicamente nauseante.
Timesteps variáveis não são adequados para VR. Dê uma olhada em alguns exemplos do Unity VR que usam timesteps variáveis. É desagradável.
A regra é que, se o seu jogo em 3D for suave no modo VR, ele será excelente no modo não VR.
Compare esses dois (aplicativos Cardboard VR)
(Timestados variáveis)
(Timestados fixos)
Seu jogo deve ser multithread para atingir um tempo / taxa de quadros consistente. Física, interface do usuário e renderização devem ser separados em segmentos dedicados. É horrível PITA sincronizá-los, mas os resultados são o espelho da renderização suave que você deseja (especialmente para VR).
Jogos para celular são esp. desafiador porque as CPUs e GPUs incorporadas têm desempenho limitado. Use GLSL (gíria) com moderação para descarregar o máximo de trabalho possível da CPU. Esteja ciente de que a passagem de parâmetros para a GPU consome recursos de barramento.
Sempre mantenha sua taxa de quadros exibida durante o desenvolvimento. O verdadeiro jogo é mantê-lo fixo a 60fps. Essa é a taxa de sincronização nativa para a maioria das telas e também para a maioria dos olhos.
A estrutura que você está usando deve poder notificá-lo sobre uma solicitação de sincronização ou usar um timer. Não insira um atraso de sono / espera para conseguir isso - até pequenas variações são visíveis.
fonte
As etapas de tempo variável são para procedimentos que devem ser executados o mais rápido possível: ciclos de renderização, manipulação de eventos, coisas de rede etc.
As etapas de tempo fixo são para quando você precisa de algo previsível e estável. Isso inclui, mas não se limita à física e detecção de colisões.
Em termos práticos, a física e a detecção de colisões devem ser dissociadas de todo o resto, em seu próprio passo no tempo. O motivo para executar procedimentos como esses em uma pequena etapa de tempo fixo é mantê-los precisos. As magnitudes dos impulsos são altamente dependentes do tempo e, se o intervalo for muito grande, a simulação se tornará instável, e coisas loucas acontecerão como uma bola quicando em fases no chão ou quicando fora do mundo do jogo, e nada disso é desejável.
Tudo o resto pode ser executado em uma etapa de tempo variável (embora falando profissionalmente, geralmente é uma boa idéia permitir bloquear a renderização em uma etapa de tempo fixa). Para que um mecanismo de jogo seja responsivo, coisas como mensagens de rede e entrada do usuário devem ser tratadas o mais rápido possível, o que significa que o intervalo entre as pesquisas deve idealmente ser o mais curto possível. Isso geralmente significa variável.
Tudo o resto pode ser tratado de forma assíncrona, tornando o tempo um ponto discutível.
fonte