"The Game Object" - y diseño basado en componentes

25

He estado trabajando en algunos proyectos de pasatiempos en los últimos 3-4 años. Solo juegos simples en 2d y 3d. Pero últimamente he comenzado un proyecto más grande. Así que en los últimos meses he estado tratando de diseñar una clase de objeto de juego que pueda ser la base de todos mis objetos de juego. Entonces, después de muchas pruebas de probar y morir, recurrí a Google, que rápidamente me señaló algunos PDF y PowerPoints de GDC. Y ahora estoy tratando de entender los objetos de juego basados ​​en componentes.

Entiendo que el motor crea un objeto de juego y luego une diferentes componentes que manejan cosas como la salud, la física, las redes y lo que sea que hagas que hagan. Pero lo que no entiendo es cómo el componente X sabe si Y ha cambiado el estado del objeto. ¿Cómo sabe el componente de física si el jugador está vivo, porque la salud está controlada por el componente de salud? ¿Y cómo juega HealthComponent la "animación de jugador muerto"?

Tenía la impresión de que era algo así (en el componente de salud):

if(Health < 0) {
   AnimationComponent.PlayAnimation("played-died-animation")
}

Pero, de nuevo, ¿cómo sabe HealthComponent que el objeto del juego al que está conectado tiene un AnimationComponent adjunto? La única solución que veo aquí es

  1. Verifique si hay un AnimationComponent adjunto o no (ya sea dentro del código del componente o en el lado del motor)

  2. Tener componentes requiere otros componentes, pero eso parece luchar contra todo el diseño de componentes.

  3. Escriba como, HealthWithAnimationComponent, HealthNoAnimationComponent, y así sucesivamente, lo que nuevamente parece luchar contra toda la idea del diseño de componentes.

hayer
fuente
1
Me encanta la pregunta Debería haber preguntado lo mismo hace meses, pero nunca lo hice. Un problema adicional que he enfrentado es cuando un objeto del juego tiene múltiples instancias del mismo componente (múltiples animaciones, por ejemplo). Sería genial si las respuestas pudieran tocar eso. Terminé usando mensajes para notificaciones, con variables compartidas entre todos los componentes de un objeto Juego (por lo que no necesitan enviar un mensaje para obtener un valor para una variable).
ADB
1
Dependiendo del tipo de juego, es probable que no tenga objetos de juego que tengan un componente de salud y ningún componente de animación. Y todos estos objetos de juego son probablemente una representación de algo como Unit. Por lo tanto, puede descartar el componente de salud y crear UnitComponent que tenga salud de campo y conozca todos los componentes que la unidad debe ser. Esta granularidad de componentes realmente no ayuda en nada: es más realista tener un componente por dominio (renderizado, audio, física, lógica de juego).
Kikaimaru

Respuestas:

11

En todos sus ejemplos, hay un problema terrible. El componente de salud necesita saber acerca de cada tipo de componente que pueda necesitar responder a la muerte de la entidad. Por lo tanto, ninguno de sus escenarios es apropiado. Su entidad tiene un componente de salud. Tiene un componente de animación. Ni dependen ni conocen al otro. Se comunican a través de un sistema de mensajería.

Cuando el componente de salud detecta que la entidad ha "muerto", envía un mensaje "He muerto". Es responsabilidad del componente de animación responder a este mensaje reproduciendo la animación apropiada.

El componente de salud no envía el mensaje directamente al componente de animación. Tal vez lo transmite a todos los componentes de esa entidad, tal vez a todo el sistema; tal vez el componente de animación necesita informarle al sistema de mensajería que está interesado en los mensajes 'Morí'. Hay muchas formas de implementar el sistema de mensajería. Independientemente de cómo lo implemente, el punto es que el componente de salud y el componente de animación nunca necesitan saber o preocuparse si el otro está presente, y agregar nuevos componentes nunca requerirá modificar los existentes para enviarles los mensajes apropiados.

