Cómo diseñar un sistema de repetición

75

Entonces, ¿cómo diseñaría un sistema de repetición?

Puede saberlo de ciertos juegos como Warcraft 3 o Starcraft donde puede ver el juego nuevamente después de que ya se haya jugado.

Terminas con un archivo de reproducción relativamente pequeño. Entonces mis preguntas son:

  • ¿Cómo guardar los datos? (formato personalizado?) (tamaño de archivo pequeño)
  • ¿Qué se salvará?
  • ¿Cómo hacerlo genérico para que pueda usarse en otros juegos para registrar un período de tiempo (y no una coincidencia completa, por ejemplo)?
  • Permitir reenviar y rebobinar (WC3 no pudo rebobinar hasta donde recuerdo)
escalable
fuente
3
Aunque las respuestas a continuación proporcionan mucha información valiosa, solo quería enfatizar la importancia de desarrollar su juego / motor para que sea altamente determinista ( en.wikipedia.org/wiki/Deterministic_algorithm ), ya que es esencial para lograr su objetivo.
Ari Patrick el
2
También tenga en cuenta que los motores de física no son deterministas (Havok afirma que lo es ...), por lo que la solución para almacenar solo las entradas y las marcas de tiempo producirá resultados diferentes cada vez que su juego use física.
Samaursa
55
La mayoría de los motores de física son deterministas siempre que use un paso de tiempo fijo, lo que debería hacer de todos modos. Me sorprendería mucho si Havok no lo es. El no determinismo es bastante difícil de encontrar en las computadoras ...
44
Determinista significa las mismas entradas = mismas salidas. Si tiene flotadores en una plataforma y se duplica en otra (por ejemplo), o deshabilitó deliberadamente su implementación estándar de punto flotante IEEE, eso significa que no está ejecutando con las mismas entradas, no es que no sea determinista.
3
¿Soy yo, o esta pregunta recibe una recompensa cada dos semanas?
El pato comunista el

Respuestas:

39

Este excelente artículo cubre muchos de los problemas: http://www.gamasutra.com/view/feature/2029/developing_your_own_replay_system.php

