Generación de procedimientos, actualizaciones de juegos y efecto mariposa

10

NOTA: pregunté esto en Stack Overflow hace unos días, pero tuve muy pocas vistas y ninguna respuesta. Supuse que debería preguntar en gamdev.stackexchange en su lugar.

Esta es una pregunta / solicitud general de asesoramiento sobre el mantenimiento de un sistema de generación de procedimientos a través de múltiples actualizaciones posteriores al lanzamiento, sin romper el contenido generado anteriormente.

Estoy tratando de encontrar información y técnicas para evitar problemas de "efecto mariposa" al crear contenido procesal para juegos. Cuando se usa un generador de números aleatorios sembrados, se puede usar una secuencia repetitiva de números aleatorios para crear un mundo reproducible. Si bien algunos juegos simplemente guardan el mundo generado en el disco una vez generado, una de las características poderosas de la generación de procedimientos es el hecho de que puede confiar en la reproducibilidad de la secuencia numérica para recrear una región varias veces de la misma manera, eliminando la necesidad de persistencia. Debido a las limitaciones de mi situación particular, debo minimizar la persistencia y necesito confiar en el concentrado puramente sembrado tanto como sea posible.

El principal peligro en este enfoque es que incluso el más mínimo cambio en el sistema de generación de procedimientos puede causar un efecto mariposa que cambia el mundo entero. Esto hace que sea muy difícil actualizar el juego sin destruir los mundos que los jugadores están explorando.

La técnica principal que he estado usando para evitar este problema es diseñar la generación de procedimientos en múltiples fases, cada una de las cuales tiene su propio generador de números aleatorios. Esto significa que cada subsistema es autónomo, y si algo se rompe, no afectará a todo en el mundo. Sin embargo, parece que todavía tiene un gran potencial de "rotura", incluso si está en una parte aislada del juego.

Otra posible forma de lidiar con este problema podría ser mantener versiones completas de sus generadores dentro del código, y seguir usando el generador correcto para una instancia mundial dada. Sin embargo, esto me parece una pesadilla de mantenimiento, y tengo curiosidad por saber si alguien realmente hace esto.

Por lo tanto, mi pregunta es realmente una solicitud de asesoramiento general, técnicas y patrones de diseño para tratar este problema del efecto mariposa, especialmente en el contexto de las actualizaciones del juego posteriores al lanzamiento. (Esperemos que esa no sea una pregunta demasiado amplia).

Actualmente estoy trabajando en Unity3D / C #, aunque esta es una pregunta independiente del lenguaje.

ACTUALIZAR:

Gracias por las respuestas

Se parece cada vez más a que los datos estáticos son el mejor y más seguro enfoque, y también que cuando almacenar una gran cantidad de datos estáticos no es una opción, tener una campaña larga en un mundo generado requeriría una versión estricta de los generadores utilizados. La razón de la limitación en mi caso es la necesidad de un guardado / sincronización en la nube basado en dispositivos móviles. Mi solución puede ser encontrar formas de almacenar pequeñas cantidades de datos compactos sobre cosas esenciales.

Creo que el concepto de "Jaulas" de Stormwind es una forma particularmente útil de pensar sobre las cosas. Una jaula es básicamente un punto de reinicio, evitando los efectos de pequeños cambios, es decir, enjaulando a la mariposa.

nulo
fuente
Estoy votando para cerrar esta pregunta como fuera de tema porque es demasiado amplia.
Almo
Me doy cuenta de que es muy amplio. ¿Recomendarías probar un foro de gamedev o algo así? Realmente no hay ninguna manera de hacer la pregunta más específica. Esperaba tener noticias de alguien con mucha experiencia en esta área, con algunos trucos astutos que no se me han ocurrido.
nulo
2
Almo está equivocado. No es del todo amplio. Esta es una pregunta excelente y lo suficientemente estrecha como para dar buenas respuestas. Es algo que creo que muchos de nosotros, los procesales, hemos meditado a menudo.
Ingeniero

Respuestas:

8

