2010-07-05 16 views
19

Para un juego en 2D que estoy haciendo (para Android) Estoy usando un sistema basado en componentes donde un GameObject contiene varios objetos GameComponent. GameComponents puede ser elementos tales como componentes de entrada, componentes de renderizado, componentes de emisión de bala, etc. Actualmente, GameComponents tiene una referencia al objeto que los posee y puede modificarlo, pero GameObject solo tiene una lista de componentes y no le importa cuáles son los componentes, siempre y cuando puedan actualizarse cuando se actualice el objeto.Comunicación en el motor de juegos basado en componentes

A veces un componente tiene alguna información que el GameObject necesita saber. Por ejemplo, para la detección de colisiones, GameObject se registra con el subsistema de detección de colisiones para que se le notifique cuando choca con otro objeto. El subsistema de detección de colisión necesita conocer el cuadro delimitador del objeto. Guardo xey en el objeto directamente (porque lo utilizan varios componentes), pero el ancho y el alto solo se conocen por el componente de representación que contiene el mapa de bits del objeto. Me gustaría tener un método getBoundingBox o getWidth en el GameObject que obtenga esa información. O, en general, quiero enviar cierta información de un componente al objeto. Sin embargo, en mi diseño actual, GameObject no sabe qué componentes específicos tiene en la lista.