Algunas cosas que el artículo menciona y hace bien:

  • Tu juego tiene que ser determinista.
  • registra el estado inicial de los sistemas de juego en el primer cuadro y solo la entrada del jugador durante el juego.
  • cuantifique las entradas para disminuir el número de bits. Es decir. representan flotadores dentro de varios rangos (p. ej., rango [0, 1] o [-1, 1] dentro de menos bits. También se deben obtener entradas cuantificadas durante el juego real.
  • use un solo bit para determinar si una secuencia de entrada tiene datos nuevos. Dado que algunas secuencias no cambiarán con frecuencia, esto explota la coherencia temporal en las entradas.

Una forma de mejorar aún más la relación de compresión para la mayoría de los casos sería desacoplar todas las secuencias de entrada y codificarlas de forma totalmente independiente. Esta será una victoria sobre la técnica de codificación delta si codifica su carrera en 8 bits y la carrera en sí excede los 8 cuadros (muy probablemente a menos que su juego sea un verdadero mezclador de botones). He usado esta técnica en un juego de carreras para comprimir 8 minutos de entradas de 2 jugadores mientras corría por una pista hasta unos pocos cientos de bytes.

En términos de hacer que dicho sistema sea reutilizable, he hecho que el sistema de reproducción se ocupe de flujos de entrada genéricos, pero también proporcione ganchos para permitir que la lógica específica del juego organice la entrada del teclado / gamepad / mouse a estos flujos.

Si desea un rebobinado rápido o búsquedas aleatorias, puede guardar un punto de control (su estado de juego completo) cada N cuadros. Se debe elegir N para minimizar el tamaño del archivo de reproducción y también asegurarse de que el tiempo que el jugador tiene que esperar es razonable mientras el estado se reproduce hasta el punto elegido. Una forma de evitar esto es asegurarse de que las búsquedas aleatorias solo se puedan realizar en estas ubicaciones exactas de puntos de control. El rebobinado es una cuestión de establecer el estado del juego en el punto de control inmediatamente antes del cuadro en cuestión, y luego volver a reproducir las entradas hasta llegar al cuadro actual. Sin embargo, si N es demasiado grande, podría engancharse cada pocos fotogramas. Una forma de suavizar estos enganches es preasincronizar asincrónicamente los cuadros entre los 2 puntos de control anteriores mientras reproduce un cuadro en caché de la región de punto de control actual.

jpaver
fuente
si hay RNG involucrado, entonces incluya los resultados de dicho RNG en las transmisiones
fanático del trinquete
1
@ratchet freak: con el uso determinista de PRNG, puedes sobrevivir almacenando solo su semilla durante los puntos de control.
numérico
22

Además de la solución "asegúrese de que las pulsaciones de teclas se pueden volver a jugar", que puede ser sorprendentemente difícil, simplemente puede grabar el estado del juego completo en cada cuadro. Con un poco de compresión inteligente puedes apretarlo significativamente. Así es como Braid maneja su código de rebobinado y funciona bastante bien.

Dado que de todos modos necesitará un punto de verificación para rebobinar, es posible que desee intentar implementarlo de manera simple antes de complicar las cosas.

ZorbaTHut
fuente
2
+1 Con una compresión inteligente, realmente puede reducir la cantidad de datos que necesita almacenar (por ejemplo, no almacene el estado si no ha cambiado en comparación con el último estado que almacenó para el objeto actual) . Ya he intentado esto con la física y funciona muy bien. Si no tienes física y no quieres rebobinar el juego completo, optaría por la solución de Joe simplemente porque producirá los archivos más pequeños posibles, en cuyo caso si también quieres rebobinar, puedes almacenar solo los últimos nsegundos de el juego.
Samaursa
@Samaursa: si utiliza bibliotecas de compresión estándar (por ejemplo, gzip), obtendrá la misma compresión (probablemente mejor) sin necesidad de hacer manualmente cosas como verificar si el estado ha cambiado o no.
Justin
2
@ Kragen: No es realmente cierto. Las bibliotecas de compresión estándar son ciertamente buenas, pero a menudo no podrán aprovechar el conocimiento específico del dominio. Si puede ayudarlos un poco, colocando datos similares adyacentes y eliminando cosas que realmente no cambiaron, puede reducir las cosas sustancialmente.
ZorbaTHut
1
@ZorbaTHut En teoría sí, pero en la práctica ¿realmente vale la pena el esfuerzo?
Justin
44
Que valga la pena el esfuerzo depende completamente de la cantidad de datos que tenga. Si tienes un RTS con cientos o miles de unidades, probablemente sea importante. Si necesita almacenar las repeticiones en la memoria como Braid, probablemente sea importante.
21

Puede ver su sistema como si estuviera compuesto por una serie de estados y funciones, donde una función f[j]con entrada x[j]cambia el estado del sistema s[j]a estado s[j+1], de la siguiente manera:

s[j+1] = f[j](s[j], x[j])

Un estado es la explicación de todo tu mundo. La ubicación del jugador, la ubicación del enemigo, el puntaje, la munición restante, etc. Todo lo que necesita para dibujar un marco de su juego.

Una función es cualquier cosa que pueda afectar al mundo. Un cambio de marco, una pulsación de tecla, un paquete de red.

La entrada son los datos que toma la función. Un cambio de fotograma puede llevar la cantidad de tiempo transcurrido desde que pasó el último fotograma, la pulsación de tecla puede incluir la tecla real presionada, así como si se presionó o no la tecla Mayús.

En aras de esta explicación, haré los siguientes supuestos:

Supuesto 1:

La cantidad de estados para una ejecución determinada del juego es mucho mayor que la cantidad de funciones. Probablemente tenga cientos de miles de estados, pero solo una docena de funciones (cambio de trama, pulsación de teclas, paquete de red, etc.). Por supuesto, la cantidad de entradas debe ser igual a la cantidad de estados menos uno.

Supuesto 2:

El costo espacial (memoria, disco) de almacenar un solo estado es mucho mayor que el de almacenar una función y su entrada.

Supuesto 3:

El costo temporal (tiempo) de presentar un estado es similar, o solo uno o dos órdenes de magnitud más largos que el de calcular una función sobre un estado.

Dependiendo de los requisitos de su sistema de reproducción, hay varias formas de implementar un sistema de reproducción, por lo que podemos comenzar con el más simple. También haré un pequeño ejemplo usando el juego de ajedrez, grabado en pedazos de papel.

Método 1:

Tienda s[0]...s[n]. Esto es muy simple, muy sencillo. Debido a la suposición 2, el costo espacial de esto es bastante alto.

Para el ajedrez, esto se lograría dibujando todo el tablero para cada movimiento.

Método 2:

Si solo necesita la reproducción hacia adelante, simplemente puede almacenar s[0]y luego almacenar f[0]...f[n-1](recuerde, este es solo el nombre de identificación de la función) y x[0]...x[n-1](cuál fue la entrada para cada una de estas funciones). Para volver a jugar, simplemente comienza s[0]y calcula

s[1] = f[0](s[0], x[0])
s[2] = f[1](s[1], x[1])

y así...

Quiero hacer una pequeña anotación aquí. Varios otros comentaristas dijeron que el juego "debe ser determinista". Cualquiera que diga que necesita tomar Computer Science 101 nuevamente, porque a menos que su juego esté destinado a ejecutarse en computadoras cuánticas, TODOS LOS PROGRAMAS DE COMPUTADORA SON DETERMINISTOS¹. Eso es lo que hace que las computadoras sean tan increíbles.

Sin embargo, dado que su programa probablemente depende de programas externos, desde bibliotecas hasta la implementación real de la CPU, puede ser bastante difícil asegurarse de que sus funciones se comporten de la misma manera entre plataformas.

Si usa números pseudoaleatorios, puede almacenar los números generados como parte de su entrada xo almacenar el estado de la función prng como parte de su estado sy su implementación como parte de la función f.

Para el ajedrez, esto se lograría dibujando el tablero inicial (que se conoce) y luego describirá cada movimiento diciendo qué pieza fue a dónde. Así es como lo hacen, por cierto.

Método 3:

Ahora, lo más probable es que desees poder buscar tu repetición. Es decir, calcular s[n]para un arbitrario n. Al utilizar el método 2, debe calcular s[0]...s[n-1]antes de poder calcular s[n], lo que, según el supuesto 2, puede ser bastante lento.

Para implementar esto, el método 3 es una generalización de los métodos 1 y 2: almacenar f[0]...f[n-1]y x[0]...x[n-1]al igual que el método 2, pero también almacenar s[j], para todos, j % Q == 0para una constante dada Q. En términos más fáciles, esto significa que almacena un marcador en uno de cada Qestados. Por ejemplo, para Q == 100, usted almacenas[0], s[100], s[200]...

Para calcular s[n]un arbitrario n, primero carga el almacenado previamente s[floor(n/Q)]y luego calcula todas las funciones de floor(n/Q)a n. A lo sumo, estarás calculando Qfunciones. Los valores más pequeños de Qson más rápidos de calcular pero consumen mucho más espacio, mientras que los valores más grandes de Qconsumen menos espacio, pero tardan más en calcularse.

El método 3 con Q==1es el mismo que el método 1, mientras que el método 3 con Q==infes el mismo que el método 2.

Para el ajedrez, esto se lograría dibujando cada movimiento, así como uno de cada 10 tableros (para Q==10).

Método 4:

Si desea reproducción inversa, se puede hacer una pequeña variación del método 3. Supongamos Q==100, y si desea calcular s[150]a través de s[90]a la inversa. Con el método 3 no modificado, necesitará hacer 50 cálculos para obtener s[150]y luego 49 cálculos más para obtener s[149]y así sucesivamente. Pero dado que ya calculó s[149]obtener s[150], puede crear un caché s[100]...s[150]cuando calcule s[150]por primera vez, y luego ya está s[149]en el caché cuando necesita mostrarlo.

Solo necesita regenerar el caché cada vez que necesita calcular s[j], j==(k*Q)-1para cualquier momento k. Esta vez, el aumento Qdará como resultado un tamaño más pequeño (solo para el caché), pero tiempos más largos (solo para recrear el caché). Se Qpuede calcular un valor óptimo para si conoce los tamaños y tiempos necesarios para calcular estados y funciones.

Para el ajedrez, esto se lograría dibujando cada movimiento, así como uno de cada 10 tableros (para Q==10), pero también, requeriría dibujar en un pedazo de papel separado, los últimos 10 tableros que ha calculado.

Método 5:

Si los estados simplemente consumen demasiado espacio o las funciones consumen demasiado tiempo, puede crear una solución que realmente implemente (no falsifique) la reproducción inversa. Para hacer esto, debe crear funciones inversas para cada una de las funciones que tiene. Sin embargo, esto requiere que cada una de sus funciones sea una inyección. Si esto es factible, entonces para f'denotar el inverso de la función f, calcular s[j-1]es tan simple como

s[j-1] = f'[j-1](s[j], x[j-1])

Tenga en cuenta que aquí, la función y la entrada son ambas j-1, no j. Esta misma función y entrada serían las que habría utilizado si estuviera calculando

s[j] = f[j-1](s[j-1], x[j-1])

Crear la inversa de estas funciones es la parte difícil. Sin embargo, generalmente no puede hacerlo, ya que algunos datos de estado generalmente se pierden después de cada función en un juego.

Este método, tal como está, puede realizar el cálculo inverso s[j-1], pero solo si lo tiene s[j]. Esto significa que solo puede ver la reproducción al revés, comenzando desde el punto en el que decidió reproducirla al revés. Si desea reproducir hacia atrás desde un punto arbitrario, debe mezclar esto con el método 4.

Para el ajedrez, esto no se puede implementar, ya que con un tablero dado y el movimiento anterior, puede saber qué pieza se movió, pero no de dónde se movió.

Método 6:

Finalmente, si no puede garantizar que todas sus funciones sean inyecciones, puede hacer un pequeño truco para hacerlo. En lugar de hacer que cada función devuelva solo un nuevo estado, también puede hacer que devuelva los datos que descartó, así:

s[j+1], r[j] = f[j](s[j], x[j])

¿Dónde r[j]están los datos descartados? Y luego cree sus funciones inversas para que tomen los datos descartados, así:

s[j] = f'[j](s[j+1], x[j], r[j])

Además de f[j]y x[j], también debe almacenar r[j]para cada función. Una vez más, si desea poder buscar, debe almacenar marcadores, como con el método 4.

Para el ajedrez, esto sería lo mismo que el método 2, pero a diferencia del método 2, que solo dice qué pieza va a dónde, también debe almacenar de dónde vino cada pieza.

Implementación:

Dado que esto funciona para todo tipo de estados, con todo tipo de funciones, para un juego específico, puede hacer varias suposiciones, lo que facilitará la implementación. En realidad, si implementa el método 6 con todo el estado del juego, no solo podrá reproducir los datos, sino que también retrocederá en el tiempo y reanudará la reproducción desde cualquier momento. Eso sería bastante asombroso.

En lugar de almacenar todo el estado del juego, simplemente puede almacenar el mínimo necesario que necesita para dibujar un estado dado y serializar estos datos cada cantidad fija de tiempo. Sus estados serán estas serializaciones, y su entrada ahora será la diferencia entre dos serializaciones. La clave para que esto funcione es que la serialización debería cambiar poco si el estado mundial también cambia poco. Esta diferencia es completamente reversible, por lo que es muy posible implementar el método 5 con marcadores.

He visto esto implementado en algunos juegos importantes, principalmente para la reproducción instantánea de datos recientes cuando ocurre un evento (un fragmento en fps o una puntuación en juegos deportivos).

Espero que esta explicación no haya sido demasiado aburrida.

¹ Esto no significa que algunos programas actúen como si no fueran deterministas (como MS Windows ^^). Ahora en serio, si puedes hacer un programa no determinista en una computadora determinista, puedes estar seguro de que ganarás simultáneamente la medalla Fields, el premio Turing y probablemente incluso un Oscar y un Grammy por todo lo que vale.


fuente
En "TODOS LOS PROGRAMAS DE COMPUTADORA SON DETERMINISTAS", usted no considera los programas que dependen de subprocesos. Si bien el enhebrado se usa principalmente para cargar recursos o para separar el bucle de representación, hay excepciones a eso, y en ese punto es posible que ya no pueda reclamar un verdadero determinismo, a menos que sea estrictamente estricto sobre la aplicación del determinismo. Los mecanismos de bloqueo por sí solos no serán suficientes. No podría compartir CUALQUIER información mutable sin trabajo adicional adicional. En muchos escenarios, un juego no necesita ese nivel de rigor por sí mismo, pero podría para repeticiones.
krdluzni
1
@krdluzni Threading, paralelismo y números aleatorios de fuentes aleatorias verdaderas no hacen que los programas no sean deterministas. Los tiempos de subprocesos, los puntos muertos, la memoria no inicializada e incluso las condiciones de carrera son solo entradas adicionales que toma su programa. Su elección de descartar estas entradas o incluso no considerarlas en absoluto (por cualquier razón) no afectará el hecho de que su programa se ejecutará exactamente igual dadas las mismas entradas. "no determinista" es un término informático muy preciso, por lo tanto, evite usarlo si no sabe lo que significa.
@oscar (puede ser algo conciso, ocupado, podría editar más tarde): aunque en un sentido teórico estricto podría reclamar tiempos de subproceso, etc. como entradas, esto no es útil en ningún sentido práctico, ya que generalmente no pueden ser observados por el programa en sí mismo o totalmente controlado por el desarrollador. Además, un programa que no es determinista es significativamente diferente, ya que no es determinista (en el sentido de máquina de estado). Entiendo el significado del término. Desearía que hubieran elegido algo más, en lugar de sobrecargar un término preexistente.
krdluzni
@krdluzni Mi objetivo al diseñar sistemas de repetición con elementos impredecibles, como los tiempos de subprocesos (si afectan su capacidad para calcular con precisión una repetición), es tratarlos como cualquier otra fuente de entrada, al igual que la entrada del usuario. No veo a nadie quejándose de que un programa sea "no determinista" porque requiere una entrada del usuario completamente impredecible. En cuanto al término, es inexacto y confuso. Prefiero que usen algo como "prácticamente impredecible" o algo así. Y no, no es imposible, verifique la depuración de reproducción de VMWare.
9

Una cosa que otras respuestas aún no han cubierto es el peligro de flotadores. No puede hacer una aplicación totalmente determinista usando flotantes.

Usando flotadores, puede tener un sistema completamente determinista, pero solo si:

  • Usando exactamente el mismo binario
  • Usando exactamente la misma CPU

Esto se debe a que la representación interna de los flotantes varía de una CPU a otra, más dramáticamente entre las CPU AMD e Intel. Mientras los valores estén en registros de FPU, son más precisos de lo que parecen desde el lado C, por lo que cualquier cálculo intermedio se realiza con mayor precisión.

Es bastante obvio cómo esto afectará el bit AMD vs Intel, digamos que uno usa flotantes de 80 bits y el otro 64, por ejemplo, pero ¿por qué el mismo requisito binario?

Como dije, la precisión más alta está en uso siempre que los valores estén en registros de FPU . Esto significa que cada vez que recompila, la optimización de su compilador puede intercambiar valores dentro y fuera de los registros de FPU, lo que resulta en resultados sutilmente diferentes.

Puede ayudarlo configurando los indicadores _control87 () / _ controlfp () para usar la precisión más baja posible. Sin embargo, algunas bibliotecas también pueden tocar esto (al menos alguna versión de d3d lo hizo).

Jari Komppa
fuente
3
Con GCC puede usar -ffloat-store para forzar los valores fuera de los registros y truncarlos a 32/64 bits de precisión, sin tener que preocuparse de que otras bibliotecas entren en juego con sus banderas de control. Obviamente, esto afectará negativamente su velocidad (pero también lo hará cualquier otra cuantización).
8

Guarde el estado inicial de sus generadores de números aleatorios. Luego guarde, con marca de tiempo, cada entrada (mouse, teclado, red, lo que sea). Si tienes un juego en red, probablemente ya lo tengas todo en su lugar.

Vuelva a configurar los RNG y reproduzca la entrada. Eso es.

Esto no resuelve el rebobinado, para el cual no hay una solución general, aparte de reproducir desde el principio lo más rápido posible. Puede mejorar el rendimiento para esto señalando el estado completo del juego cada X segundos, luego solo tendrá que volver a reproducir ese número, pero el estado completo del juego también puede ser prohibitivo para obtener.

Los detalles del formato de archivo no importan, pero la mayoría de los motores ya tienen una forma de serializar comandos y estados, para redes, guardar o lo que sea. Solo usa eso.


fuente
4

Votaría en contra de la repetición determinista. Es MUCHO más simple y MUCHO menos propenso a errores salvar el estado de cada entidad cada 1/N de segundo.

Guarde justo lo que desea mostrar en la reproducción; si solo se trata de la posición y el rumbo, bien, si también desea mostrar estadísticas, guarde eso también, pero en general guarde lo menos posible.

Ajusta la codificación. Use la menor cantidad de bits posible para todo. La repetición no tiene que ser perfecta siempre que se vea lo suficientemente bien. Incluso si usa un flotante para, por ejemplo, encabezado, puede guardarlo en un byte y obtener 256 valores posibles (precisión de 1,4º). Eso puede ser suficiente o incluso demasiado para su problema particular.

Usa la codificación delta. A menos que sus entidades se teletransporten (y si lo hacen, trate el caso por separado), codifique las posiciones como la diferencia entre la nueva posición y la posición anterior; para movimientos cortos, puede salirse con mucho menos bits de los que necesitaría para posiciones completas .

Si desea rebobinar fácilmente, agregue fotogramas clave (datos completos, sin deltas) cada N fotogramas. De esta forma puede salirse con una precisión menor para los deltas y otros valores, los errores de redondeo no serán tan problemáticos si restablece los valores "verdaderos" periódicamente.

Finalmente, gzip todo :)

