Cuándo / dónde actualizar componentes

10

En lugar de mis motores habituales de juegos pesados, estoy jugando con un enfoque más basado en componentes. Sin embargo, me resulta difícil justificar dónde dejar que los componentes hagan lo suyo.

Digamos que tengo una entidad simple que tiene una lista de componentes. Por supuesto, la entidad no sabe cuáles son estos componentes. Puede haber un componente presente que le da a la entidad una posición en la pantalla, otro podría estar allí para dibujar la entidad en la pantalla.

Para que estos componentes funcionen, tienen que actualizar cada cuadro, la forma más fácil de hacerlo es caminar sobre el árbol de la escena y luego, para cada entidad, actualizar cada componente. Pero algunos componentes pueden necesitar un poco más de administración. Por ejemplo, un componente que hace que una entidad sea colisionable debe ser administrado por algo que pueda supervisar todos los componentes colisionables. Un componente que hace que una entidad sea dibujable necesita que alguien supervise todos los demás componentes dibujables para determinar el orden de extracción, etc.

Entonces mi pregunta es, ¿dónde actualizo los componentes, cuál es una forma limpia de llevarlos a los gerentes?

He pensado en usar un objeto de administrador de singleton para cada uno de los tipos de componentes, pero eso tiene los inconvenientes habituales de usar un singleton, una forma de aliviar esto es un poco usando la inyección de dependencia, pero eso parece excesivo para este problema. También podría caminar sobre el árbol de la escena y luego reunir los diferentes componentes en listas usando algún tipo de patrón de observación, pero parece un poco inútil hacer cada cuadro.

Roy T.
fuente
1
¿Estás utilizando sistemas de alguna manera?
Asakeron
Los sistemas de componentes son la forma habitual de hacer esto. Personalmente, llamo a la actualización en todas las entidades, que llama a la actualización en todos los componentes, y tengo algunos casos "especiales" (como el administrador espacial para la detección de colisiones, que es estático).
cenizas999
Sistemas de componentes? Nunca he oído hablar de esos antes. Comenzaré a buscar en Google, pero agradecería cualquier enlace recomendado.
Roy T.
1
Los sistemas de entidades son el futuro del desarrollo MMOG es un gran recurso. Y, para ser honesto, siempre estoy confundido por estos nombres de arquitectura. La diferencia con el enfoque sugerido es que los componentes solo contienen datos y los sistemas los procesan. Esta respuesta también es muy relevante.
Asakeron
1
Escribí una publicación en un blog sobre este tema aquí: gamedevrubberduck.wordpress.com/2012/12/26/…
AlexFoxGill

Respuestas:

15

Sugeriría comenzar leyendo las 3 grandes mentiras de Mike Acton, porque violas dos de ellas. Lo digo en serio, esto cambiará la forma en que diseñas tu código: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html

Entonces, ¿qué violas?

Mentira # 3: el código es más importante que los datos

Hablas de la inyección de dependencia, que puede ser útil en algunos (y solo en algunos) casos, pero siempre debe sonar una gran alarma si la usas, ¡especialmente en el desarrollo de juegos! ¿Por qué? Porque es una abstracción a menudo innecesaria. Y las abstracciones en los lugares equivocados son horribles. Entonces tienes un juego. El juego tiene gestores para diferentes componentes. Los componentes están todos definidos. Así que haga una clase en algún lugar de su código de bucle principal del juego que "tenga" los gerentes. Me gusta:

private CollissionManager _collissionManager;
private BulletManager _bulletManager;

Dele algunas funciones getter para obtener cada clase de administrador (getBulletManager ()). Tal vez esta clase en sí misma sea Singleton o sea accesible desde una (de todos modos, probablemente tengas un Singleton Game central). No hay nada de malo con datos y comportamientos codificados bien definidos.

No cree un ManagerManager que le permita registrar Managers con una clave, que otras clases que desean utilizar el manager pueden recuperar con esa clave. Es un gran sistema y muy flexible, pero aquí se habla de un juego. Sabes exactamente qué sistemas hay en el juego. ¿Por qué pretender que no lo haces? Porque este es un sistema para las personas que piensan que el código es más importante que los datos. Dirán "El código es flexible, los datos simplemente lo llenan". Pero el código es solo información. El sistema que describí es mucho más fácil, más confiable, más fácil de mantener y mucho más flexible (por ejemplo, si el comportamiento de un gerente difiere del de otros gerentes, solo tiene que cambiar algunas líneas en lugar de reelaborar todo el sistema)

