¿Qué se mueve realmente en un corredor sin fin?

107

Por ejemplo, el famoso juego Flappy Bird, o cualquier cosa que realmente clasifique, es el jugador (en este caso, el pájaro o la cámara, lo que prefiera) avanzando o el mundo entero retrocediendo (el pájaro solo cambia la posición Y y tiene un posición X constante)?

Lendrit Ibrahimi
fuente
1
Hemos eliminado muchos comentarios fuera de tema de esta pregunta y sus respuestas. También hemos movido varias discusiones razonables para chatear, en algunos casos repetidamente. A pesar de esto, varios usuarios han sentido la necesidad de ignorar el hecho de que los comentarios no son para discusiones extendidas y continuaron. Como tal, esta pregunta se ha bloqueado temporalmente.
Josh

Respuestas:

146

Estoy un poco en desacuerdo con la respuesta de Philipp ; o al menos con cómo lo presentó. Da la impresión de que mover el mundo alrededor del jugador podría ser una mejor idea; cuando es exactamente lo contrario. Así que aquí está mi propia respuesta ...


Ambas opciones pueden funcionar, pero generalmente es una mala idea "invertir la física" moviendo el mundo alrededor del jugador en lugar del jugador alrededor del mundo.


Pérdida / desperdicio de rendimiento:

El mundo generalmente tendrá muchos objetos; muchos, si no la mayoría, estáticos o durmiendo. El jugador tendrá uno, o relativamente pocos, objetos. Mover a todo el mundo alrededor del jugador, significa mover todo en la escena excepto el jugador. Objetos estáticos, objetos dinámicos durmientes, objetos dinámicos activos, fuentes de luz, fuentes de sonido, etc. Todos tienen que ser trasladados.

Eso (obviamente) es considerablemente más caro que mover solo lo que realmente se está moviendo (el jugador, y tal vez algunas cosas más).


Mantenibilidad y extensibilidad:

Mover el mundo alrededor del jugador hace que el mundo (y todo lo que contiene) sea el punto donde las cosas están sucediendo más activamente. Cualquier error o cambio en el sistema significa que, potencialmente, todo cambia. Esta no es una buena manera de hacer las cosas; desea que los errores / cambios estén tan aislados como sea posible, para que no obtenga comportamientos inesperados en algún lugar donde no haya realizado cambios.

También hay muchos otros problemas con este enfoque. Por ejemplo, rompe muchas suposiciones de cómo se supone que las cosas funcionan en el motor. No podrá utilizar la dinámica RigidBodypara otra cosa que no sea el reproductor, por ejemplo; como un objeto con un adjunto RigidBodyno establecido en cinemática se comportará inesperadamente al establecer la posición / rotación / escala (que estaría haciendo cada cuadro, para cada objeto en la escena, excepto el jugador 😨)


¡Entonces la respuesta es solo mover al jugador!

Bueno ... si y no . Como se menciona en la respuesta de Philipp, en un tipo de juego de corredor infinito (o cualquier juego con una gran área explorable sin fisuras), ir demasiado lejos del origen eventualmente introduciría FPPE ( errores de precisión de punto flotante) notables , y aún más, eventualmente, desbordar el tipo numérico, ya sea haciendo que tu juego se bloquee o, básicamente, haciendo que el mundo del juego se agriete ... ¡con esteroides! 😵 (porque en este punto, los FPPE harían que el juego ya esté en crack "normal")


La solución real :

¡No hagas nada y haz las dos cosas! Debes mantener el mundo estático y mover al jugador a su alrededor. Pero "vuelva a enraizar" tanto al jugador como al mundo, cuando el jugador comience a alejarse demasiado de la raíz (posición [0, 0, 0]) de la escena.

Si mantienes la posición relativa de las cosas (jugador en el mundo que lo rodea) y haces este proceso en una sola actualización de cuadro, ¡el jugador (real) ni siquiera se dará cuenta!