ggambett
fuente
1
Sin embargo, esto depende un poco del tipo de juego.
Jari Komppa
Sería muy cuidadoso con esta declaración. Especialmente para proyectos más grandes con dependencias de terceros, salvar el estado puede ser imposible. Al restablecer y reproducir la entrada siempre es posible.
TomSmartBishop
2

Es dificil. Primero y sobre todo, lea las respuestas de Jari Komppa.

Una reproducción realizada en mi computadora puede no funcionar en su computadora porque el resultado flotante es LIGERAMENTE diferente. Tiene mucha importancia.

Pero después de eso, si tiene números aleatorios es almacenar el valor semilla en la repetición. Luego cargue todos los estados predeterminados y establezca el número aleatorio en esa semilla. Desde allí, simplemente puede registrar el estado actual de la tecla / mouse y el tiempo que ha sido así. Luego ejecute todos los eventos usando eso como entrada.

Para saltar archivos (lo cual es mucho más difícil) necesitará volcar LA MEMORIA. Por ejemplo, dónde está cada unidad, dinero, tiempo transcurrido, todo el estado del juego. Luego, avance rápido pero reproduzca todo, excepto omitir la representación, el sonido, etc. hasta llegar al destino de tiempo que desee. Esto podría suceder cada minuto o 5 minutos, dependiendo de lo rápido que sea avanzar.

