oop - unreal - Comunicación en un motor de juego basado en componentes.
ubiart framework descargar (4)
Para un juego en 2D que estoy haciendo (para Android) estoy usando un sistema basado en componentes donde un GameObject tiene varios objetos GameComponent. Los GameComponents pueden ser cosas como componentes de entrada, componentes de renderización, componentes de emisión de bala, etc. Actualmente, los GameComponents tienen una referencia al objeto que los posee y pueden modificarlo, pero el propio GameObject solo tiene una lista de componentes y no le importa qué componentes sean, siempre que puedan actualizarse cuando se actualice el objeto.
A veces, un componente tiene cierta información que GameObject necesita conocer. Por ejemplo, para la detección de colisiones, un GameObject se registra en el subsistema de detección de colisiones para ser notificado cuando choca con otro objeto. El subsistema de detección de colisiones debe conocer el cuadro delimitador del objeto. Almaceno xey en el objeto directamente (porque lo usan varios componentes), pero el componente de representación que contiene el mapa de bits del objeto solo conoce el ancho y la altura. Me gustaría tener un método getBoundingBox o getWidth en GameObject que obtenga esa información. O, en general, quiero enviar alguna 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.
Puedo pensar en varias maneras de resolver este problema:
En lugar de tener una lista de componentes completamente genérica, puedo permitir que GameObject tenga un campo específico para 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 todavía 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.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 por defecto, pero un componente de representación puede establecerlos en los valores correctos en su ciclo de actualización. Esto se siente como un hack y podría terminar enviando muchas cosas a la clase GameObject para mayor comodidad, incluso si no todos los objetos las necesitan.
Haga que los componentes implementen una interfaz que indique para qué tipo de información se pueden consultar. Por ejemplo, un componente de representación implementaría la interfaz HasSize que incluye métodos como getWidth y getHeight. Cuando GameObject necesita el ancho, recorre sus componentes para verificar si implementan la interfaz HasSize (usando la palabra clave
instanceof
en Java, ois
en C #). Esto parece una solución más genérica, una desventaja es que la búsqueda del componente puede llevar algún tiempo (pero la mayoría de los objetos solo tienen 3 o 4 componentes).
Esta pregunta no es sobre un problema específico. Aparece a menudo en mi diseño y me preguntaba cuál sería la mejor manera de manejarlo. El rendimiento es algo importante ya que se trata de 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?
Como han dicho otros, no siempre hay una respuesta correcta aquí. Diferentes juegos se prestarán hacia diferentes soluciones. Si está construyendo un juego complejo grande con muchos tipos diferentes de entidades, una arquitectura genérica más desacoplada con algún tipo de mensaje abstracto entre componentes puede valer la pena por el mantenimiento que obtiene. Para un juego más simple con entidades similares, puede tener más sentido simplemente empujar todo ese estado en GameObject.
Para su situación específica en la que necesita almacenar el cuadro delimitador en algún lugar y solo el componente de colisión se preocupa por él, yo:
- Almacénelo en el mismo componente de colisión.
- Hacer que el código de detección de colisión funcione con los componentes directamente.
Entonces, en lugar de tener el motor de colisión iterar a través de una colección de GameObjects para resolver la interacción, hágalo iterar directamente a través de una colección de CollisionComponents. Una vez que se haya producido una colisión, dependerá del componente empujar a su GameObject principal.
Esto te da un par de beneficios:
- Deja el estado específico de colisión fuera de GameObject.
- Le evita tener que iterar sobre GameObjects que no tienen componentes de colisión. (Si tienes muchos objetos no interactivos como efectos visuales y decoración, esto puede ahorrar un número decente de ciclos).
- Le evita tener que quemar ciclos caminando entre el objeto y su componente. Si
getCollisionComponent()
los objetos, entoncesgetCollisionComponent()
en cada uno de ellos, ese puntero puede causar una falta de caché. Hacer eso por cada fotograma para cada objeto puede quemar una gran cantidad de CPU.
Si está interesado, tengo más información sobre este patrón here , aunque parece que ya entiende la mayor parte de lo que hay en ese capítulo.
Obtenemos variaciones sobre esta pregunta tres o cuatro veces a la semana en GameDev.net (donde el objeto de juego generalmente se llama una ''entidad'') y hasta ahora no hay consenso sobre el mejor enfoque. Sin embargo, se ha demostrado que varios enfoques diferentes son viables, por lo que no me preocuparía demasiado.
Sin embargo, generalmente los problemas se refieren a la comunicación entre componentes. Rara vez la gente se preocupa por obtener información de un componente a la entidad: si una entidad sabe qué información necesita, es probable que sepa exactamente a qué tipo de componente necesita acceder y a qué propiedad o método necesita recurrir para obtener ese componente. los datos. Si necesita ser reactivo en lugar de activo, registre las devoluciones de llamada o configure un patrón de observador con los componentes para que la entidad sepa cuándo ha cambiado algo en el componente y lea el valor en ese punto.
Los componentes completamente genéricos son en gran medida inútiles: necesitan proporcionar algún tipo de interfaz conocida, de lo contrario, no tienen mucho sentido. De lo contrario, también puede tener una gran variedad 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 ofrecerle una forma más genérica de usar subclases específicas sin tener que codificar información de tipo e interfaz en los componentes.
En cuanto a la comunicación:
Algunas personas están asumiendo que una entidad siempre contendrá un conjunto conocido de tipos de componentes (donde cada instancia es una de varias subclases posibles) y, por lo tanto, solo puede tomar una referencia directa al otro componente y leer / escribir a través de su interfaz pública.
Algunas personas están utilizando publicación / suscripción, señales / ranuras, etc., para crear conexiones arbitrarias entre componentes. Esto parece un poco más flexible pero, en última instancia, aún necesita algo con el 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 sistema de pizarra en AI) en la que cada uno de los componentes puede leer y escribir. Por lo general, esto requiere cierta robustez ante ciertas propiedades que no existen cuando usted esperaba que lo hicieran. Tampoco se presta para el paralelismo, aunque dudo que sea una preocupación masiva en un pequeño sistema integrado ...
Finalmente, algunas personas tienen sistemas donde la entidad no existe en absoluto. Los componentes viven dentro de sus subsistemas y la única noción de una entidad es un valor de ID en ciertos componentes: si un componente de Rendering (dentro del sistema de Rendering) y un componente de Player (dentro del sistema de Players) tienen la misma ID, entonces puede asumir que el primero se encarga del dibujo del segundo. Pero no hay ningún objeto único que agregue cualquiera de esos componentes.
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 entorno, tiene que encontrar un método para proporcionar esta información.
En su forma más simple, tiene conexiones uno a uno entre componentes, pero también necesitará conexiones uno a varios. Por ejemplo, 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 su solución 3), espere que las conexiones entre los componentes se conecten solo una vez y no se verifiquen en cada iteración del bucle del juego.
El marco de extensibilidad administrada para .NET es una buena solución para 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.
Utilice un " bus de eventos ". (tenga en cuenta que probablemente no pueda usar el código tal como está, pero debería darle una idea básica).
Básicamente, cree un recurso central donde cada objeto pueda registrarse como oyente y diga "Si X ocurre, quiero saber". Cuando algo sucede en el juego, el objeto responsable puede simplemente enviar un evento X al bus de eventos y todas las partes interesantes lo notarán.
[EDITAR] Para una discusión más detallada, vea el paso del mensaje (gracias a snk_kid por señalarlo).