Mentira # 2: el código debe diseñarse en torno a un modelo del mundo

Entonces tienes una entidad en el mundo del juego. La entidad tiene varios componentes que definen su comportamiento. Entonces, crea una clase Entity con una lista de objetos Component y una función Update () que llama a la función Update () de cada Componente. ¿Derecho?

No :) Eso es diseñar alrededor de un modelo del mundo: tienes viñeta en tu juego, así que agregas una clase Bullet. Luego actualiza cada viñeta y pasa a la siguiente. Esto matará absolutamente su rendimiento y le brinda una base de código retorcida horrible con código duplicado en todas partes y sin estructuración lógica de código similar. (Consulte mi respuesta aquí para obtener una explicación más detallada de por qué el diseño OO tradicional apesta, o busque Diseño orientado a datos)

Echemos un vistazo a la situación sin nuestro sesgo OO. Queremos lo siguiente, ni más ni menos (tenga en cuenta que no hay ningún requisito para hacer una clase para entidad u objeto):

  • Tienes un montón de entidades
  • Las entidades se componen de una serie de componentes que definen el comportamiento de la entidad.
  • Desea actualizar cada componente del juego en cada cuadro, preferiblemente de forma controlada
  • Aparte de identificar los componentes como pertenecientes, no hay nada que la entidad misma deba hacer. Es un enlace / ID para un par de componentes.

Y veamos la situación. Su sistema de componentes actualizará el comportamiento de cada objeto en el juego en cada cuadro. Este es definitivamente un sistema crítico de su motor. ¡El rendimiento es importante aquí!

Si está familiarizado con la arquitectura de la computadora o el diseño orientado a datos, sabe cómo se logra el mejor rendimiento: memoria compacta y agrupando la ejecución del código. Si ejecuta fragmentos de código A, B y C como este: ABCABCABC, no obtendrá el mismo rendimiento que cuando lo ejecuta así: AAABBBCCC. Esto no es solo porque las instrucciones y el caché de datos se usarán de manera más eficiente, sino también porque si ejecuta todas las "A" una tras otra, hay mucho espacio para la optimización: eliminar el código duplicado, calcular previamente los datos que utilizan todas las "A", etc.

Entonces, si queremos actualizar todos los componentes, no los hagamos clases / objetos con una función de actualización. No llamemos a esa función de actualización para cada componente en cada entidad. Esa es la solución "ABCABCABC". Agrupemos todas las actualizaciones de componentes idénticos. Entonces podemos actualizar todos los componentes A, seguidos de B, etc. ¿Qué necesitamos para hacer esto?

Primero, necesitamos gerentes de componentes. Para cada tipo de componente en el juego, necesitamos una clase de administrador. Tiene una función de actualización que actualizará todos los componentes de ese tipo. Tiene una función de creación que agregará un nuevo componente de ese tipo y una función de eliminación que destruirá el componente especificado. Puede haber otras funciones auxiliares para obtener y establecer datos específicos de ese componente (por ejemplo: establecer el modelo 3D para Componente del modelo). Tenga en cuenta que el administrador es de alguna manera un cuadro negro para el mundo exterior. No sabemos cómo se almacenan los datos de cada componente. No sabemos cómo se actualiza cada componente. No nos importa, siempre y cuando los componentes se comporten como deberían.

Luego necesitamos una entidad. Podrías hacer de esto una clase, pero eso no es necesario. Una entidad no podría ser más que una ID entera única o una cadena hash (también un número entero). Cuando crea un componente para la Entidad, pasa la ID como argumento al Administrador. Cuando desee eliminar el componente, vuelva a pasar la ID. Puede haber algunas ventajas al agregar un poco más de datos a la Entidad en lugar de simplemente convertirlo en una ID, pero esas solo serán funciones auxiliares porque, como he enumerado en los requisitos, todos los componentes definen el comportamiento de la entidad. Sin embargo, es su motor, así que haga lo que tenga sentido para usted.