Los puntos principales son - Manejo de números aleatorios - Copia de entrada (jugador (es) y jugador (es) remoto) - Estado de volcado para saltar archivos y ... - TENER FLOTADOR NO ROMPER COSAS (sí, tuve que gritar)


fuente
2

Estoy algo sorprendido de que nadie haya mencionado esta opción, pero si su juego tiene un componente multijugador, es posible que ya haya hecho mucho trabajo duro para esta función. Después de todo, ¿qué es el modo multijugador sino un intento de reproducir los movimientos de otra persona en un momento (ligeramente) diferente en su propia computadora?

Esto también le brinda los beneficios de un tamaño de archivo más pequeño como efecto secundario, nuevamente asumiendo que ha estado trabajando en un código de red compatible con el ancho de banda.

En muchos sentidos, combina las opciones "ser extremadamente determinista" y "mantener un registro de todo". Aún necesitarás determinismo: si tu repetición es esencialmente bots jugando el juego nuevamente exactamente como lo jugaste originalmente, cualquier acción que tomen que pueda tener resultados aleatorios debe tener el mismo resultado.

El formato de datos podría ser tan simple como un volcado del tráfico de la red, aunque imagino que no estaría de más limpiarlo un poco (después de todo, no tiene que preocuparse por el retraso en una reproducción). Podrías volver a jugar solo una parte del juego utilizando el mecanismo de punto de control que otras personas han mencionado; por lo general, un juego multijugador enviará un estado completo de la actualización del juego de vez en cuando de todos modos, así que nuevamente es posible que ya hayas hecho este trabajo.