Creo que has cubierto las bases aquí:

  • Usar múltiples generadores o volver a sembrar a intervalos (por ejemplo, usar hashes espaciales) para limitar la propagación de los cambios. Esto probablemente funcione para el contenido cosmético, pero como usted señala, aún puede causar roturas dentro de una sección.

  • Realizar un seguimiento de la versión del generador utilizada en el archivo de guardar y responder adecuadamente. Lo que significa "apropiado" podría ser ...

    • Mantener un historial de todas las versiones anteriores de los generadores en el ejecutable de tu juego y usar el que coincida con el guardado. Esto hace que sea más difícil parchear los errores si los jugadores siguen usando salvaciones antiguas.
    • Advierte al jugador que este archivo guardado es de una versión anterior y proporciona un enlace para acceder a esa versión como un ejecutable separado. Bueno para juegos con campañas que duran unas pocas horas o días, malo para una campaña que esperas jugar durante semanas o más.
    • Mantenga solo las últimas nversiones del generador en su ejecutable. Si el archivo de guardado usa una de esas versiones recientes, (ofrezca) actualizar el archivo de guardado a la última versión. Esto usa el generador apropiado para desempaquetar cualquier estado desactualizado en literales (o en deltas de la salida del nuevo generador en la misma semilla, si son muy similares). Cualquier estado nuevo de aquí en adelante proviene de los generadores más nuevos. Sin embargo, los jugadores que no juegan durante mucho tiempo podrían quedarse atrás. Y en el peor de los casos, terminas almacenando todo el estado del juego en forma literal, en cuyo caso también podrías ...
  • Si espera alterar su lógica de generación con frecuencia y no quiere romper la compatibilidad con versiones anteriores, no confíe en el determinismo del generador: guarde todo su estado en su archivo de guardado. (es decir, "Destrozarlo desde la órbita. Es la única forma de estar seguro")

DMGregory
fuente
Si construyó las reglas de generación, ¿hay alguna forma de revertir la generación? IE, dado un estado de juego, ¿puedes revertir esto a una semilla? Si eso es posible con sus datos, en lugar de vincular al jugador a una versión diferente del juego, podría proporcionar una utilidad de actualización que genere un mundo a partir de una semilla con el sistema anterior, luego use el estado generado para producir una semilla para el nuevo generador. Sin embargo, es posible que tengas que advertir a tus jugadores que esperen la conversión.
Joe
1
Esto no es en general posible. Ni siquiera tiene la garantía de que exista una semilla para el nuevo generador que proporcione el mismo resultado que el anterior. Por lo general, estas semillas contienen aproximadamente 64 bits, pero la cantidad de mundos posibles que su juego podría soportar es mayor que 2 ^ 64, por lo que cada generador solo produce un subconjunto de estos. Cambiar el generador muy probablemente dará como resultado un nuevo subconjunto de niveles, que puede tener poca o incluso ninguna intersección con el conjunto del generador anterior.
DMGregory
Fue difícil elegir la respuesta "correcta". Elegí este porque era conciso y resumía los problemas principales de una manera clara. Gracias.
nulo
4

Podría decirse que la fuente principal de este efecto mariposa no es la generación de números, que debería ser lo suficientemente fácil como para mantener la determinación de un solo generador de números, sino más bien el uso de esos números por el código del cliente. Los cambios de código son el verdadero desafío para mantener las cosas estables.

Código: Pruebas unitarias La mejor manera de garantizar que algún cambio menor en algún lugar no se manifieste involuntariamente en otro lugar, es incluir pruebas unitarias exhaustivas para cada aspecto generativo, en su construcción. Esto es cierto para cualquier pieza de código compacto en el que cambiar una cosa puede afectar a muchas otras: necesita pruebas para todo para que pueda ver en una sola compilación lo que se ha visto afectado.

Números: secuencias periódicas / ranuras Supongamos que tiene un generador de números que sirve para todo. No asigna significado, solo escupe números en secuencia, como cualquier PRNG. Dada la misma semilla en dos carreras, obtenemos las mismas secuencias, ¿sí? Ahora piensa un poco en las cosas y decides que habrá unos 30 aspectos de tu juego que regularmente necesitarán un valor aleatorio. Aquí asignamos una secuencia de ciclismo de 30 espacios, por ejemplo, cada primer número de la secuencia es un diseño de terreno irregular, cada segundo número es perturbaciones del terreno ... etc. ... cada décimo número agrega algún error al estado de IA para el realismo. Entonces tu período es 30.

