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)
Respuestas:
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:
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.
fuente
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.
fuente
n
segundos de el juego.Puede ver su sistema como si estuviera compuesto por una serie de estados y funciones, donde una función
f[j]
con entradax[j]
cambia el estado del sistemas[j]
a estados[j+1]
, de la siguiente manera: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 almacenarf[0]...f[n-1]
(recuerde, este es solo el nombre de identificación de la función) yx[0]...x[n-1]
(cuál fue la entrada para cada una de estas funciones). Para volver a jugar, simplemente comienzas[0]
y calculay 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
x
o almacenar el estado de la función prng como parte de su estados
y su implementación como parte de la funciónf
.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 arbitrarion
. Al utilizar el método 2, debe calculars[0]...s[n-1]
antes de poder calculars[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]
yx[0]...x[n-1]
al igual que el método 2, pero también almacenars[j]
, para todos,j % Q == 0
para una constante dadaQ
. En términos más fáciles, esto significa que almacena un marcador en uno de cadaQ
estados. Por ejemplo, paraQ == 100
, usted almacenas[0], s[100], s[200]...
Para calcular
s[n]
un arbitrarion
, primero carga el almacenado previamentes[floor(n/Q)]
y luego calcula todas las funciones defloor(n/Q)
an
. A lo sumo, estarás calculandoQ
funciones. Los valores más pequeños deQ
son más rápidos de calcular pero consumen mucho más espacio, mientras que los valores más grandes deQ
consumen menos espacio, pero tardan más en calcularse.El método 3 con
Q==1
es el mismo que el método 1, mientras que el método 3 conQ==inf
es 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 calculars[150]
a través des[90]
a la inversa. Con el método 3 no modificado, necesitará hacer 50 cálculos para obteners[150]
y luego 49 cálculos más para obteners[149]
y así sucesivamente. Pero dado que ya calculós[149]
obteners[150]
, puede crear un cachés[100]...s[150]
cuando calcules[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)-1
para cualquier momentok
. Esta vez, el aumentoQ
dará como resultado un tamaño más pequeño (solo para el caché), pero tiempos más largos (solo para recrear el caché). SeQ
puede 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ónf
, calculars[j-1]
es tan simple comoTenga en cuenta que aquí, la función y la entrada son ambas
j-1
, noj
. Esta misma función y entrada serían las que habría utilizado si estuviera calculandoCrear 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 tienes[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í:
¿Dónde
r[j]
están los datos descartados? Y luego cree sus funciones inversas para que tomen los datos descartados, así:Además de
f[j]
yx[j]
, también debe almacenarr[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
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:
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).
fuente
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
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 :)
fuente
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
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.
fuente
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.
fuente
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:
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.
fuente
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