algorithm - pensar - ¿Cómo separe la lógica del juego de la pantalla?
los mejores juegos de logica para android (8)
Creo que la pregunta revela un poco de incomprensión de cómo deberían diseñarse los motores de los juegos. Lo cual está perfectamente bien, porque son cosas muy complejas que son difíciles de entender;)
Tiene la impresión correcta de que desea lo que se denomina independencia de tasa de fotogramas. Pero esto no solo se refiere a Rendering Frames.
Un Frame en motores de juego de una sola rosca se conoce comúnmente como Tick. Cada Tick que procesa ingresa, procesa la lógica del juego y representa un cuadro basado en los resultados del procesamiento.
Lo que quiere hacer es poder procesar su lógica de juego en cualquier FPS (Frames Per Second) y tener un resultado determinista.
Esto se convierte en un problema en el siguiente caso:
Entrada de verificación: - La entrada es clave: ''W'', lo que significa que movemos al personaje jugador hacia adelante 10 unidades:
playerPosition + = 10;
Ahora que está haciendo esto en cada cuadro, si está ejecutando a 30 FPS moverá 300 unidades por segundo.
Pero si en cambio estás corriendo a 10 FPS, solo moverás 100 unidades por segundo. Y así tu lógica de juego no es Frame Rate Independent.
Afortunadamente, para resolver este problema y hacer que tu lógica de juego sea Frame Rate Independent es una tarea bastante simple.
Primero, necesitas un temporizador que contará el tiempo que tarda cada fotograma en renderizar. Este número en términos de segundos (por lo tanto, 0.001 segundos para completar un Tick) se multiplicará por lo que sea que desee que sea Frame Rate Independent. Entonces en este caso:
Cuando sostiene ''W''
playerPosition + = 10 * frameTimeDelta;
(Delta es una palabra elegante para "Cambiar en algo")
Entonces tu jugador moverá una fracción de 10 en una sola marca, y después de un segundo completo de Ticks, habrás movido las 10 unidades completas.
Sin embargo, esto disminuirá cuando se trate de propiedades donde la tasa de cambio también cambia con el tiempo, por ejemplo, un vehículo en aceleración. Esto puede resolverse usando un integrador más avanzado, como "Verlet".
Enfoque multiproceso
Si todavía está interesado en una respuesta a su pregunta (ya que no la contesté pero presentó una alternativa), aquí está. Separación de la lógica del juego y la representación en diferentes hilos. Aunque tiene sus inconvenientes. Suficiente para que la gran mayoría de los Game Engines permanezcan con un solo hilo.
Eso no quiere decir que solo haya un hilo ejecutándose en los llamados motores de un solo hilo. Pero todas las tareas importantes generalmente están en un hilo central. Algunas cosas, como la detección de colisión, pueden ser de múltiples hilos, pero generalmente la fase de colisión de un Tick bloquea hasta que todos los hilos han regresado, y el motor vuelve a tener un solo hilo de ejecución.
Multithreading presenta una clase de problemas muy amplia, incluso algunos de rendimiento, ya que todo, incluso contenedores, debe ser seguro para subprocesos. Y los Game Engines son programas muy complejos para empezar, por lo que rara vez vale la pena la complicación adicional de multithreading.
Enfoque de paso de tiempo fijo
Por último, como señaló otro comentador, tener un paso de tiempo de tamaño fijo y controlar la frecuencia con la que "pisa" la lógica del juego también puede ser una forma muy efectiva de manejar esto con muchos beneficios.
Vinculado aquí para que esté completo, pero el otro comentarista también lo vincula: Fix Your Time Step
¿Cómo se puede hacer que los marcos de visualización por segundo sean independientes de la lógica del juego? Eso es para que la lógica del juego funcione a la misma velocidad sin importar qué tan rápido pueda procesar la tarjeta de video.
Desde mi experiencia (no mucho) las respuestas de Jesse y Adam deberían ponerlo en el camino correcto.
Si buscas más información y una idea de cómo funciona esto, descubrí que las aplicaciones de muestra para TrueVision 3D eran muy útiles.
Esto no cubre el material de abstracción de programa más alto, es decir, máquinas de estado, etc.
Está bien controlar el movimiento y la aceleración ajustándolos con el lapso de tiempo de tu fotograma. Pero ¿qué tal cosas como disparar un sonido 2.55 segundos después de esto o aquello, o cambiar el nivel de juego 18.25 segundos más tarde, etc.
Eso se puede vincular a un acumulador de tiempo de cuadro transcurrido (contador), PERO estos tiempos pueden estropearse si la velocidad de cuadro cae por debajo de la resolución de script de estado, es decir, si su lógica superior necesita una granularidad de 0,05 segundos y cae por debajo de 20 fps.
El determinismo se puede mantener si la lógica del juego se ejecuta en un "hilo" separado (a nivel de software, que prefiero para esto, o nivel de sistema operativo) con un intervalo de tiempo fijo, independiente de fps.
La pena puede ser que pierdas el tiempo de CPU entre fotogramas si no ocurre mucho, pero creo que probablemente valga la pena.
Hubo un excelente artículo sobre flipcode sobre esto en el día. Me gustaría desenterrarlo y presentarlo para ti.
http://www.flipcode.com/archives/Main_Loop_with_Fixed_Time_Steps.shtml
Es un bucle muy bien pensado para ejecutar un juego:
- Rosca simple
- En un reloj de juego fijo
- Con gráficos lo más rápido posible usando un reloj interpolado
Bueno, al menos eso es lo que creo que es. :-) Lástima que la discusión que siguió después de esta publicación es más difícil de encontrar. Tal vez la máquina de retorno puede ayudar allí.
time0 = getTickCount();
do
{
time1 = getTickCount();
frameTime = 0;
int numLoops = 0;
while ((time1 - time0) TICK_TIME && numLoops < MAX_LOOPS)
{
GameTickRun();
time0 += TICK_TIME;
frameTime += TICK_TIME;
numLoops++;
// Could this be a good idea? We''re not doing it, anyway.
// time1 = getTickCount();
}
IndependentTickRun(frameTime);
// If playing solo and game logic takes way too long, discard pending
time.
if (!bNetworkGame && (time1 - time0) TICK_TIME)
time0 = time1 - TICK_TIME;
if (canRender)
{
// Account for numLoops overflow causing percent 1.
float percentWithinTick = Min(1.f, float(time1 - time0)/TICK_TIME);
GameDrawWithInterpolation(percentWithinTick);
}
}
while (!bGameDone);
Koen Witters tiene un artículo muy detallado sobre diferentes configuraciones de bucles de juego.
Él cubre:
- FPS depende de la velocidad constante del juego
- Velocidad de juego dependiente de FPS variable
- Velocidad de juego constante con FPS máximo
- Velocidad de juego constante independiente de FPS variable
(Estos son los títulos extraídos del artículo, en orden de conveniencia).
Las soluciones de un solo subproceso con demoras de tiempo antes de mostrar los gráficos están bien, pero creo que la forma progresiva es ejecutar la lógica del juego en un subproceso y mostrar en otro subproceso.
Pero debes sincronizar los hilos correctamente;) Llevará mucho tiempo implementarlos, por lo que si tu juego no es demasiado grande, la solución de un solo subproceso estará bien.
Además, la extracción de GUI en subprocesos separados parece ser un gran enfoque. ¿Alguna vez has visto el mensaje emergente "Misión completa" mientras las unidades se mueven en los juegos de estrategia en tiempo real? De eso estoy hablando :)
Podrías hacer que tu bucle de juego se vea así:
int lastTime = GetCurrentTime();
while(1) {
// how long is it since we last updated?
int currentTime = GetCurrentTime();
int dt = currentTime - lastTime;
lastTime = currentTime;
// now do the game logic
Update(dt);
// and you can render
Draw();
}
Entonces solo tiene que escribir su función Update()
para tener en cuenta el diferencial de tiempo; por ejemplo, si tienes un objeto moviéndose a alguna velocidad v
, entonces actualiza su posición v * dt
cada cuadro.
Enginuity tiene un enfoque ligeramente diferente, pero interesante: el grupo de tareas.