Qual é o melhor padrão para criar um sistema em que todas as posições dos objetos sejam interpoladas entre dois estados de atualização?
A atualização sempre será executada na mesma frequência, mas eu quero poder renderizar em qualquer FPS. Portanto, a renderização será a mais suave possível, independentemente dos quadros por segundo, seja menor ou maior que a frequência de atualização.
Gostaria de atualizar 1 quadro para o futuro interpolar do quadro atual para o quadro futuro. Esta resposta tem um link que fala sobre fazer isso:
Timestep semi-fixo ou totalmente fixo?
Edit: Como eu também poderia usar a última e atual velocidade na interpolação? Por exemplo, com interpolação apenas linear, ele se moverá na mesma velocidade entre as posições. Preciso de uma maneira de interpolar a posição entre os dois pontos, mas leve em consideração a velocidade em cada ponto para a interpolação. Seria útil para simulações de baixa taxa, como efeitos de partículas.
fonte
Respostas:
Você deseja separar as taxas de atualização (lógica) e desenhar (renderizar).
Suas atualizações produzirão a posição de todos os objetos no mundo a serem desenhados.
Vou abordar duas possibilidades diferentes aqui, a que você solicitou, extrapolação e também outro método, a interpolação.
1
Extrapolação é onde calcularemos a posição (prevista) do objeto no próximo quadro e, em seguida, interpolaremos entre a posição atual dos objetos e a posição em que o objeto estará no próximo quadro.
Para fazer isso, cada objeto a ser desenhado deve ter um
velocity
e associadoposition
. Para encontrar a posição em que o objeto estará no próximo quadro, basta adicionarvelocity * draw_timestep
a posição atual do objeto, para encontrar a posição prevista do próximo quadro.draw_timestep
é a quantidade de tempo que passou desde o tick de renderização anterior (também conhecido como call de draw anterior).Se você deixar assim, verá que os objetos "piscam" quando a posição prevista não corresponde à posição real no próximo quadro. Para remover a tremulação, você pode armazenar a posição prevista e o lerp entre a posição prevista anteriormente e a nova posição prevista em cada etapa do sorteio, usando o tempo decorrido desde a atualização anterior como o fator lerp. Isso ainda resultará em mau comportamento quando objetos em movimento rápido mudarem repentinamente de local, e você pode querer lidar com esse caso especial. Tudo o que foi dito neste parágrafo são as razões pelas quais você não deseja usar a extrapolação.
2)
Interpolação é onde armazenamos o estado das duas últimas atualizações e interpolamos entre elas com base na quantidade de tempo atual que se passou desde a atualização antes da última. Nesta configuração, cada objeto deve ter um
position
eprevious_position
. Nesse caso, nosso desenho representará, na pior das hipóteses, uma marca de atualização atrás do estado do jogo atual e, na melhor das hipóteses, exatamente no mesmo estado que a marca de atualização atual.Na minha opinião, você provavelmente deseja interpolação como a descrevi, pois é o mais fácil de implementar e desenhar uma pequena fração de segundo (por exemplo, 1/60 segundo) atrás do seu estado atualizado atual é bom.
Editar:
Caso o exposto acima não seja suficiente para permitir a execução de uma implementação, aqui está um exemplo de como executar o método de interpolação que descrevi. Não cobrirei extrapolação, porque não consigo pensar em nenhum cenário do mundo real em que você deva preferir.
Quando você cria um objeto desenhável, ele armazena as propriedades necessárias para serem desenhadas (ou seja, as informações de estado necessárias para desenhá-lo).
Neste exemplo, armazenaremos a posição e a rotação. Você também pode armazenar outras propriedades, como cor ou posição da coordenada da textura (ou seja, se uma textura rolar).
Para impedir que os dados sejam modificados enquanto o thread de renderização o desenha (ou seja, a localização de um objeto é alterada enquanto o thread de renderização é desenhado, mas todos os outros ainda não foram atualizados), precisamos implementar algum tipo de buffer duplo.
Um objeto armazena duas cópias dele
previous_state
. Vou colocá-los em uma matriz e me referir a eles comoprevious_state[0]
eprevious_state[1]
. Da mesma forma, precisa de duas cópiascurrent_state
.Para rastrear qual cópia do buffer duplo é usada, armazenamos uma variável
state_index
, que está disponível para os threads de atualização e de desenho.O thread de atualização primeiro calcula todas as propriedades de um objeto usando seus próprios dados (qualquer estrutura de dados que você desejar). Em seguida, ele copia
current_state[state_index]
aprevious_state[state_index]
, e copia os novos dados relevantes para o desenho,position
erotation
emcurrent_state[state_index]
. Para fazer issostate_index = 1 - state_index
, inverter a cópia atualmente usada do buffer duplo.Tudo no parágrafo acima deve ser feito com uma trava retirada
current_state
. A atualização e os threads de desenho eliminam esse bloqueio. O bloqueio é retirado apenas durante a cópia das informações do estado, o que é rápido.No encadeamento de renderização, você faz uma interpolação linear na posição e rotação da seguinte maneira:
current_position = Lerp(previous_state[state_index].position, current_state[state_index].position, elapsed/update_tick_length)
Onde
elapsed
é a quantidade de tempo que passou no encadeamento de renderização, desde o último tick de atualização, eupdate_tick_length
o tempo que sua taxa fixa de atualização leva por tick (por exemplo, em atualizações de 20FPSupdate_tick_length = 0.05
).Se você não sabe qual é a
Lerp
função acima, consulte o artigo da wikipedia sobre o assunto: Interpolação linear . No entanto, se você não sabe o que é leitura, provavelmente não está pronto para implementar atualização / desenho desacoplado com desenho interpolado.fonte
Lerp(previous_speed, current_speed, elapsed/update_tick_length)
). Você pode fazer isso com qualquer número que desejar armazenar no estado. A leitura apenas fornece um valor entre dois valores, considerando um fator de leiturap.Esse problema exige que você pense sobre suas definições de início e término de maneira um pouco diferente. Os programadores iniciantes costumam pensar em mudanças de posição por quadro e esse é um bom caminho a percorrer no início. Para o bem da minha resposta, vamos considerar uma resposta unidimensional.
Digamos que você tenha um macaco na posição x. Agora você também tem um "addX" ao qual você adiciona a posição do macaco por quadro, com base no teclado ou em algum outro controle. Isso funcionará desde que você tenha uma taxa de quadros garantida. Digamos que seu x é 100 e seu addX é 10. Após 10 quadros, seu x + = addX deve acumular para 200.
Agora, em vez de addX, quando você tem uma taxa de quadros variável, pense em termos de velocidade e aceleração. Vou guiar você por toda essa aritmética, mas é super simples. O que queremos saber é até onde você quer viajar por milissegundo (1/1000 de segundo)
Se você estiver gravando a 30 FPS, seu velX deve ser 1/3 de segundo (10 quadros do último exemplo a 30 FPS) e você sabe que deseja viajar 100 'x' nesse tempo, então configure seu velX para 100 distância / 10 FPS ou 10 distância por quadro. Em milissegundos, isso resulta em 1 distância x por 3,3 milissegundos ou 0,3 'x' por milissegundo.
Agora, toda vez que você atualiza, tudo o que você precisa fazer é descobrir o tempo decorrido. Se 33 ms passaram (1/30 de segundo) ou o que for, basta multiplicar a distância 0,3 pelo número de milissegundos passados. Isso significa que você precisa de um cronômetro que ofereça precisão em ms (milissegundos), mas a maioria dos cronômetros fornece isso. Simplesmente faça algo assim:
var beginTime = getTimeInMillisecond ()
... mais tarde ...
var time = getTimeInMillisecond ()
var elapsedTime = time-beginTime
beginTime = hora
... agora use esse tempo decorrido para calcular todas as suas distâncias.
fonte