Después de 10, tienes 20 espacios libres que puedes usar para otros aspectos a medida que avanza el diseño del juego. El costo aquí es, por supuesto, que debe generar números para las ranuras 11-30 a pesar de que actualmente no están en uso , es decir, completar el período, para volver a la siguiente secuencia de 1-10. Eso tiene un costo de CPU, aunque debería ser menor (dependiendo del número de ranuras libres). La otra desventaja es que debe asegurarse de que su diseño final pueda acomodarse en la cantidad de ranuras que puso a disposición al comienzo de su proceso de desarrollo ... y cuanto más asigne al comienzo, más ranuras "vacías" potencialmente tienes que pasar por cada uno para que las cosas funcionen.

Los efectos de esto son:

  • Tienes un generador que produce números para todo
  • Cambiar el número de aspectos para los que necesita generar números no afectará el determinismo (siempre que su período sea lo suficientemente grande como para acomodar todos los aspectos)

Por supuesto, habrá un largo período durante el cual tu juego no estará disponible para el público, en alfa, por así decirlo, para que puedas reducir de 30 a 20 aspectos sin afectar a ningún jugador, solo a ti mismo, si te das cuenta de que que había asignado manera demasiadas ranuras en la salida. Por supuesto, esto ahorraría algunos ciclos de CPU. Pero tenga en cuenta que una buena función hash (que puede escribir usted mismo) debería ser extremadamente rápida. Por lo tanto, tener que ejecutar espacios adicionales no debería ser costoso.

Ingeniero
fuente
Hola. Eso suena similar a algunas cosas que estoy haciendo. Por lo general, genero un montón de sub-semillas por adelantado en función de la semilla mundial inicial. Recientemente comencé a pregenerar un conjunto de ruido más largo, y luego cada "ranura" es simplemente un índice en ese conjunto. De esa manera, cada subsistema puede tomar la semilla correcta y trabajar de forma aislada. Otra gran técnica es usar las coordenadas x, y para generar una semilla para cada ubicación. Estoy usando el código de la respuesta de Euphoric en esta página de la pila: programmers.stackexchange.com/questions/161336/…
nulo
3

Si desea persistencia con PCG, le sugiero que trate el código PCG en sí mismo como datos . Del mismo modo que conservaría los datos en las revisiones con contenido regular, con contenido generado, si desea conservarlos en las revisiones, deberá conservar el generador.

Por supuesto, el enfoque más popular es convertir los datos generados en datos estáticos, como has mencionado.

No conozco ejemplos de juegos que tengan muchas versiones de generador, porque la persistencia es inusual en los juegos de PCG; es por eso que permadeath a menudo va de la mano con PCG. Sin embargo, hay muchos ejemplos de múltiples PCG, incluso del mismo tipo, dentro del mismo juego. Por ejemplo, Unangband tiene muchos generadores separados para salas de mazmorras y, a medida que se agregan nuevos, los viejos siguen funcionando igual. Si eso se puede mantener depende de su implementación. Una forma de mantenerlo mantenible es usar scripts para implementar sus generadores, manteniéndolos aislados con el resto del código del juego.

congusbongus
fuente
Esa es una idea inteligente, simplemente usar diferentes generadores para diferentes áreas.
nulo
2

Mantengo un área de aproximadamente 30000 kilómetros cuadrados, con capacidad para aproximadamente 1 millón de edificios y otros objetos, además de ubicaciones aleatorias de cosas misceláneas. Una simulación al aire libre de c. Los datos almacenados son de aproximadamente 4 GB. Tengo la suerte de tener espacio de almacenamiento, pero no es ilimitado.