Lo que sí necesitamos es un Entity Manager. Esta clase generará ID únicos si usa la solución de solo ID, o puede usarse para crear / administrar objetos Entity. También puede mantener una lista de todas las entidades en el juego si lo necesitas. El Entity Manager podría ser la clase central de su sistema de componentes, almacenando las referencias a todos los ComponentManagers en su juego y llamando a sus funciones de actualización en el orden correcto. De esa manera, todo el bucle del juego tiene que hacer es llamar a EntityManager.update () y todo el sistema está muy bien separado del resto de su motor.

Esa es la vista panorámica, echemos un vistazo a cómo funcionan los gerentes de componentes. Esto es lo que necesitas:

  • Crear datos de componentes cuando se llama a create (entityID)
  • Eliminar datos de componentes cuando se llama a remove (entityID)
  • Actualice todos los datos de componentes (aplicables) cuando se llame a update () (es decir, no todos los componentes necesitan actualizar cada marco)

El último es donde define el comportamiento / lógica de los componentes y depende completamente del tipo de componente que está escribiendo. AnimationComponent actualizará los datos de animación en función del marco en el que se encuentre. DragableComponent actualizará solo un componente que está siendo arrastrado por el mouse. PhysicsComponent actualizará los datos en el sistema de física. Aún así, debido a que actualiza todos los componentes del mismo tipo de una vez, puede hacer algunas optimizaciones que no son posibles cuando cada componente es un objeto separado con una función de actualización que podría llamarse en cualquier momento.

Tenga en cuenta que todavía nunca he pedido la creación de una clase XxxComponent para contener datos de componentes. Eso depende de usted. ¿Te gusta el diseño orientado a datos? Luego, estructura los datos en matrices separadas para cada variable. ¿Te gusta el diseño orientado a objetos? (No lo recomendaría, todavía matará su rendimiento en muchos lugares). Luego cree un objeto XxxComponent que contendrá los datos de cada componente.

Lo mejor de los gerentes es la encapsulación. Ahora la encapsulación es una de las filosofías más horriblemente mal utilizadas en el mundo de la programación. Así es como debe usarse. Solo el administrador sabe qué datos del componente se almacenan, dónde y cómo funciona la lógica de un componente. Hay algunas funciones para obtener / establecer datos, pero eso es todo. Puede reescribir todo el administrador y sus clases subyacentes y, si no cambia la interfaz pública, nadie se da cuenta. ¿Cambió el motor de física? Simplemente reescriba PhysicsComponentManager y listo.

Luego hay una última cosa: comunicación e intercambio de datos entre componentes. Ahora esto es complicado y no existe una solución única para todos. Puede crear funciones get / set en los administradores para permitir, por ejemplo, que el componente de colisión obtenga la posición del componente de posición (es decir, PositionManager.getPosition (entityID)). Podrías usar un sistema de eventos. Podría almacenar algunos datos compartidos en la entidad (la solución más fea en mi opinión). Podría usar (esto se usa a menudo) un sistema de mensajería. ¡O use una combinación de múltiples sistemas! No tengo el tiempo ni la experiencia para entrar en cada uno de estos sistemas, pero google y stackoverflow search son tus amigos.

Mercado
fuente
Encuentro esta respuesta muy interesante. Solo una pregunta (espero que usted o alguien pueda responderme). ¿Cómo logra eliminar la entidad en un sistema basado en componentes DOD? Incluso Artemis usa Entity como clase, no estoy seguro de que sea muy malo.
Wolfrevo Kcats
1
¿Qué quieres decir con eliminarlo? ¿Te refieres a un sistema de entidad sin una clase de entidad? La razón por la cual Artemis tiene una Entidad es porque en Artemis, la clase Entidad administra sus propios componentes. En el sistema que propuse, las clases ComponentManager administran los componentes. Entonces, en lugar de necesitar una clase Entity, puede tener una ID entera única. Entonces, supongamos que tiene la entidad 254, que tiene un componente de posición. Cuando desee cambiar la posición, puede llamar a PositionCompMgr.setPosition (int id, Vector3 newPos), con 254 como parámetro id.
Martes
Pero, ¿cómo gestionas las identificaciones? ¿Qué sucede si desea eliminar un componente de una entidad para luego asignarlo a otro? ¿Qué sucede si desea eliminar una entidad y agregar una nueva? ¿Qué sucede si desea que un componente se comparta entre dos o más entidades? Estoy realmente interesado en esto.
Wolfrevo Kcats
1
EntityManager se puede usar para dar nuevas ID. También podría usarse para crear entidades completas basadas en plantillas predefinidas (por ejemplo, crear "EnemyNinja" que genera una nueva ID y crea todos los componentes que componen un ninja enemigo, como renderizable, colisión, IA, tal vez algún componente para el combate cuerpo a cuerpo , etc.) También puede tener una función removeEntity que llama automáticamente a todas las funciones de eliminación de ComponentManager. El ComponentManager puede verificar si tiene datos de componentes para la Entidad dada y, de ser así, eliminar esos datos.
Martes
1
¿Mover un componente de una entidad a otra? Simplemente agregue una función swapComponentOwner (int oldEntity, int newEntity) a cada ComponentManager. Todos los datos están allí en ComponentManager, todo lo que necesita es una función para cambiar a qué propietario pertenece. Cada ComponentManager tendrá algo así como un índice o un mapa para almacenar qué datos pertenecen a qué ID de entidad. Simplemente cambie la ID de la entidad de la antigua a la nueva. No estoy seguro de si compartir componentes es fácil en el sistema que pensé, pero ¿qué tan difícil puede ser? En lugar de un enlace de Datos de componente de ID de entidad <-> en la tabla de índice, hay varios.
Martes
3