Para hacerlo, tienes dos opciones principales:

  1. Mueva al jugador a la raíz de la escena y mueva la porción del mundo a su nueva posición con respecto al jugador.
  2. Piensa en el mundo como una cuadrícula; mueve la parte de la cuadrícula en la que se encuentra el jugador a la raíz, y mueve el jugador a su nueva posición en relación con esa parte de la cuadrícula.

Aquí hay un ejemplo de este proceso en acción.


¿Pero que lejos es muy lejos?

Si observa el código fuente de Unity, usan 1e-5( 0.00001) como base para considerar dos valores de punto flotante "iguales", dentro de Vector2y Vector3(los tipos de datos responsables de las posiciones de los objetos, rotaciones y escalas [euler-]). Dado que la pérdida de precisión de punto flotante ocurre en ambos sentidos lejos de cero, es seguro asumir que cualquier cosa debajo de 1e+5( 100000) unidades lejos de la raíz / origen de la escena es seguro para trabajar.

¡Pero! Ya que...

  1. Es más apropiado hacer un sistema para manejar estos procesos de re-rooting automáticamente.
  2. Sea cual sea su juego, no es necesario que una "sección" contigua del mundo tenga 100000 unidades (metros [?]) De ancho.

... entonces probablemente sea una buena idea volver a rootear mucho antes / con más frecuencia que esa marca de 100000 unidades. El video de ejemplo que proporcioné parece hacerlo cada 1000 unidades más o menos, por ejemplo.

XenoRo
fuente
2
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat . Utilice ese chat en lugar de publicar más comentarios aquí.
Alexandre Vaillancourt
> Eso es (obviamente) considerablemente más caro que mover solo lo que realmente se mueve <¿Es así? Al renderizar, de todos modos tendrá que hacer una multiplicación de matrices con la matriz de la cámara en todos los objetos, tener la cámara fija en la identidad sería menos costoso de esta manera.
Matsemann
Ese momento cuando el desarrollo del juego se convierte en desarrollo de la unidad ...
LJ ᛃ
@Matsemann: si hubiera ido a chatear, se habría dado cuenta de que este no es un punto nuevo. Como ya se explicó, renderice la escena NO IGUAL; son temas y tuberías diferentes y casi completamente independientes. Hacer una inversión del marco de referencia en la forma en que se colocan los objetos en la escena significará que tendrá un proceso más pesado en ambos extremos , en lugar de solo en el renderizado. --- Además, incluso si el rendimiento se negoció por igual como usted argumenta, todos los demás problemas con la inversión seguirían sin resolverse, lo que haría que ese enfoque fuera aún peor que el de enraizamiento.
XenoRo
@LJ ᛃ La pregunta se hizo sobre la unidad específicamente (ver las etiquetas de OP antes de las ediciones de D. Everhard [IMHO defacing]), por lo que la respuesta se adaptó para reflejar eso . --- Pero esta es la mejor parte: ya sea Unity, Unreal, CryEngine, Source, etc. Los mejores y / o más populares motores funcionan de esta manera (y lo hacen por una razón), por lo que la respuesta no es solo perfectamente válido en general, pero los puntos que se hacen todavía están en la cima de la precisión . En términos generales, si invierte el marco de referencia de la física, puede esperar encontrarse con problemas, especialmente en objetos dinámicos.
XenoRo
89

Ambas opciones funcionan.

Pero si quieres que el corredor interminable sea verdaderamente interminable, tendrás que mantener al jugador inmóvil y mover el mundo. De lo contrario, finalmente alcanzará los límites de las variables que utiliza para almacenar la posición X. Un número entero eventualmente se desbordaría y una variable de punto flotante se volvería cada vez menos precisa, lo que haría que el juego fallara después de un tiempo. Pero puede evitar este problema utilizando un tipo lo suficientemente grande como para que nadie encuentre estos problemas dentro del intervalo de tiempo que uno podría jugar en una sesión (cuando un jugador mueve 1000 píxeles por segundo, un número entero de 32 bits se desbordará después de 49 días).

Así que haz lo que te parezca conceptualmente más intuitivo.

