Tenía una pregunta sobre la arquitectura del juego: ¿Cuál es la mejor manera de que diferentes componentes se comuniquen entre sí?
Realmente me disculpo si esta pregunta ya se ha hecho un millón de veces, pero no puedo encontrar nada con exactamente el tipo de información que estoy buscando.
He estado tratando de construir un juego desde cero (C ++ si es importante) y he observado algún software de juegos de código abierto en busca de inspiración (Super Maryo Chronicles, OpenTTD y otros). Noté que muchos de estos diseños de juegos usan instancias globales y / o singletons en todo el lugar (para cosas como colas de renderizado, gestores de entidades, gestores de video, etc.). Intento evitar instancias globales y singletons y construir un motor que esté lo más acoplado posible, pero me encuentro con algunos obstáculos que se deben a mi inexperiencia en el diseño efectivo. (Parte de la motivación para este proyecto es abordar esto :))
He creado un diseño donde tengo un GameCore
objeto principal que tiene miembros que son análogos a las instancias globales que veo en otros proyectos (es decir, tiene un administrador de entrada, un administrador de video, un GameStage
objeto que controla todas las entidades y el juego) para cualquier etapa que esté cargada actualmente, etc.). El problema es que, dado que todo está centralizado en el GameCore
objeto, no tengo una manera fácil para que los diferentes componentes se comuniquen entre sí.
Mirando Super Maryo Chronicles, por ejemplo, cada vez que un componente del juego necesita comunicarse con otro componente (es decir, un objeto enemigo quiere agregarse a la cola de renderizado para ser dibujado en la etapa de renderizado), solo habla con el instancia global
Para mí, tengo que hacer que mis objetos del juego pasen información relevante al GameCore
objeto, para que el GameCore
objeto pueda pasar esa información a los otros componentes del sistema que la necesita (es decir, para la situación anterior, cada objeto enemigo pasaría su información de renderizado al GameStage
objeto, que lo recolectaría todo y se lo devolvería GameCore
, lo que a su vez lo pasaría al administrador de video para su renderizado). Parece un diseño realmente horrible, y estaba tratando de pensar en una resolución para esto. Mis pensamientos sobre posibles diseños:
- Instancias globales (diseño de Super Maryo Chronicles, OpenTTD, etc.)
- Hacer que el
GameCore
objeto actúe como un intermediario a través del cual se comunican todos los objetos (diseño actual descrito anteriormente) - Proporcione punteros de componentes a todos los demás componentes con los que necesitarán hablar (es decir, en el ejemplo de Maryo anterior, la clase enemiga tendría un puntero al objeto de video con el que necesita hablar)
- Divida el juego en subsistemas: por ejemplo, tenga objetos de administrador en el
GameCore
objeto que manejen la comunicación entre los objetos en su subsistema - (¿Otras opciones? ....)
Me imagino que la opción 4 anterior es la mejor solución, pero tengo algunos problemas para diseñarla ... tal vez porque he estado pensando en términos de los diseños que he visto que usan globales. Parece que estoy tomando el mismo problema que existe en mi diseño actual y lo estoy replicando en cada subsistema, solo que a una escala menor. Por ejemplo, el GameStage
objeto descrito anteriormente es un intento de esto, pero el GameCore
objeto todavía está involucrado en el proceso.
¿Alguien puede ofrecer algún consejo de diseño aquí?
¡Gracias!
fuente
Respuestas:
Algo que usamos en nuestros juegos para organizar nuestros datos globales es el patrón de diseño de ServiceLocator . La ventaja de este patrón en comparación con el patrón Singleton es que la implementación de sus datos globales puede cambiar durante el tiempo de ejecución de la aplicación. Además, sus objetos globales también se pueden cambiar durante el tiempo de ejecución. Otra ventaja es que es más fácil administrar el orden de inicialización de sus objetos globales, lo cual es muy importante especialmente en C ++.
por ejemplo (código C # que se puede traducir fácilmente a C ++ o Java)
Digamos que tiene una interfaz de back-end de representación que tiene algunas operaciones comunes para representar cosas.
Y que tiene la implementación de back-end de renderizado predeterminada
En algunos diseños parece legítimo poder acceder al backend de renderizado globalmente. En el patrón Singleton , eso significa que cada implementación de IRenderBackend debe implementarse como una instancia global única. Pero usar el patrón ServiceLocator no requiere esto.
Así es cómo:
Para poder acceder a su objeto global, primero debe inicializarlo.
Solo para demostrar cómo las implementaciones pueden variar durante el tiempo de ejecución, digamos que su juego tiene un minijuego donde el renderizado es isométrico e implementa un IsometricRenderBackend .
Cuando realiza la transición del estado actual al estado del minijuego, solo necesita cambiar el backend de representación global proporcionado por el localizador de servicios.
Otra ventaja es que también puede usar servicios nulos. Por ejemplo, si tuviéramos un ISoundManager servicio y el usuario desea desactivar el sonido, que pudimos implementar un NullSoundManager que no hace nada cuando se llama a sus métodos, por lo que mediante el establecimiento de la ServiceLocator 's objeto de servicio a un NullSoundManager objeto que podríamos lograr este resultado con casi ninguna cantidad de trabajo.
Para resumir, a veces puede ser imposible eliminar datos globales, pero eso no significa que no pueda organizarlos correctamente y de una manera orientada a objetos.
fuente
std::unique_ptr<ISomeService>
.Hay muchas formas de diseñar un motor de juego y todo se reduce a preferencias.
Para sacar lo básico del camino, algunos desarrolladores prefieren diseñarlo como una pirámide en la que hay una clase principal superior a la que se suele denominar clase kernel, core o framework que crea, posee e inicializa una serie de subsistemas como como audio, gráficos, redes, física, IA y gestión de tareas, entidades y recursos. En general, estos subsistemas están expuestos a usted por esta clase de marco y generalmente pasaría esta clase de marco a sus propias clases como argumento de constructor cuando sea apropiado.
Creo que estás en el camino correcto con tu pensamiento de la opción # 4.
Tenga en cuenta cuando se trata de la comunicación en sí misma, que no siempre tiene que implicar una función directa llamada a sí misma. Hay muchas formas indirectas en que puede ocurrir la comunicación, ya sea a través de algún método indirecto usando
Signal and Slots
o usandoMessages
.A veces, en los juegos, es importante permitir que las acciones ocurran de forma asincrónica para mantener nuestro bucle de juego en movimiento lo más rápido posible para que las velocidades de cuadros sean fluidas a simple vista. A los jugadores no les gustan las escenas lentas y entrecortadas, por lo que tenemos que encontrar formas de mantener las cosas fluyendo para ellos, pero mantener la lógica fluyendo pero también bajo control y ordenada. Si bien las operaciones asincrónicas tienen su lugar, tampoco son la respuesta para cada operación.
Solo sepa que tendrá una combinación de comunicaciones sincrónicas y asincrónicas. Elija lo que sea apropiado, pero sepa que necesitará admitir ambos estilos entre sus subsistemas. Diseñar soporte para ambos le servirá en el futuro.
fuente
Solo debe asegurarse de que no haya dependencias inversas o cíclicas. Por ejemplo, si tiene una clase
Core
, y estaCore
tiene unLevel
, yLevel
tiene una lista deEntity
, entonces el árbol de dependencia debería verse así:Entonces, dado este árbol de dependencia inicial, nunca debería
Entity
depender deLevel
oCore
, yLevel
nunca debería depender deCore
. Si necesitaLevel
oEntity
tiene acceso a datos que están más arriba en el árbol de dependencias, debe pasarse como parámetro por referencia.Considere el siguiente código (C ++):
Usando esta técnica, puede ver que cada uno
Entity
tiene acceso alLevel
y el queLevel
tiene acceso alCore
. Observe que cada unoEntity
almacena una referencia al mismoLevel
, desperdiciando memoria. Al darse cuenta de esto, debe preguntarse si cada unoEntity
realmente necesita acceso alLevel
.En mi experiencia, hay A) Una solución realmente obvia para evitar dependencias inversas, o B) No hay forma posible de evitar instancias globales y singletons.
fuente
Entonces, básicamente, ¿quieres evitar el estado mutable global ? Puede hacerlo local, inmutable o no ser un estado en absoluto. Esta última es más eficiente y flexible, en mi opinión. Se conoce como ocultación de la multiplicación.
fuente
La pregunta en realidad parece ser acerca de cómo reducir el acoplamiento sin sacrificar el rendimiento. Todos los objetos globales (servicios) generalmente forman una especie de contexto que es mutable durante el tiempo de ejecución del juego. En este sentido, el patrón del localizador de servicios dispersa diferentes partes del contexto en diferentes partes de la aplicación, lo que puede o no ser lo que desea. Otro enfoque del mundo real sería declarar una estructura como esta:
Y páselo como un puntero bruto no propietario
sEnvironment*
. Aquí los punteros apuntan a las interfaces, por lo que el acoplamiento se reduce de manera similar en comparación con el localizador de servicios. Sin embargo, todos los servicios están en un solo lugar (lo que podría o no ser bueno). Este es solo otro enfoque.fuente