Atiaxi
fuente
0

Para obtener el archivo de reproducción más pequeño posible, deberá asegurarse de que su juego sea determinista. Por lo general, esto implica mirar el generador de números aleatorios y ver dónde se usa en la lógica del juego.

Lo más probable es que necesite tener un RNG de lógica de juego y un RNG de todo lo demás para cosas como GUI, efectos de partículas, sonidos. Una vez que hayas hecho esto, debes registrar el estado inicial de la lógica del juego RNG, luego los comandos del juego de todos los jugadores en cada cuadro.

Para muchos juegos hay un nivel de abstracción entre la entrada y la lógica del juego donde la entrada se convierte en comandos. Por ejemplo, al presionar el botón A en el controlador, el comando digital de "salto" se establece en verdadero y la lógica del juego reacciona a los comandos sin verificar el controlador directamente. Al hacer esto, solo necesitará registrar los comandos que afectan la lógica del juego (no es necesario registrar el comando "Pausa") y lo más probable es que estos datos sean más pequeños que registrar los datos del controlador. Tampoco tiene que preocuparse por registrar el estado del esquema de control en caso de que el jugador decida reasignar botones.

Rebobinar es un problema difícil usando el método determinista y aparte de usar una instantánea del estado del juego y avanzar rápidamente hasta el punto en el que desea ver que no hay mucho que pueda hacer aparte de registrar todo el estado del juego en cada fotograma.

