He hecho algunas preguntas similares en los últimos 8 meses más o menos sin verdadera alegría, así que haré la pregunta más general.
Tengo un juego de Android que es OpenGL ES 2.0. dentro tengo el siguiente Game Loop:
Mi ciclo funciona en un principio de paso de tiempo fijo (dt = 1 / ticksPerSecond )
loops=0;
while(System.currentTimeMillis() > nextGameTick && loops < maxFrameskip){
updateLogic(dt);
nextGameTick+=skipTicks;
timeCorrection += (1000d/ticksPerSecond) % 1;
nextGameTick+=timeCorrection;
timeCorrection %=1;
loops++;
}
render();
Mi integración funciona así:
sprite.posX+=sprite.xVel*dt;
sprite.posXDrawAt=sprite.posX*width;
Ahora, todo funciona más o menos como me gustaría. Puedo especificar que me gustaría que un objeto se mueva a través de una cierta distancia (por ejemplo, el ancho de la pantalla) en 2.5 segundos y lo hará. Además, debido al salto de fotogramas que permito en mi ciclo de juego, puedo hacer esto en casi cualquier dispositivo y siempre tomará 2.5 segundos.
Problema
Sin embargo, el problema es que cuando se omite un marco de representación, el gráfico tartamudea. Es extremadamente molesto. Si elimino la capacidad de omitir cuadros, entonces todo es suave como lo desee, pero se ejecutará a diferentes velocidades en diferentes dispositivos. Entonces no es una opción.
Todavía no estoy seguro de por qué se salta el marco, pero me gustaría señalar que esto no tiene nada que ver con el bajo rendimiento , he llevado el código de vuelta a 1 pequeño sprite y sin lógica (aparte de la lógica requerida para muevo el sprite) y todavía me saltan cuadros. Y esto está en una tableta Google Nexus 10 (y como se mencionó anteriormente, necesito omitir cuadros para mantener la velocidad constante en todos los dispositivos).
Entonces, la única otra opción que tengo es usar la interpolación (o extrapolación), he leído todos los artículos que existen, pero ninguno realmente me ha ayudado a comprender cómo funciona y todas mis implementaciones intentadas han fallado.
Utilizando un método, pude hacer que las cosas se movieran sin problemas, pero no fue posible porque arruinó mi colisión. Puedo prever el mismo problema con cualquier método similar porque la interpolación se pasa (y actúa dentro) al método de representación, en el momento de la representación. Entonces, si Collision corrige la posición (el personaje ahora está parado justo al lado del muro), entonces el renderizador puede alterar su posición y dibujarlo en el muro.
Entonces estoy realmente confundido. La gente ha dicho que nunca deberías alterar la posición de un objeto desde el método de renderizado, pero todos los ejemplos en línea lo muestran.
Por lo tanto, estoy pidiendo un empujón en la dirección correcta, por favor no se vincule a los artículos populares del bucle del juego (deWitters, Fix your timestep, etc.) ya que lo he leído varias veces . No le pido a nadie que escriba mi código por mí. Simplemente explique por favor en términos simples cómo la interpolación realmente funciona con algunos ejemplos. Luego iré e intentaré integrar cualquier idea en mi código y haré preguntas más específicas si es necesario más adelante. (Estoy seguro de que este es un problema con el que muchas personas luchan).
editar
Alguna información adicional: variables utilizadas en el bucle del juego.
private long nextGameTick = System.currentTimeMillis();
//loop counter
private int loops;
//Amount of frames that we will allow app to skip before logic is affected
private final int maxFrameskip = 5;
//Game updates per second
final int ticksPerSecond = 60;
//Amount of time each update should take
private final int skipTicks = (1000 / ticksPerSecond);
float dt = 1f/ticksPerSecond;
private double timeCorrection;
fuente
Respuestas:
Hay dos cosas cruciales para que el movimiento parezca suave, el primero es obviamente que lo que renderiza debe coincidir con el estado esperado en el momento en que se presenta el marco al usuario, el segundo es que debe presentar los marcos al usuario en un intervalo relativamente fijo. Presentar un cuadro a T + 10ms, luego otro a T + 30ms, luego otro a T + 40ms, parecerá que el usuario está juzgando, incluso si lo que realmente se muestra para esos momentos es correcto de acuerdo con la simulación.
Su bucle principal parece carecer de cualquier mecanismo de activación para asegurarse de que solo se procesa a intervalos regulares. Entonces, a veces puede hacer 3 actualizaciones entre renders, a veces puede hacer 4. Básicamente, su bucle se renderizará con la mayor frecuencia posible, tan pronto como haya simulado el tiempo suficiente para impulsar el estado de simulación antes de la hora actual, entonces renderiza ese estado. Pero cualquier variabilidad en cuanto al tiempo que lleva actualizar o renderizar, y el intervalo entre cuadros también variará. Tienes un paso de tiempo fijo para tu simulación, pero un paso de tiempo variable para tu renderizado.
Lo que probablemente necesite es esperar justo antes de su renderizado, lo que garantiza que solo comience a renderizar al comienzo de un intervalo de renderizado. Idealmente, eso debería ser adaptativo: si ha tardado demasiado en actualizar / renderizar y el inicio del intervalo ya ha pasado, debe renderizar de inmediato, pero también aumentar la duración del intervalo, hasta que pueda renderizar y actualizar constantemente y aún así llegar a el siguiente render antes de que finalice el intervalo. Si tiene tiempo de sobra, puede reducir lentamente el intervalo (es decir, aumentar la velocidad de fotogramas) para volver a procesar más rápido.
Pero, y aquí está el truco, si no renderiza el marco inmediatamente después de detectar que el estado de simulación se ha actualizado a "ahora", entonces introduce un alias temporal. El marco que se presenta al usuario se presenta un poco en el momento equivocado, y eso en sí mismo se sentirá como un tartamudeo.
Esta es la razón del "paso de tiempo parcial" que verá mencionado en los artículos que ha leído. Está ahí por una buena razón, y eso es porque a menos que fije su paso de tiempo de física a algún múltiplo integral fijo de su paso de tiempo de renderizado fijo, simplemente no puede presentar los cuadros en el momento correcto. Terminas presentándolos demasiado temprano o demasiado tarde. La única forma de obtener una tasa de representación fija y aún presentar algo que es físicamente correcto, es aceptar que en el momento en que se produce el intervalo de representación, lo más probable es que esté a medio camino entre dos de sus pasos de tiempo de física fija. Pero eso no significa que los objetos se modifiquen durante el renderizado, solo que el renderizado tiene que establecer temporalmente dónde están los objetos para poder representarlos en algún lugar entre donde estaban antes y dónde están después de la actualización. Eso es importante: nunca cambie el estado mundial para la representación, solo las actualizaciones deberían cambiar el estado mundial.
Entonces, para ponerlo en un bucle de pseudocódigo, creo que necesitas algo más como:
Para que esto funcione, todos los objetos que se actualizan necesitan preservar el conocimiento de dónde estaban antes y dónde están ahora, para que la representación pueda usar su conocimiento de dónde está el objeto.
Y establezcamos una línea de tiempo en milisegundos, diciendo que el renderizado tarda 3 ms en completarse, la actualización tarda 1 ms, su paso de tiempo de actualización se fija en 5 ms, y su paso de tiempo de renderizado comienza (y permanece) a 16 ms [60Hz].
Aquí hay otro matiz acerca de simular con demasiada anticipación, lo que significa que las entradas del usuario pueden ignorarse a pesar de que ocurrieron antes de que se procesara el marco, pero no se preocupe por eso hasta que esté seguro de que el bucle está simulando sin problemas.
fuente
Lo que todos te han estado diciendo es correcto. Nunca actualice la posición de simulación de su sprite en su lógica de renderizado.
Piénsalo así, tu sprite tiene 2 posiciones; donde la simulación dice que está en la última actualización de simulación, y donde se representa el sprite. Son dos coordenadas completamente diferentes.
El sprite se representa en su posición extrapolada. La posición extrapolada se calcula en cada cuadro de renderizado, se usa para renderizar el sprite y luego se tira. Eso es todo al respecto.
Aparte de eso, pareces tener una buena comprensión. Espero que esto ayude.
fuente