me ocurren varias maneras de resolver este problema:

  1. En lugar de tener una lista completa de los componentes genéricos, que puede permitir que el GameObject tiene campo específico de algunos de los componentes importantes. Por ejemplo, puede tener una variable miembro llamada renderingComponent; cada vez que necesito obtener el ancho del objeto solo uso renderingComponent.getWidth(). Esta solución aún permite una lista genérica de componentes, pero trata a algunos de ellos de manera diferente, y me temo que terminaré teniendo varios campos excepcionales ya que es necesario consultar más componentes. Algunos objetos ni siquiera tienen componentes de renderizado.

  2. Tenga la información requerida como miembros de GameObject pero permita que los componentes la actualicen. Por lo tanto, un objeto tiene un ancho y una altura que son 0 o -1 de forma predeterminada, pero un componente de representación puede establecerlos en los valores correctos en su ciclo de actualización. Esto parece un truco y podría terminar empujando muchas cosas a la clase GameObject para mayor comodidad, incluso si no todos los objetos las necesitan.

  3. Haga que los componentes implementen una interfaz que indique qué tipo de información se puede consultar. Por ejemplo, un componente de renderizado implementaría la interfaz HasSize que incluye métodos como getWidth y getHeight. Cuando GameObject necesita el ancho, recorre sus componentes comprobando si implementan la interfaz HasSize (usando la palabra clave instanceof en Java o is en C#). Esto parece una solución más genérica, una desventaja es que la búsqueda del componente puede llevar algo de tiempo (pero la mayoría de los objetos solo tienen 3 o 4 componentes).

Esta pregunta no se trata de un problema específico. Aparece a menudo en mi diseño y me preguntaba cuál es la mejor manera de manejarlo. El rendimiento es algo importante ya que es un juego, pero el número de componentes por objeto es generalmente pequeño (el máximo es 8).

La versión corta

En un sistema basado en componentes para un juego, ¿cuál es la mejor manera de pasar información de los componentes al objeto mientras se mantiene el diseño genérico?

Respuesta

15

Recibimos variaciones de esta pregunta tres o cuatro veces a la semana en GameDev.net (donde el objeto del juego se suele llamar una 'entidad') y hasta ahora no hay consenso sobre el mejor enfoque. Varios enfoques diferentes han demostrado ser factibles, sin embargo, así que no me preocuparía demasiado.

Sin embargo, generalmente los problemas se refieren a la comunicación entre componentes. Rara vez las personas se preocupan por obtener información de un componente para la entidad: si una entidad sabe qué información necesita, es de suponer que sabe exactamente a qué tipo de componente necesita acceder y qué propiedad o método necesita invocar para obtener ese componente. los datos. si necesita ser reactivo en lugar de activo, registre devoluciones de llamada o configure un patrón de observador con los componentes para informar a la entidad cuando algo en el componente ha cambiado, y lea el valor en ese punto.

Los componentes completamente genéricos son en gran parte inútiles: necesitan proporcionar algún tipo de interfaz conocida; de lo contrario, es poco probable que existan. De lo contrario, también podría tener una gran matriz asociativa de valores sin tipo y terminar con ella. En Java, Python, C# y otros lenguajes de nivel ligeramente superior a C++, puede usar la reflexión para darle una forma más genérica de usar subclases específicas sin tener que codificar el tipo y la información de la interfaz en los componentes mismos.

En cuanto a la comunicación:

Algunas personas están haciendo suposiciones de que una entidad siempre contendrá un conjunto conocido de los tipos de componentes (donde cada instancia es una de varias subclases posibles) y por lo tanto solo puede tomar una referencia directa a la otro componente y lectura/escritura a través de su interfaz pública.

Algunas personas están utilizando publicar/suscribir, señales/ranuras, etc., para crear conexiones arbitrarias entre los componentes. Esto parece un poco más flexible, pero en última instancia todavía necesita algo con conocimiento de estas dependencias implícitas. (Y si esto se conoce en tiempo de compilación, ¿por qué no usar el enfoque anterior?)

O bien, puede poner todos los datos compartidos en la propia entidad y usarlos como un área de comunicación compartida (tenuemente relacionada con el blackboard system en AI) que cada uno de los componentes puede leer y escribir. Esto generalmente requiere cierta solidez frente a ciertas propiedades que no existen cuando usted lo esperaba. Tampoco se presta para el paralelismo, aunque dudo que sea una gran preocupación en un pequeño sistema integrado ...

Finalmente, algunas personas tienen sistemas en los que la entidad no existe en absoluto. Los componentes viven dentro de sus subsistemas y la única noción de una entidad es un valor ID en ciertos componentes: si un componente de Rendering (dentro del sistema de Rendering) y un componente de Player (dentro del sistema Players) tienen la misma ID, entonces puede suponer el primero maneja el dibujo de este último. Pero no hay ningún objeto que agregue ninguno de esos componentes.

+0

Esperaba que esta fuera la respuesta; que hay muchos enfoques y que todos son viables en diferentes contextos. Tiene razón acerca de los sistemas completamente genéricos, estaba sobre-ingeniería y preocupándome por problemas que no existen. –

4

Utilice un "event bus". (tenga en cuenta que probablemente no pueda usar el código tal como está, pero debería darle la idea básica).

Básicamente, cree un recurso central donde cada objeto puede registrarse como un oyente y decir "Si sucede X, quiero saber". Cuando sucede algo en el juego, el objeto responsable simplemente puede enviar un evento X al autobús del evento y todas las partes interesadas lo notarán.

[EDITAR] Para una discusión más detallada, vea message passing (gracias a snk_kid para señalar esto).

+1

Suena como una arquitectura de paso de mensajes, como suele llamarse. –

+0

Este es probablemente el enfoque más genérico. Un poco exagerado para un juego pequeño como el mío, pero vale la pena considerarlo como una solución general. –

+0

Bueno, hace que el diseño sea bastante simple ya que es muy genérico y fácil de implementar (solo <50 líneas de código en Java, 20 en Python). Cuando su juego funciona, aún puede optimizar algunos bits, pero lo pone en marcha rápidamente. –

3

Un enfoque es inicializar un contenedor de componentes. Cada componente puede proporcionar un servicio y también puede requerir servicios de otros componentes. Dependiendo de su lenguaje de programación y su entorno, tiene que encontrar un método para proporcionar esta información.

En su forma más simple, tiene conexiones uno a uno entre los componentes, pero también necesitará conexiones uno a muchos. P.ej. el CollectionDetector tendrá una lista de componentes que implementan IBoundingBox.

Durante la inicialización, el contenedor conectará las conexiones entre los componentes, y durante el tiempo de ejecución no habrá ningún costo adicional.

Esto está cerca de la solución 3), esperamos que las conexiones entre los componentes estén cableadas solo una vez y no se verifiquen en cada iteración del ciclo del juego.