Por otro lado, el avance rápido es ciertamente factible. Siempre y cuando la lógica de tu juego no dependa de tu renderizado, puedes ejecutar la lógica del juego tantas veces como quieras antes de renderizar un nuevo marco del juego. La velocidad de avance rápido estará limitada por su máquina. Si desea avanzar en grandes incrementos, deberá usar el mismo método de instantánea que necesitaría para rebobinar.

Posiblemente, la parte más importante de escribir un sistema de repetición que se base en el determinismo es registrar una secuencia de depuración de datos. Esta secuencia de depuración contiene una instantánea de la mayor cantidad de información posible en cada cuadro (semillas RNG, transformaciones de entidades, animaciones, etc.) y puede probar esa secuencia de depuración registrada contra el estado del juego durante las repeticiones. Esto le permitirá informarle rápidamente las discrepancias al final de cualquier marco dado. Esto le ahorrará innumerables horas de querer arrancarse el pelo de errores desconocidos no deterministas. Algo tan simple como una variable no inicializada arruinará todo a la hora 11.

NOTA: Si su juego implica una transmisión dinámica de contenido o tiene lógica de juego en múltiples hilos o en diferentes núcleos ... buena suerte.

Lathentar
fuente
0

Para habilitar tanto la grabación como el rebobinado, registre todos los eventos (generados por el usuario, generados por el temporizador, generados por la comunicación, ...).

