¿Cómo se separa la lógica y la representación del juego? Sé que parece que ya hay preguntas aquí que preguntan exactamente eso, pero las respuestas no me satisfacen.
Por lo que entiendo hasta ahora, el punto de separarlos en diferentes subprocesos es para que la lógica del juego pueda comenzar a ejecutarse para el siguiente tic inmediatamente, en lugar de esperar al próximo vsync, donde el renderizado finalmente regresa de la llamada swapbuffer en la que se ha bloqueado.
Pero específicamente qué estructuras de datos se utilizan para evitar condiciones de carrera entre el hilo lógico del juego y el hilo de renderizado. Presumiblemente, el hilo de renderizado necesita acceso a varias variables para descubrir qué dibujar, pero la lógica del juego podría estar actualizando estas mismas variables.
¿Existe una técnica estándar de facto para manejar este problema? Tal vez, como copiar los datos que necesita el hilo de renderizado después de cada ejecución de la lógica del juego. Cualquiera sea la solución, ¿será la sobrecarga de la sincronización o lo que sea menor que simplemente ejecutar todo con un solo subproceso?
fuente
Respuestas:
He estado trabajando en lo mismo. La preocupación adicional es que OpenGL (y que yo sepa, OpenAL), y una serie de otras interfaces de hardware, son efectivamente máquinas de estado que no se llevan bien con la llamada de múltiples subprocesos. No creo que su comportamiento esté definido, y para LWJGL (posiblemente también JOGL) a menudo arroja una excepción.
Lo que terminé haciendo fue crear una secuencia de hilos que implementaban una interfaz específica y cargarlos en la pila de un objeto de control. Cuando ese objeto recibe una señal para cerrar el juego, se ejecutará a través de cada hilo, llamará a un método implementado ceaseOperations () y esperará a que se cierre antes de cerrarse. Los datos universales que podrían ser relevantes para reproducir sonido, gráficos o cualquier otro dato se mantienen en una secuencia de objetos que son volátiles o están disponibles universalmente para todos los hilos, pero nunca se guardan en la memoria de hilos. Hay una leve penalización de rendimiento allí, pero si se usa correctamente, me ha permitido asignar flexiblemente audio a un hilo, gráficos a otro, física a otro, y así sucesivamente sin vincularlos al tradicional (y temido) "bucle de juego".
Por lo tanto, por regla general, todas las llamadas de OpenGL pasan por el subproceso de gráficos, todas las OpenAL a través del subproceso de audio, todas las entradas a través del subproceso de entrada y todo lo que el subproceso de control de organización debe preocuparse es la gestión de subprocesos. El estado del juego se lleva a cabo en la clase GameState, que todos pueden ver cuando lo necesiten. Si alguna vez decido que, por ejemplo, JOAL se ha puesto al día y quiero usar la nueva edición de JavaSound, solo implemento un hilo diferente para Audio.
Espero que vean lo que digo, ya tengo algunos miles de líneas en este proyecto. Si desea que intente reunir una muestra, veré qué puedo hacer.
fuente
Por lo general, la lógica que se ocupa de los pases de representación gráfica (y su programación, y cuándo se ejecutarán, etc.) se maneja en un hilo separado. Sin embargo, ese hilo ya está implementado (en funcionamiento) por la plataforma que utiliza para desarrollar su ciclo de juego (y juego).
Entonces, para obtener un bucle de juego donde la lógica del juego se actualiza independientemente del programa de actualización de gráficos, no necesita hacer hilos adicionales, simplemente toque el hilo ya existente para dichas actualizaciones de gráficos.
Esto depende de qué plataforma estés usando. Por ejemplo:
Si lo está haciendo en la mayoría de las plataformas relacionadas con Open GL ( GLUT para C / C ++ , JOLG para Java , Acción relacionada con OpenGL ES de Android ), generalmente le proporcionarán un método / función que periódicamente se llama por el hilo de renderizado, y que usted puede integrarse en su bucle de juego (sin hacer que las iteraciones del gameloop dependan de cuándo se llama a ese método). Para GLUT usando C, haces algo como esto:
glutDisplayFunc (myFunctionForGraphicsDrawing);
glutIdleFunc (myFunctionForUpdatingState);
en JavaScript, puedes usar Web Workers
ya que no hay subprocesos múltiples (que puede alcanzar programáticamente), también puede usar el mecanismo "requestAnimationFrame" para recibir notificaciones cuando se programe una nueva representación gráfica y hacer las actualizaciones de estado del juego en consecuencia .Básicamente, lo que quieres es un ciclo de juego de pasos mixtos: tienes un código que actualiza el estado del juego, y que se llama dentro del hilo principal de tu juego, y también quieres acceder periódicamente (o ser llamado por) Subproceso de representación de gráficos existente para avisar cuándo es el momento de actualizar los gráficos.
fuente
En Java existe la palabra clave "sincronizada", que bloquea las variables que le pasa para que sean seguras. En C ++ puede lograr lo mismo con Mutex. P.ej:
Java:
C ++:
El bloqueo de variables asegura que no cambien mientras se ejecuta el código que lo sigue, por lo que las variables no cambian por su hilo de actualización mientras las procesa (de hecho, cambian, pero desde el punto de vista de su hilo de representación no lo hacen ' t). Sin embargo, debe tener cuidado con la palabra clave sincronizada en Java, ya que solo se asegura de que el puntero a la variable / objeto no cambie. Los atributos aún pueden cambiar sin cambiar el puntero. Para contemplar esto, puede copiar el objeto usted mismo o llamar sincronizado en todos los atributos del objeto que no desea cambiar.
fuente
Lo que generalmente he visto para manejar la comunicación de hilo lógico / render es triplicar sus datos. De esta manera, el subproceso de representación tiene el depósito 0 del que lee. El hilo lógico utiliza el depósito 1 como fuente de entrada para el siguiente cuadro y escribe los datos del cuadro en el depósito 2.
En los puntos de sincronización, los índices de lo que significa cada uno de los tres cubos se intercambian para que los datos del siguiente cuadro se entreguen al hilo de renderizado y el hilo lógico pueda continuar hacia adelante.
Pero no hay necesariamente una razón para dividir el renderizado y la lógica en sus respectivos hilos. De hecho, puede mantener el ciclo del juego en serie y desacoplar su velocidad de cuadros de renderización del paso lógico utilizando la interpolación. Para aprovechar los procesadores multinúcleo que utilizan este tipo de configuración es donde tendría un grupo de subprocesos que funciona en grupos de tareas. Estas tareas pueden ser simplemente cosas como, en lugar de iterar una lista de objetos de 0 a 100, iterar la lista en 5 cubos de 20 en 5 hilos aumentando efectivamente su rendimiento pero sin complicar demasiado el ciclo principal.
fuente
Esta es una publicación antigua pero aún aparece, así que quería agregar mis 2 centavos aquí.
Primero, enumere los datos que deberían almacenarse en la interfaz de usuario / subproceso de visualización frente al subproceso lógico. En el subproceso de la interfaz de usuario, puede incluir malla 3D, texturas, información de luz y una copia de los datos de posición / rotación / dirección.
En el hilo de la lógica del juego, es posible que necesite el tamaño del objeto del juego en 3D, primitivas delimitadores (esfera, cubo), datos de malla 3D simplificados (para colisiones detalladas, por ejemplo), todos los atributos que afectan el movimiento / comportamiento, como la velocidad del objeto, la relación de giro, etc. y también datos de posición / rotación / dirección.
Si compara dos listas, puede ver que solo se necesita pasar una copia de los datos de posición / rotación / dirección de la lógica al hilo de la interfaz de usuario. Es posible que también necesite algún tipo de identificación de correlación para determinar a qué objeto del juego pertenecen estos datos.
Cómo lo hagas depende del idioma con el que estés trabajando. En Scala puede usar la memoria transaccional de software, en Java / C ++ algún tipo de bloqueo / sincronización. Me gustan los datos inmutables, por lo que tiendo a devolver un nuevo objeto inmutable para cada actualización. Esto es un poco de pérdida de memoria, pero con las computadoras modernas no es tan importante. Aún así, si desea bloquear las estructuras de datos compartidos, puede hacerlo. Echa un vistazo a la clase Exchanger en Java, usar dos o más buffers puede acelerar las cosas.
Antes de comenzar a compartir datos entre subprocesos, calcule cuántos datos realmente necesita pasar. Si tiene un octree que divide su espacio 3d, y puede ver 5 objetos del juego de un total de 10 objetos, incluso si su lógica necesita actualizar los 10, debe volver a dibujar solo los 5 que está viendo. Para más información, visite este blog: http://gameprogrammingpatterns.com/game-loop.html Esto no se trata de sincronización, pero muestra cómo la lógica del juego se separa de la pantalla y qué desafíos necesita superar (FPS). Espero que esto ayude,
marca
fuente