Philipp
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Josh
1
No creo que eso resuelva nada. En lugar de mantener la posición del jugador (y la cámara), mantienes la posición de desplazamiento del mundo, ¿cómo es eso diferente?
Agent_L
1
@Agent_L Generalmente, los mundos se generan por partes, y cada pieza tiene su posición X / Y. Cuando la pieza se queda detrás del jugador lo suficiente (por ejemplo, fuera de la pantalla), se elimina y solo se genera una cierta cantidad por adelantado, por lo que siempre están dentro de un rango relativamente pequeño: un juego que hice nunca tuvo una coordenada mayor a ~ 1000 o menos de 0, a pesar de ser un corredor infinito, generado aleatoriamente, por eso, seguí la posición del jugador en cada fragmento y el índice de cada fragmento en la secuencia.
Nic Hartley
No compro el concepto de "desbordamiento variable". Si el jugador no se mueve, entonces el "fondo" se compensará negativamente por la misma cantidad que el jugador se compensaría si se moviera. Ambos casos tendrían exactamente el mismo problema (en direcciones opuestas), y ambos requerirían un manejo especial en los límites del desbordamiento.
gastador
2
a 1000 píxeles por segundo, llevaría más de 586 millones de años superar un número entero de 64 bits. Esto solo es realmente un problema si está utilizando tipos realmente pequeños como enteros de 16 bits.
Lyndon White
38

Partiendo de la respuesta de XenoRo , en lugar del método de re-rooting que describen, se podría hacer lo siguiente:

Cree un búfer circular de partes de su mapa infinito generado, a través del cual su personaje se moverá con la posición actualizada con el módulo aritmético (para que simplemente corra alrededor del búfer circular). Comienza a reemplazar partes de tu búfer tan pronto como tu personaje abandone el fragmento. La ecuación de actualización de los jugadores sería algo como:

player.position = (player.position + player.velocity) % worldBuffer.width;

Aquí hay un ejemplo pictoral de lo que estoy hablando:

ingrese la descripción de la imagen aquí

Aquí hay un ejemplo de lo que sucede al envolver al final.

ingrese la descripción de la imagen aquí

Con este método, nunca se encontrará con errores de precisión, pero es posible que necesite hacer un búfer muy grande si tiene la intención de hacerlo en 3D con una distancia de visión muy lejana (ya que aún tendrá que poder ver por delante de usted mismo) ) Si es un pájaro flappy, el tamaño de su fragmento probablemente solo sea tan grande como sea necesario para mantener una sola escena para una sola pantalla, y su búfer puede ser muy pequeño.

Tenga en cuenta que comenzará a obtener resultados repetidos con CUALQUIER prng, y el número máximo de secuencias no repetitivas de una generación PRNG es generalmente menor que la longitud del pow (2, número de bits utilizados internamente), con merzenne twister esto no es es un gran problema, ya que usa 2.5k de estado interno, pero si usa la variante pequeña, tiene 2 ^ 127-1 iteraciones máximas antes de repetir (o peor), sin embargo , este es un número astronómicamente grande . Puede solucionar problemas de períodos repetitivos incluso si su PRNG tiene un período corto a través de buenas funciones de mezcla de avalanchas para la semilla (a medida que agrega más estado implícitamente) repetidamente.

opa
fuente
Los comentarios no son para discusión extendida; Esta conversación se ha movido al chat .
Josh
15

Como ya se preguntó y aceptó, realmente depende del alcance y el estilo de su juego, pero como no se mencionó: FlappyBird mueve el obstáculo a través de la pantalla, en lugar del jugador en todo el mundo.

Un generador está creando instancias de objetos fuera de la pantalla con una velocidad fija en la Vector2.leftdirección.

Stephan
fuente
3
Eso funciona para FlappyBird porque es un juego muy simple. Como mencioné en mi respuesta, la misma técnica, aunque todavía es posible , sería muy problemática en cualquier cosa más compleja (con más objetos / detalles mundiales), como Subway Surfer.
XenoRo
15
Estoy de acuerdo, y tu respuesta es muy completa. Simplemente no vi a nadie mencionar qué método usa realmente el ave de aleteo, así que pensé en agregarlo.
Stephan