Mecanismo de tiempo inverso en juegos

10

Me pregunto cómo se diseñan típicamente los mecanismos de manipulación del tiempo en los juegos. Estoy particularmente interesado en invertir el tiempo (algo así como en el último SSX o Prince of Persia).

El juego es un tirador 2D de arriba hacia abajo.

El mecanismo que estoy tratando de diseñar / implementar tiene los siguientes requisitos:

1) Las acciones de entidades aparte del personaje del jugador son completamente deterministas.

  • La acción que realiza una entidad se basa en los marcos progresados ​​desde el inicio del nivel y / o la posición del jugador en la pantalla
  • Las entidades se generan a la hora establecida durante el nivel.

2) El tiempo inverso funciona retrocediendo en tiempo real.

  • Las acciones del jugador también se invierten, reproduce a la inversa lo que realizó el jugador. El jugador no tiene control durante el tiempo inverso.
  • No hay límite en el tiempo invertido en revertir, podemos revertir todo el camino hasta el comienzo del nivel si lo desea.

Como ejemplo:

Cuadros 0-50: el jugador se mueve hacia adelante 20 unidades durante este tiempo. El enemigo 1 se genera en el cuadro 20 El enemigo 1 se mueve hacia la izquierda 10 unidades durante el cuadro 30-40 El jugador dispara una bala en el cuadro 45 La bala viaja 5 hacia adelante (45-50) y mata al enemigo 1 en marco 50

Revertir esto se reproduciría en tiempo real: el jugador se mueve hacia atrás 20 unidades durante este tiempo. El enemigo 1 revive en el cuadro 50 La bala vuelve a aparecer en el cuadro 50 La bala se mueve hacia atrás 5 y desaparece (50-45) El enemigo se mueve hacia la izquierda 10 (40-30) Enemigo eliminado en marco 20.

Solo mirando el movimiento tenía algunas ideas sobre cómo lograr esto, pensé en tener una interfaz que cambiara el comportamiento cuando el tiempo avanzaba o retrocedía. En lugar de hacer algo como esto:

void update()
{
    movement += new Vector(0,5);
}

Haría algo como esto:

public interface movement()
{
    public void move(Vector v, Entity e);
}

public class advance() implements movement
{
    public void move(Vector v, Entity e)
    {
            e.location += v;
    }
}


public class reverse() implements movement
{
    public void move(Vector v, Entity e)
    { 
        e.location -= v;
    }
}

public void update()
{
    moveLogic.move(new vector(5,0));
}

Sin embargo, me di cuenta de que esto no sería un rendimiento óptimo y rápidamente se volvería complicado para acciones más avanzadas (como un movimiento suave a lo largo de caminos curvos, etc.).

Jkh2
fuente
1
No he visto todo esto (cámara inestable de YouTube, 1,5 horas) , pero tal vez hay algunas ideas de que Jonathan Blow trabajó esto en su juego Braid.
MichaelHouse
Posible duplicado de gamedev.stackexchange.com/questions/15251/…
Hackworth

Respuestas:

9

Es posible que desee echar un vistazo al patrón de Comando .

Básicamente, cada acción reversible que realizan sus entidades se implementa como un objeto de comando. Todos esos objetos implementan al menos 2 métodos: Execute () y Undo (), además de cualquier otra cosa que necesite, como una propiedad de marca de tiempo para el momento correcto.

Siempre que su entidad realice una acción reversible, primero debe crear un objeto de comando apropiado. Lo guardas en una pila Deshacer, luego lo alimentas en tu motor de juego y lo ejecutas. Cuando desea invertir el tiempo, hace estallar acciones desde la parte superior de la pila y llama a su método Undo (), que hace lo contrario al método Execute (). Por ejemplo, en caso de un salto desde el punto A al punto B, realiza un salto desde B a A.

Después de hacer estallar una acción, guárdela en una pila de Rehacer si desea avanzar y retroceder a voluntad, al igual que la función de deshacer / rehacer en un editor de texto o programa de pintura. Por supuesto, sus animaciones también deben admitir un modo de "rebobinado" para reproducirlas al revés.

Para más travesuras de diseño de juegos, deje que cada entidad almacene sus acciones en su propia pila, para que pueda deshacerlas / rehacerlas independientemente una de la otra.

Un patrón de comando tiene otras ventajas: por ejemplo, es bastante trivial construir una grabadora de repetición, ya que solo necesita guardar todos los objetos en las pilas en un archivo, y en el momento de la repetición, simplemente ingrese uno al motor del juego uno.