Para cada evento, registre el tiempo del evento, lo que se modificó, los valores anteriores, los valores nuevos.

No es necesario registrar los valores calculados a menos que el cálculo sea aleatorio
(en estos casos, también puede registrar los valores calculados o registrar los cambios en la semilla después de cada cálculo aleatorio).

Los datos guardados son una lista de cambios.
Los cambios se pueden guardar en varios formatos (binario, xml, ...).
El cambio consiste en la identificación de la entidad, el nombre de la propiedad, el valor anterior, el valor nuevo.

Asegúrese de que su sistema pueda reproducir estos cambios (acceder a la entidad deseada, cambiar la propiedad deseada hacia adelante al nuevo estado o hacia atrás al estado anterior).

Ejemplo:

  • tiempo desde inicio = t1, entidad = jugador 1, propiedad = posición, cambiado de a a b
  • tiempo desde inicio = t1, entidad = sistema, propiedad = modo de juego, cambiado de c a d
  • tiempo desde inicio = t2, entidad = jugador 2, propiedad = estado, cambiado de e a f
  • Para permitir un rebobinado / avance rápido más rápido o grabar solo ciertos intervalos de tiempo,
    son necesarios fotogramas clave, si se graba todo el tiempo, de vez en cuando guarde todo el estado del juego.
    Si graba solo ciertos intervalos de tiempo, al principio guarde el estado inicial.

    Danny Varod
    fuente
    -1

    Si necesita ideas sobre cómo implementar su sistema de reproducción, busque en Google cómo implementar deshacer / rehacer en una aplicación. Puede ser obvio para algunos, pero tal vez no para todos, que deshacer / rehacer es conceptualmente lo mismo que reproducir para juegos. Es solo un caso especial en el que puede rebobinar y, según la aplicación, buscar un punto específico en el tiempo.

    Verá que nadie que implemente deshacer / rehacer se queja de deterministas / no deterministas, variables flotantes o CPU específicas.


    fuente
    Deshacer / rehacer ocurre en aplicaciones que son fundamentalmente deterministas, controladas por eventos y con luz de estado (por ejemplo, el estado de un documento de procesador de texto es únicamente el texto y la selección, no el diseño completo, que puede ser recalculado).
    Entonces es obvio que nunca ha usado aplicaciones CAD / CAM, software de diseño de circuitos, software de seguimiento de movimiento o cualquier aplicación con deshacer / rehacer más sofisticada que un procesador de textos. No digo que el código para deshacer / rehacer se pueda copiar para reproducirlo en un juego, solo que conceptualmente es el mismo (guardar estados y reproducirlos más tarde). Sin embargo, la estructura de datos principal no es una cola sino una pila.