Blecki
fuente
Okey, esto tiene sentido. Pero, ¿quién declara los "estados" como 'muertos' o 'portal-está-roto', etc. El componente o el motor? Porque agregar un estado 'muerto' a algo que nunca va a tener el componente de salud adjunto me parece un desperdicio. Supongo que me sumergiré y comenzaré a probar un código y veré qué funciona.
hayer
Michael y Patrick Hughes tienen la respuesta correcta arriba. Los componentes son solo datos; así que no es realmente el componente de salud el que detecta cuándo la entidad ha muerto y envía el mensaje, es una parte de la lógica específica del juego de nivel superior. Cómo abstraer eso depende de usted. El estado real de la muerte nunca necesita ser almacenado en ningún lado. El objeto está muerto si su estado es <0, y el componente de estado puede encapsular ese bit de lógica de verificación de datos sin romper un "¡no comportamiento!" restricción si solo considera cosas que modifican el estado del componente como comportamiento.
Blecki
Por curiosidad, ¿cómo manejarías un Componente de Movimiento? Cuando detecta la entrada, necesita aumentar la velocidad en el Componente de posición. ¿Cómo se vería el mensaje?
Tips48
8

La forma en que Artemis resuelve el problema es no realizar el procesamiento dentro de los Componentes. Los componentes contienen solo los datos que necesitan. Los sistemas leen múltiples tipos de componentes y hacen cualquier procesamiento que sea necesario.

Entonces, en su caso, es posible que tenga un RenderSystem que lea el HealthComponent (y otros) y reproduzca las colas en las animaciones apropiadas. Separar los datos de las funciones de esta manera hace que sea más fácil mantener las dependencias adecuadamente administradas.

Miguel
fuente
Esto termina siendo una buena manera de manejar el problema: los componentes representan propiedades, mientras que los sistemas unen propiedades dispares y las usan para hacer el trabajo. Es un gran cambio del pensamiento tradicional de OOP y hace que la cabeza de algunas personas duela =)
Patrick Hughes
Okey, ahora estoy realmente perdido ... "Por el contrario, en un ES, si tienes 100 unidades en un campo de batalla, cada una representada por una Entidad, entonces tienes cero copias de cada método que se puede invocar en una unidad, porque Las entidades no contienen métodos. Tampoco los componentes contienen métodos. En cambio, tiene un sistema externo para cada aspecto, y ese sistema externo contiene todos los métodos que se pueden invocar en cualquier entidad que posea el componente que lo marca como compatible con este sistema." Bueno, ¿dónde se almacenan los datos en un componente de arma? Como rondas, etc. Si todas las entidades comparten el mismo componente.
hayer
1
Según tengo entendido, todas las entidades no comparten el mismo componente, cada entidad puede tener N instancias de componentes adjuntas. Luego, un sistema consulta el juego para obtener una lista de todas las entidades que tienen instancias de componentes que les interesan y luego realiza cualquier procesamiento en ellas
Jake Woods,
Esto simplemente mueve el problema. ¿Cómo sabe un sistema qué componentes usar? Un sistema también podría necesitar otros sistemas (el sistema StateMachine podría querer solicitar una animación, por ejemplo). Sin embargo, resuelve el problema de que la OMS posee los datos. De hecho, una implementación más simple sería tener un diccionario en el objeto del juego y cada sistema crea sus variables allí.
ADB
Realmente mueve el problema pero a un lugar que es más sostenible. Los sistemas tienen sus componentes relevantes cableados. Los sistemas pueden comunicarse entre sí a través de Componentes (StateMachine puede establecer un valor de componente que Animation lee para saber qué hacer (o podría disparar un Evento). El enfoque del diccionario suena como el Patrón de Propiedades que también puede funcionar. Lo bueno de Componentes es que las propiedades relacionadas se agrupan y se pueden verificar estáticamente. No hay errores extraños porque agregaste "Daño" en un lugar pero intentaste recuperarlo usando "Daño" en otro.
Michael
6

En su código, puede haber formas (las usé, posiblemente existen otras formas) para saber si el objeto cambió de estado:

  1. Enviar mensaje.
  2. Leer directamente los datos del componente.

1) Verifique si hay un AnimationComponent adjunto o no (ya sea dentro del código del componente o en el lado del motor)