El Managed Extensibility Framework for .NET es una buena solución a este problema. Me doy cuenta de que tiene la intención de desarrollar en Android, pero aún puede obtener algo de inspiración de este marco.

+0

Esta es una buena idea. Es un enfoque muy genérico y las conexiones de cableado entre los componentes en la inicialización lo hacen eficiente.Parece demasiado complicado para un juego pequeño, pero lo consideraría si mi diseño se vuelve demasiado complejo o para proyectos más grandes. Gracias. –

10

Como han dicho otros, aquí no siempre hay una respuesta correcta. Diferentes juegos se prestarán a diferentes soluciones. Si está construyendo un gran juego complejo con muchos tipos diferentes de entidades, una arquitectura genérica más desacoplada con algún tipo de mensaje abstracto entre los componentes puede valer la pena el esfuerzo para la mantenibilidad que obtiene. Para un juego más simple con entidades similares, puede tener más sentido simplemente empujar todo ese estado hasta GameObject.

Para su escenario específico donde se necesita almacenar el cuadro de límite en alguna parte y sólo el componente de la colisión se preocupa por él, lo haría:

  1. tienda en el propio componente de colisión.
  2. Haga que el código de detección de colisiones funcione con los componentes directamente.

Así, en lugar de tener el motor iterate colisión a través de una colección de GameObjects para resolver la interacción, haga que la iteración directamente a través de una colección de CollisionComponents. Una vez que se produce una colisión, corresponderá al componente empujarla hacia su GameObject principal.

Esto le da un par de ventajas:

  1. Hojas estado de colisión específico de GameObject.
  2. Le evita iterar sobre GameObjects que no tienen componentes de colisión. (Si tiene muchos objetos no interactivos como efectos visuales y decoración, puede ahorrar una cantidad decente de ciclos).
  3. Le evita ciclos de quemaduras al caminar entre el objeto y su componente. Si repite los objetos a continuación, haga getCollisionComponent() en cada uno, que el seguimiento del puntero puede causar una falta de caché. Hacer eso para cada cuadro para cada objeto puede quemar una gran cantidad de CPU.

Si le interesa tengo más información sobre este patrón here, aunque parece que ya comprende la mayor parte de lo que está en ese capítulo.

+0

Me gusta mucho esta idea y la voy a usar para este escenario en particular. Estoy familiarizado con su libro/sitio y, de hecho, mi implementación se basa en ese capítulo (es por eso que lo llamo GameObject y no Entity, por ejemplo). Sé que hablas de comunicación de componentes allí, pero me preguntaba qué tan genérica debe ser la entidad cuando se comunica con sus componentes. Gracias por el maravilloso recurso (¡ya es hora de que agregue otro capítulo!). –

+0

Sí, lo sé! El libro lamentablemente está en pausa por un tiempo. Hace poco abandoné la industria de los videojuegos y me mudé por todo el país, así que tengo otras cosas en mente. Con suerte, volveré pronto. – munificent

+1

He estado reflexionando sobre el marco de la entidad/componente durante un par de años, ocasionalmente volviendo a intentarlo con un proyecto diferente cuando tengo el tiempo libre. Si hubiera leído ese capítulo en aquel entonces en vez de las descripciones más técnicas, podría haber estado mucho mejor :). Excelente lectura. Parece que tu respuesta es correcta: sube cosas muy comunes (por ejemplo, la posición), junta componentes que ya están acoplados conductualmente (por ejemplo, física y colisión) y activa algunos mensajes para desencadenantes simples de componentes cruzados. ¡Ahora estoy listo para darle otra oportunidad! – orlade

Cuestiones relacionadas