Estoy creando un juego de física que involucra cuerpos rígidos en el que los jugadores mueven piezas y partes para resolver un rompecabezas / mapa. Un aspecto muy importante del juego es que cuando los jugadores comienzan una simulación, se ejecuta igual en todas partes , independientemente de su sistema operativo, procesador, etc.
Hay espacio para mucha complejidad, y las simulaciones pueden ejecutarse durante mucho tiempo, por lo que es importante que el motor de física sea completamente determinista con respecto a sus operaciones de coma flotante; de lo contrario, puede parecer que una solución se "resuelve" en la máquina de un jugador y "fallar" en otro.
¿Cómo puedo lograr este determinismo en mi juego? Estoy dispuesto a usar una variedad de frameworks y lenguajes, incluidos Javascript, C ++, Java, Python y C #.
Me han tentado Box2D (C ++) y sus equivalentes en otros lenguajes, ya que parece satisfacer mis necesidades, pero carece de determinismo de coma flotante, particularmente con funciones trigonométricas.
La mejor opción que he visto hasta ahora ha sido el equivalente Java de Box2D (JBox2D). Parece hacer un intento de determinismo de coma flotante mediante el uso en StrictMath
lugar de Math
muchas operaciones, pero no está claro si este motor garantizará todo lo que necesito, ya que aún no he construido el juego.
¿Es posible usar o modificar un motor existente para satisfacer mis necesidades? ¿O tendré que construir un motor por mi cuenta?
EDITAR: omita el resto de esta publicación si no le importa por qué alguien necesitaría tanta precisión. Las personas en comentarios y respuestas parecen creer que estoy buscando algo que no debería, y como tal, explicaré más a fondo cómo se supone que funciona el juego.
El jugador recibe un rompecabezas o nivel, que contiene obstáculos y un objetivo. Inicialmente, una simulación no se está ejecutando. Luego pueden usar piezas o herramientas que se les proporcionan para construir una máquina. Una vez que presionan inicio, comienza la simulación y ya no pueden editar su máquina. Si la máquina resuelve el mapa, el jugador ha superado el nivel. De lo contrario, tendrán que presionar detener, alterar su máquina e intentar nuevamente.
La razón por la que todo debe ser determinista es porque planeo producir código que mapeará cada máquina (un conjunto de piezas y herramientas que intentan resolver un nivel) a un archivo xml o json registrando la posición, el tamaño y la rotación de cada pieza. Entonces será posible que los jugadores compartan soluciones (que están representadas por estos archivos) para que puedan verificar soluciones, aprender unas de otras, realizar competiciones, colaborar, etc. Por supuesto, la mayoría de las soluciones, particularmente las simples o rápidas. unos, no se verán afectados por la falta de determinismo. Pero los diseños lentos o complejos que resuelven niveles realmente difíciles podrían ser, y esos son los que probablemente serán los más interesantes y vale la pena compartir.
fuente
Respuestas:
Sobre el manejo de números de coma flotante de manera determinista
El punto flotante es determinista. Bueno, debería ser. Es complicado.
Hay mucha literatura sobre números de coma flotante:
Y cómo son problemáticos:
Por resumen. Al menos, en un solo hilo, las mismas operaciones, con los mismos datos, que suceden en el mismo orden, deben ser deterministas. Por lo tanto, podemos comenzar preocupándonos por las entradas y reordenando.
Una de esas entradas que causa problemas es el tiempo.
En primer lugar, siempre debe calcular el mismo paso de tiempo. No digo que no midas el tiempo, digo que no pasarás el tiempo en la simulación física, porque las variaciones en el tiempo son una fuente de ruido en la simulación.
¿Por qué mide el tiempo si no lo pasa a la simulación de física? Desea medir el tiempo transcurrido para saber cuándo se debe llamar a un paso de simulación y, suponiendo que esté usando el modo de reposo, cuánto tiempo duerme.
Así:
Ahora, reordenamiento de instrucciones.
El compilador podría decidir que
f * a + b
es lo mismob + f * a
, sin embargo, eso puede tener un resultado diferente. También podría compilarse en fmadd , o podría decidir tomar varias líneas como esa que suceden juntas y escribirlas con SIMD , o alguna otra optimización que no se me ocurra en este momento. Y recuerde que queremos que las mismas operaciones sucedan en el mismo orden, por lo que queremos controlar qué operaciones suceden.Y no, usar el doble no te salvará.
Debe preocuparse por el compilador y su configuración, en particular para sincronizar números de coma flotante en la red. Debe obtener las compilaciones para aceptar hacer lo mismo.
Podría decirse que escribir ensamblaje sería ideal. De esa manera usted decide qué operación hacer. Sin embargo, eso podría ser un problema para soportar múltiples plataformas.
Así:
El caso de los números de punto fijo
Debido a la forma en que los flotadores se representan en la memoria, los valores grandes perderán precisión. Es lógico que mantener sus valores pequeños (abrazadera) mitiga el problema. Por lo tanto, no hay grandes velocidades ni habitaciones grandes. Lo que también significa que puede usar física discreta porque tiene menos riesgo de hacer túneles.
Por otro lado, se acumularán pequeños errores. Entonces, truncar. Quiero decir, escalar y emitir a un tipo entero. De esa manera, sabes que nada se está acumulando. Habrá operaciones que puede realizar manteniéndose con el tipo entero. Cuando necesitas volver al punto flotante, lanzas y deshaces la escala.
Tenga en cuenta que digo escala. La idea es que 1 unidad se represente realmente como una potencia de dos (16384 por ejemplo). Sea lo que sea, conviértalo en una constante y úselo. Básicamente lo está utilizando como número de punto fijo. De hecho, si puede usar números de punto fijo adecuados de alguna biblioteca confiable mucho mejor.
Estoy diciendo truncado. Sobre el problema de redondeo, significa que no puedes confiar en el último bit del valor que obtuviste después del lanzamiento. Entonces, antes de la escala de lanzamiento para obtener un poco más de lo que necesita, y truncarlo después.
Así:
Espera, ¿por qué necesitas coma flotante? ¿No podrías trabajar solo con un tipo entero? Correcto. Trigonometría y radiación. Puede calcular tablas para trigonometría y radiación y hacer que se horneen en su fuente. O puede implementar los algoritmos utilizados para calcularlos con un número de coma flotante, excepto que utilice números de punto fijo. Sí, necesita equilibrar la memoria, el rendimiento y la precisión. Sin embargo, puede permanecer fuera de los números de coma flotante y ser determinista.
¿Sabías que hicieron cosas así para la PlayStation original? Por favor, conoce a mi perro, parches .
Por cierto, no estoy diciendo que no use punto flotante para gráficos. Solo por la física. Quiero decir, claro, las posiciones dependerán de la física. Sin embargo, como sabe, un colisionador no tiene que coincidir con un modelo. No queremos ver los resultados del truncamiento de los modelos.
Por lo tanto: USE NÚMEROS DE PUNTO FIJO.
Para ser claros, si puede usar un compilador que le permite especificar cómo funcionan los puntos flotantes, y eso es suficiente para usted, entonces puede hacerlo. Esa no siempre es una opción. Además, estamos haciendo esto para el determinismo. Los números de puntos fijos no significan que no haya errores, después de todo, tienen una precisión limitada.
No creo que "el número de punto fijo sea difícil" sea una buena razón para no usarlos. Y si desea una buena razón para usarlos, es el determinismo, en particular el determinismo en todas las plataformas.
Ver también:
Anexo : Sugiero mantener el tamaño del mundo pequeño. Dicho esto, tanto OP como Jibb Smart mencionan que alejarse de los flotadores de origen tienen menos precisión. Eso tendrá un efecto en la física, uno que se verá mucho antes que en el borde del mundo. Los números de puntos fijos, bueno, tienen una precisión fija, serán igualmente buenos (o malos, si lo prefiere) en todas partes. Lo cual es bueno si queremos determinismo. También quiero mencionar que la forma en que solemos hacer física tiene la propiedad de amplificar pequeñas variaciones. Vea The Butterfly Effect - Deterministic Physics en The Incredible Machine and Contraption Maker .
Otra forma de hacer física.
He estado pensando, la razón por la cual el pequeño error de precisión en los números de coma flotante se amplifica es porque estamos haciendo iteraciones en esos números. Cada paso de simulación tomamos los resultados del último paso de simulación y hacemos cosas sobre ellos. Acumulación de errores encima de errores. Ese es tu efecto mariposa.
No creo que veamos una sola compilación utilizando un solo subproceso en la misma máquina que produzca diferentes resultados con la misma entrada. Sin embargo, en otra máquina podría, o una construcción diferente podría.
Hay un argumento para probar allí. Si decidimos exactamente cómo deberían funcionar las cosas y podemos probar en el hardware de destino, no deberíamos publicar compilaciones que tengan un comportamiento diferente.
Sin embargo, también hay un argumento para no trabajar lejos que acumula tantos errores. Quizás esta sea una oportunidad para hacer física de una manera diferente.
Como sabrán, existe una física continua y discreta, ambas trabajan en cuánto avanzaría cada objeto en el paso de tiempo. Sin embargo, la física continua tiene los medios para descubrir el instante de la colisión en lugar de probar diferentes instantes posibles para ver si ocurrió una colisión.
Por lo tanto, propongo lo siguiente: utilice las técnicas de física continua para determinar cuándo ocurrirá la próxima colisión de cada objeto, con un gran intervalo de tiempo, mucho mayor que el de un solo paso de simulación. Luego, toma el instante de colisión más cercano y descubre dónde estará todo en ese instante.
Sí, eso es mucho trabajo de un solo paso de simulación. Eso significa que la simulación no comenzará instantáneamente ...
... Sin embargo, puede simular los siguientes pasos de simulación sin verificar la colisión cada vez, porque ya sabe cuándo ocurrirá la próxima colisión (o que no se produce una colisión en el gran intervalo de tiempo). Además, los errores acumulados en esa simulación son irrelevantes porque una vez que la simulación alcanza el gran paso de tiempo, simplemente colocamos las posiciones que calculamos de antemano.
Ahora, podemos usar el presupuesto de tiempo que habríamos utilizado para verificar las colisiones en cada paso de simulación para calcular la próxima colisión después de la que encontramos. Es decir, podemos simular con anticipación utilizando el gran paso de tiempo. Suponiendo un mundo de alcance limitado (esto no funcionará para grandes juegos), debe haber una cola de estados futuros para la simulación, y luego cada cuadro que interpolas desde el último estado al siguiente.
Yo abogaría por la interpolación. Sin embargo, dado que hay aceleraciones, no podemos simplemente interpolar todo de la misma manera. En cambio, necesitamos interpolar teniendo en cuenta la aceleración de cada objeto. Para el caso, podríamos actualizar la posición de la misma manera que lo hacemos para el paso de tiempo grande (lo que también significa que es menos propenso a errores porque no estaríamos usando dos implementaciones diferentes para el mismo movimiento).
Nota : Si estamos haciendo estos números de coma flotante, este enfoque no resuelve el problema de los objetos que se comportan de manera diferente cuanto más lejos están del origen. Sin embargo, si bien es cierto que la precisión se pierde cuanto más te alejas del origen, eso sigue siendo determinista. De hecho, es por eso que ni siquiera mencionó eso originalmente.
Apéndice
De OP en el comentario :
Entonces, no hay formato binario, ¿verdad? Eso significa que también debemos preocuparnos de lo que sea que los números de punto flotante recuperados coincidan o no con el original. Ver: precisión de flotación revisada: portabilidad de flotación de nueve dígitos
fuente
base64
elementos. No es una forma eficiente de representar grandes cantidades de tales datos, pero es incorrecto implicar que impiden el uso de representaciones binarias.Trabajo para una compañía que hace un cierto juego de estrategia en tiempo real bien conocido, y puedo decirles que es posible el determinismo de coma flotante.
El uso de diferentes compiladores, o el mismo compilador con diferentes configuraciones, o incluso diferentes versiones del mismo compilador, puede romper el determinismo.
Si necesita un juego cruzado entre plataformas o versiones de juegos, entonces creo que tendrá que ir a un punto fijo: el único juego cruzado posible que conozco con punto flotante es entre PC y XBox1, pero eso es bastante loco.
Tendrá que encontrar un motor de física que sea completamente determinista, o tomar un motor de código abierto y hacerlo determinista, o hacer funcionar su propio motor. Fuera de mi cabeza, tengo la sensación de que Unity de todas las cosas agregó un motor de física determinista, pero no estoy seguro de si es solo determinista en la misma máquina o determinista en todas las máquinas.
Si vas a intentar rodar tus propias cosas, algunas cosas que pueden ayudarte:
fuente
rsqrtps
?No estoy seguro de si este es el tipo de respuesta que está buscando, pero una alternativa podría ser ejecutar los cálculos en un servidor central. Haga que los clientes envíen la configuración a su servidor, deje que realice la simulación (o recupere una en caché) y envíe los resultados, que luego el cliente interpreta y procesa en gráficos.
Por supuesto, esto cierra cualquier plan que pueda tener para ejecutar el cliente en modo fuera de línea, y dependiendo de cuán intensivamente computacional sean las simulaciones, es posible que necesite un servidor muy potente. O múltiples, pero al menos tiene la opción de asegurarse de que tengan la misma configuración de hardware y software. Una simulación en tiempo real puede ser difícil pero no imposible (piense en las transmisiones de video en vivo, funcionan, pero con un ligero retraso).
fuente
Voy a dar una sugerencia contraintuitiva que, aunque no es 100% confiable, debería funcionar bien la mayor parte del tiempo y es muy fácil de implementar.
Reduce la precisión.
Use un tamaño de paso de tiempo constante predeterminado, realice la física sobre cada paso de tiempo en un flotador estándar de doble precisión, pero luego cuantifique la resolución de todas las variables a precisión simple (o algo aún peor) después de cada paso. Entonces, la mayoría de las posibles desviaciones que la reordenación de punto flotante podría introducir (en comparación con una ejecución de referencia del mismo programa) se eliminarán porque esas desviaciones ocurren en dígitos que ni siquiera existen en la precisión reducida. Por lo tanto, la desviación no tiene la posibilidad de una acumulación de Lyapunov (Efecto Mariposa) que eventualmente se volvería notable.
Por supuesto, la simulación será un poco menos precisa de lo que podría ser (en comparación con la física real), pero eso no es realmente notable siempre y cuando todas las ejecuciones de su programa sean inexactas de la misma manera .
Ahora, técnicamente hablando, por supuesto, es posible que un reordenamiento cause una desviación que llegue a un dígito de mayor importancia, pero si las desviaciones son realmente solo causadas por flotación y sus valores representan cantidades físicas continuas, esto es extremadamente improbable. Tenga en cuenta que hay 500 millones de
double
valores entre dossingle
, por lo que se puede esperar que la gran mayoría de los pasos de tiempo en la mayoría de las simulaciones sean exactamente iguales entre las ejecuciones de simulación. Con suerte, los pocos casos en que una desviación se realice a través de la cuantización se verán en simulaciones que no durarán tanto (al menos no con una dinámica caótica).También le recomendaría que considere el enfoque completamente opuesto a lo que está preguntando: ¡ abrace la incertidumbre ! Si el comportamiento es ligeramente no determinista, entonces esto está más cerca de los experimentos de física reales. Entonces, ¿por qué no aleatorizar deliberadamente los parámetros de inicio para cada ejecución de simulación, y hacer que sea un requisito que la simulación tenga éxito consistentemente en múltiples ensayos? Eso enseñará mucho más sobre física y sobre cómo diseñar las máquinas para que sean lo suficientemente robustas / lineales, en lugar de las súper frágiles que solo son realistas en una simulación.
fuente
¡Crea tu propia clase para almacenar números!
Puede forzar un comportamiento determinista si sabe con precisión cómo se realizarán los cálculos. Por ejemplo, si las únicas operaciones con las que trata son multiplicación, división, suma y resta, entonces sería suficiente representar todos los números como un número racional. Para hacer esto, una clase racional simple estaría bien.
Pero si desea lidiar con cálculos más complicados (posiblemente funciones trigonométricas, por ejemplo), tendrá que escribir esas funciones usted mismo. Si quisieras poder tomar el seno de un número, deberías poder escribir una función que se aproxime al seno de un número mientras solo usas las operaciones mencionadas anteriormente. Todo esto es factible, y en mi opinión evita muchos de los detalles peludos en otras respuestas. La compensación es que, en cambio, tendrás que lidiar con algunas matemáticas.
fuente
*
/
+
-
, los denominadores se volverán cada vez más grandes con el tiempo.Hay cierta confusión de terminología aquí. Un sistema físico puede ser completamente determinista, pero imposible de modelar durante un período de tiempo útil porque su comportamiento es extremadamente sensible a las condiciones iniciales, y un cambio infinitamente pequeño en las condiciones iniciales producirá comportamientos completamente diferentes.
Aquí hay un video de un dispositivo real cuyo comportamiento es intencionalmente impredecible, excepto en un sentido estadístico:
https://www.youtube.com/watch?v=EvHiee7gs9Y
Es fácil construir sistemas matemáticos simples (usando solo la suma y la multiplicación) donde el resultado después de N pasos depende del enésimo lugar decimal de las condiciones iniciales. Escribir software para modelar dicho sistema de manera consistente, en cualquier hardware y software que el usuario pueda tener, es casi imposible, incluso si tiene un presupuesto lo suficientemente grande como para probar la aplicación en cada combinación probable de hardware y software.
La mejor manera de solucionar esto es atacar el problema en su origen: hacer que la física de su juego sea tan determinista como sea necesario para obtener resultados reproducibles.
La alternativa es tratar de hacerlo determinista ajustando el software de la computadora para modelar algo que no es lo que especificó la física. El problema es que ha introducido varias capas más de complejidad en el sistema, en comparación con el cambio explícito de la física.
Como ejemplo específico, suponga que su juego involucra colisiones de cuerpos rígidos. Incluso si ignora la fricción, el modelado exacto de colisiones entre objetos de forma arbitraria que pueden estar girando a medida que se mueven es en la práctica imposible. Pero si cambia la situación para que los únicos objetos sean ladrillos rectangulares no giratorios, la vida se vuelve mucho más simple. Si los objetos en su juego no parecen ladrillos, entonces oculte ese hecho con algunos gráficos "no físicos" - por ejemplo, literalmente oculte el instante de colisión detrás de humo o llamas, o una burbuja de texto de dibujos animados "Ouch" o lo que sea.
El jugador tiene que descubrir la física del juego jugando el juego. No importa si no es "totalmente realista" siempre que sea autoconsistente y lo suficientemente similar a la experiencia de sentido común como para ser plausible.
Si hace que la física misma se comporte de manera estable, un modelo de computadora también puede producir resultados estables, al menos en el sentido de que los errores de redondeo serán irrelevantes.
fuente
Use precisión de punto flotante doble , en lugar de precisión de punto flotante único . Aunque no es perfecto, es lo suficientemente preciso como para ser considerado determinista en su física. Puede enviar un cohete a la luna con precisión de doble punto flotante, pero no con precisión de punto flotante único.
Si realmente necesita un determinismo perfecto, use matemática de punto fijo . Esto le dará menos precisión (suponiendo que use la misma cantidad de bits), pero resultados deterministas. No conozco ningún motor de física que use matemática de punto fijo, por lo que es posible que deba escribir el suyo si desea seguir esta ruta. (Algo que desaconsejaría).
fuente
Usa el patrón de recuerdo .
En su ejecución inicial, guarde los datos posicionales de cada cuadro, o cualquier referencia que necesite. Si eso es demasiado incumplimiento, solo hazlo cada n cuadros.
Luego, cuando reproduzca la simulación, siga la física arbitraria, pero actualice los datos posicionales cada n cuadros.
Pseudocódigo demasiado simplificado:
fuente