Hackworth
fuente
2
Tenga en cuenta que la reversibilidad de las acciones en un juego puede ser muy delicada, debido a problemas de precisión de punto flotante, pasos de tiempo variables, etc. Es mucho más seguro guardar ese estado que reconstruirlo en la mayoría de las situaciones.
Steven Stadnicki
@StevenStadnicki Quizás, pero definitivamente es posible. Fuera de mi cabeza, C&C Generals lo hace de esta manera. Tiene repeticiones de hasta 8 jugadores que duran horas, pesando unos pocos cientos de kB en el peor de los casos, y así es como supongo que la mayoría, si no todos los juegos de RTS hacen su multijugador: simplemente no puedes transmitir el estado completo del juego con potencialmente cientos de unidades en cada cuadro, debe dejar que el motor realice la actualización. Entonces sí, definitivamente es viable.
Hackworth
3
La reproducción es algo muy diferente del rebobinado, porque las operaciones que son consistentemente reproducibles hacia delante (por ejemplo, encontrar la posición en el cuadro n, x_n, comenzando con x_0 = 0 y agregando los deltas v_n para cada paso) no son necesariamente reproducibles hacia atrás ; (x + v_n) -v_n no es constantemente igual a x en matemáticas de coma flotante. Y es fácil decir 'evitarlo', pero estás hablando de una posible revisión completa, incluida la imposibilidad de usar muchas bibliotecas externas.
Steven Stadnicki
1
Para algunos juegos, su enfoque puede ser factible, pero AFAIK la mayoría de los juegos que usan la inversión de tiempo como mecánica están usando algo más cercano al enfoque Memento de OriginalDaemon donde el estado relevante se guarda para cada cuadro.
Steven Stadnicki
2
¿Qué pasa con el rebobinado recalculando los pasos, pero guardando un cuadro clave cada dos segundos? No es probable que los errores de coma flotante hagan una diferencia significativa en solo unos segundos (dependiendo de la complejidad, por supuesto). También se muestra que funciona en compresión de video: P
Tharwen
1

Puedes echar un vistazo al patrón de recuerdo; su intención principal es implementar operaciones de deshacer / rehacer haciendo retroceder el estado del objeto, pero para ciertos tipos de juegos debería ser suficiente.

Para un juego en un bucle en tiempo real, podría considerar cada cuadro de sus operaciones como un cambio de estado y almacenarlo. Este es un enfoque simple de implementar. La alternativa es atrapar cuando se cambia el estado de un objeto. Por ejemplo, detectar cuándo cambian las fuerzas que actúan sobre un cuerpo rígido. Si está utilizando propiedades para obtener y establecer variables, esto también puede ser una implementación relativamente sencilla, la parte difícil es identificar cuándo revertir el estado, ya que no será el mismo tiempo para cada objeto (podría almacenar el tiempo de reversión como un recuento de fotogramas desde el inicio del sistema).

OriginalDaemon
fuente
0

En su caso particular, manejar el retroceso rebobinando el movimiento debería funcionar bien. Si está utilizando cualquier forma de búsqueda de ruta con las unidades AI, asegúrese de volver a calcularla después de la reversión para evitar la superposición de unidades.

El problema es la forma en que maneja el movimiento en sí: un motor de física decente (un tirador 2D de arriba hacia abajo estará bien con uno muy simple) que realiza un seguimiento de la información de los pasos pasados ​​(incluida la posición, la fuerza neta, etc.) proporcionará Una base sólida. Luego, al decidir una reversión máxima y una granularidad para los pasos de reversión, debe obtener el resultado que desea.

Alas oscuras
fuente
0

Si bien esta es una idea interesante. Aconsejaría en contra.

Volver a jugar el juego hacia adelante funciona bien, porque una operación siempre tendrá el mismo efecto en el estado del juego. Esto no implica que la operación inversa le dé el estado original. Por ejemplo, evalúe la siguiente expresión en cualquier lenguaje de programación (desactive la optimización)

(1.1 + 3 - 3) == 1.1

Al menos en C y C ++, devuelve falso. Si bien la diferencia puede ser pequeña, imagine cuántos errores se pueden acumular a 60 fps durante 10 segundos de minutos. Habrá casos en los que un jugador solo falla algo, pero lo golpea mientras el juego se reproduce al revés.

Recomendaría almacenar fotogramas clave cada medio segundo. Esto no ocupará demasiada memoria. A continuación, puede interpolar entre fotogramas clave o, mejor aún, simular el tiempo entre dos fotogramas clave y luego reproducirlo al revés.

Si su juego no es demasiado complicado, solo almacene los fotogramas clave del estado del juego 30 veces por segundo y juegue al revés. Si tuviera 15 objetos cada uno con una posición 2D, le tomaría unos 1,5 minutos llegar a un MB, sin compresión. Las computadoras tienen gigabytes de memoria.

Así que no lo compliques demasiado, no será fácil reproducir un juego al revés, y provocará muchos errores.

Hannesh
fuente