Para esto utilicé: 1. Función HasComponent de GameObject, o 2. cuando adjuntas un componente puedes verificar las dependencias en alguna función de construcción, o 3. Si estoy seguro de que ese objeto tiene este componente, simplemente lo uso.

2) Tener componentes requieren otros componentes, pero eso parece luchar contra todo el diseño de componentes.

En algunos artículos que he leído, los componentes del sistema Ideal no dependen unos de otros, pero en la vida real no es así.

3) Escriba como, HealthWithAnimationComponent, HealthNoAnimationComponent, y así sucesivamente, lo que nuevamente parece luchar contra toda la idea del diseño de componentes.

Es una mala idea escribir tales componentes. En mi aplicación creé el componente de salud más independiente. Ahora estoy pensando en algún patrón de Observador que notifique a los suscriptores acerca de algún evento específico (por ejemplo, "hit", "heal", etc.). Entonces AnimationComponent debe decidir por sí mismo cuándo reproducir animación.

Pero cuando leí un artículo sobre CBES me impresionó, así que estoy muy feliz cuando uso CBES y descubro nuevas posibilidades.

Yevhen
fuente
1
Bueno, google.no/… @ diapositiva 16
hayer
@bobenko, envíe un enlace al artículo sobre CBES. También soy muy interesante;)
Edward83
1
Y lambdor.net/?p=171 @ bottom, este es un resumen de mi pregunta ¿Cómo se pueden definir diferentes funcionalidades en términos de componentes relativamente complejos, no elementales? ¿Cuáles son los componentes más elementales? ¿De qué manera los componentes elementales son diferentes de las funciones puras? ¿Cómo pueden los componentes existentes comunicarse automáticamente con nuevos mensajes de nuevos componentes? ¿Cuál es el punto de ignorar un mensaje que un componente no conoce? ¿Qué pasó con el modelo de entrada-proceso-salida después de todo?
hayer
1
Aquí hay una buena respuesta en CBES stackoverflow.com/a/3495647/903195. La mayoría de los artículos que he investigado provienen de esta respuesta. Comencé e inspiré con cowboyprogramming.com/2007/01/05/evolve-your-heirachy luego en Gems 5 (como recuerdo) había un buen artículo con ejemplos.
Yevhen
Pero, ¿qué pasa con otro concepto de programación funcional-reactiva? Para mí, esta pregunta todavía está abierta, pero puede ser para usted una buena dirección para las investigaciones.
Yevhen
3

Es como Michael, dice Patrick Hughes y Blecki. La solución para evitar simplemente mover el problema es abandonar la ideología que causa el problema en primer lugar.

Es menos OOD y más como programación funcional. Cuando comencé a experimentar con el diseño basado en componentes, descubrí este problema en el futuro. Busqué en Google un poco más y encontré que la "Programación reactiva funcional" era la solución.

Ahora mis componentes no son más que una colección de variables y campos que describen su estado actual. Luego tengo un montón de clases de "Sistema" que actualizan todos los componentes que son relevantes para ellos. La parte reactiva se logra ejecutando los sistemas en un orden bien definido. Esto garantiza que cualquier sistema que sea el siguiente en la línea para realizar su procesamiento y actualización, y cualquier componente y entidad que tenga la intención de leer y actualizar, siempre está trabajando en datos actualizados.

Sin embargo, de alguna manera, aún podría afirmar que el problema se ha movido una vez más. Porque, ¿qué sucede si no es sencillo en qué orden deben ejecutar sus sistemas? ¿Qué pasa si hay relaciones cíclicas y es solo cuestión de tiempo antes de que estés mirando un desastre de declaraciones if-else y switch? Es una forma implícita de mensajería, ¿no? A primera vista, creo que es un pequeño riesgo. Por lo general, las cosas se procesan en orden. Algo así como: Entrada del jugador -> Posiciones de la entidad -> Detección de colisión -> Lógica del juego -> Representación -> Comenzar de nuevo. En ese caso, tendría un sistema para cada uno, proporcionaría a cada sistema un método update () y luego los ejecutaría en secuencia en su bucle de juego.

uhmdown
fuente