Para que estos componentes funcionen, tienen que actualizar cada cuadro, la forma más fácil de hacerlo es caminar sobre el árbol de la escena y luego, para cada entidad, actualizar cada componente.

Este es el enfoque ingenuo típico para las actualizaciones de componentes (y no hay nada necesariamente malo en que sea ingenuo, si funciona para usted). Uno de los grandes problemas que realmente ha tocado: está operando a través de la interfaz del componente (por ejemplo IComponent), por lo que no sabe nada sobre lo que acaba de actualizar. Es probable que tampoco sepa nada sobre el orden de los componentes dentro de la entidad, por lo que

  1. es probable que actualice componentes de diferentes tipos con frecuencia (localidad de referencia de código deficiente, esencialmente)
  2. este sistema no se presta bien a las actualizaciones concurrentes porque no está en condiciones de identificar dependencias de datos y, por lo tanto, divide las actualizaciones en grupos locales de objetos no relacionados.

He pensado en usar un objeto de administrador de singleton para cada uno de los tipos de componentes, pero eso tiene los inconvenientes habituales de usar un singleton, una forma de aliviar esto es un poco usando la inyección de dependencia, pero eso parece excesivo para este problema.

Aquí no se necesita un singleton, por lo que debes evitarlo porque tiene los inconvenientes que mencionaste. La inyección de dependencia no es exagerada: el núcleo del concepto es que le pasa cosas que un objeto necesita a ese objeto, idealmente en el constructor. No necesita un marco DI pesado (como Ninject ) para esto: simplemente pase un parámetro adicional a un constructor en alguna parte.

Un renderizador es un sistema fundamental y probablemente sea compatible con la creación y administración de la vida útil de un grupo de objetos renderizables que corresponden a elementos visuales en su juego (sprites o modelos, probablemente). Del mismo modo, un motor de física probablemente tenga control de por vida sobre cosas que representan las entidades que pueden moverse en la simulación de física (cuerpos rígidos). Cada uno de esos sistemas relevantes debe poseer, en cierta capacidad, esos objetos y ser responsable de actualizarlos.

Los componentes que usa en su sistema de composición de entidad de juego deberían ser envoltorios alrededor de las instancias de esos sistemas de nivel inferior: su componente de posición podría simplemente envolver un cuerpo rígido, su componente visual simplemente envuelve un sprite o modelo renderizable, etc.

Luego, el sistema en sí que posee los objetos de nivel inferior es responsable de actualizarlos, y puede hacerlo de forma masiva y de una manera que le permita multiprocesar esa actualización si corresponde. Su ciclo de juego principal controla el orden bruto en el que se actualizan esos sistemas (primero la física, luego el renderizador o lo que sea). Si tiene un subsistema que no tiene control de por vida o de actualización sobre las instancias que distribuye, puede crear un contenedor simple para manejar la actualización de todos los componentes relevantes para ese sistema en bloque y decidir dónde colocarlos. actualizar en relación con el resto de las actualizaciones de su sistema (esto sucede a menudo, encuentro, con los componentes "script").

Este enfoque se conoce ocasionalmente como el enfoque del componente externo , si está buscando más detalles.


fuente