Aleatorio es aleatorio, no controlado. Pero uno puede enjaularlo un poco:

  • Controle su inicio final final (como se mencionó en otras publicaciones, el número de semilla y cuántos números generados).
  • Limite su espacio numérico, por ejemplo. generar enteros entre 0 y 100 solamente.
  • Compensa su espacio numérico, agregando un valor (por ejemplo, 100 + [números generados entre 0 y 100] produce números aleatorios entre 100 y 200)
  • Escalarlo (por ejemplo, multiplicar por 0.1)
  • Y aplique varias jaulas a su alrededor. Esto está reduciendo, desechando parte de la generación. P.ej. Si se genera en un espacio bidimensional, se puede colocar un rectángulo en la parte superior de los pares de números y desechar lo que está afuera. O un círculo, o un polígono. Si en el espacio 3D, uno puede aceptar, por ejemplo, solo trillizos que residen dentro de una esfera, o alguna otra forma (ahora pensando visualmente, pero esto no necesariamente tiene nada que ver con la visualización o posicionamiento real).

Eso es todo. Las jaulas también consumen datos, desafortunadamente.

Hay un dicho en finlandés, Hajota ja hallitse. Se traduce en dividir y conquistar .

Rápidamente abandoné la idea de una definición precisa de los detalles más pequeños. Random quiere libertad, entonces obtuvo la libertad. Deje volar a la mariposa, dentro de su jaula. En cambio, me concentré en tener una forma rica de definir (¡y mantener !!) las jaulas. No importa qué automóviles sean, siempre que sean azules o azul oscuro (un empleador aburrido dijo una vez :-)). "Azul o azul oscuro" es la jaula (muy pequeña) aquí, a lo largo de la dimensión de color.

¿Qué es manejable para controlar y gestionar espacios numéricos?

  • Una cuadrícula booleana es (¡los bits son pequeños!)
  • Los puntos de esquina son
  • Como es una estructura de árbol (= para seguir en "jaulas con jaulas")

En cuanto al mantenimiento, y en cuanto a compatibilidad de versiones ... tenemos
: if version = n then
: elseif version = m then ...
Sí, la base del código se hace más grande :-).

Cosas familiares Su manera correcta de seguir adelante sería definir un método rico para dividir y conquistar , y sacrificar algunos datos al respecto. Luego, cuando sea posible, otorgue libertad de aleatorización (local), donde no es crucial controlarla.

No es totalmente incompatible con el divertido "nuke it fom orbit" propuesto por DMGregory, pero ¿tal vez use armas nucleares pequeñas y precisas? :-)

Ventormenta
fuente
Gracias por tu respuesta. Eso suena como un área de procedimiento impresionantemente grande para mantener. Puedo ver cómo para un área tan grande, incluso cuando tienes acceso a una gran cantidad de almacenamiento, sigue siendo imposible simplemente almacenar todo. Parece que los generadores versionados tendrán que ser el camino a seguir. Que así sea :)
nulo
De todas las respuestas, me encuentro pensando más en esta. Disfruté tus descripciones ligeramente filosóficas de las cosas. El término "jaula" me parece muy útil al explicar las ideas, así que gracias por eso. Deje que la mariposa vuele ... en su jaula :)
nulo
PD: Tengo mucha curiosidad por saber en qué juego trabajas. ¿Eres capaz de compartir esa información?
nulo
Podría agregar una cosa más sobre el espacio numérico: vale la pena mantenerse siempre cerca de cero. Que probablemente ya sabías. Cerca de cero proporciona la mejor precisión numérica y la mayor explosión por menos bits. Siempre puede compensar una gran cantidad de números cercanos a cero más adelante, pero solo necesita un solo número para eso. Del mismo modo, puede (casi diría que DEBE) mover el cálculo distante más cerca de cero, con un desplazamiento. - Stormwind
Stormwind
Evaluando lo anterior, considere un movimiento de vehículo pequeño, un incremento de 1 cuadro de 0.01 [metros, unidades]: no puede calcular con precisión 10000.1 + 0.01 en precisión numérica de 32 bits (simple) pero PUEDE calcular 0.1 + 0.01. Por lo tanto, si la "acción" tiene lugar muy lejos (detrás de las montañas :-)), no vayas allí, en lugar de mover las montañas hacia ti (muévete con 10000, entonces estás en 0.1 ahora). Válido también para espacio de almacenamiento. Uno puede ser codicioso con el almacenamiento de valores numéricos cercanos entre sí. Almacene la parte común de ellos una vez, y las variaciones individualmente, ¡pueden ahorrar bits! ¿Encontraste el enlace? ;-)
Stormwind