Consejos sobre la vinculación entre el sistema de componentes de entidad en C ++

10

Después de leer algunos documentos sobre el sistema de entidad-componente, decidí implementar el mío. Hasta ahora, tengo una clase World que contiene las entidades y el administrador del sistema (sistemas), la clase Entity que contiene los componentes como std :: map y algunos sistemas. Tengo entidades como std :: vector en World. No hay problema hasta ahora. Lo que me confunde es la iteración de entidades, no puedo tener una mente clara sobre eso, así que todavía no puedo implementar esa parte. ¿Debería cada sistema tener una lista local de entidades que les interesan? ¿O debería simplemente iterar a través de las entidades en la clase Mundial y crear un bucle anidado para iterar a través de los sistemas y verificar si la entidad tiene los componentes en los que está interesado el sistema? Quiero decir :

for (entity x : listofentities) {
   for (system y : listofsystems) {
       if ((x.componentBitmask & y.bitmask) == y.bitmask)
             y.update(x, deltatime)
       }
 }

pero creo que un sistema de máscara de bits bloqueará un poco la flexibilidad en caso de incrustar un lenguaje de script. O tener listas locales para cada sistema aumentará el uso de memoria para las clases. Estoy terriblemente confundido

deniz
fuente
¿Por qué espera el enfoque de máscara de bits para obstaculizar los enlaces de script? Como comentario aparte, use referencias (const, si es posible) en los bucles for-each para evitar copiar entidades y sistemas.
Benjamin Kloster
usando una máscara de bits, por ejemplo, un int, contendrá solo 32 componentes diferentes. No estoy dando a entender que habrá más de 32 componentes, pero ¿y si los tengo? Tendré que crear otro int o 64bit int, no será dinámico.
deniz
Puede usar std :: bitset o std :: vector <bool>, dependiendo de si desea o no que sea dinámico en tiempo de ejecución.
Benjamin Kloster

Respuestas:

7

Tener listas locales para cada sistema aumentará el uso de memoria para las clases.

Es una compensación tradicional de espacio-tiempo .

Si bien iterar a través de todas las entidades y verificar sus firmas es directo al código, puede volverse ineficiente a medida que crece su número de sistemas: imagine un sistema especializado (que sea una entrada) que busque su entidad de interés probablemente única entre miles de entidades no relacionadas .

Dicho esto, este enfoque aún puede ser lo suficientemente bueno según tus objetivos.

Sin embargo, si le preocupa la velocidad, por supuesto, hay otras soluciones a considerar.

¿Debería cada sistema tener una lista local de entidades que les interesan?

Exactamente. Este es un enfoque estándar que debería brindarle un rendimiento decente y es razonablemente fácil de implementar. La sobrecarga de memoria es insignificante en mi opinión, estamos hablando de almacenar punteros.

Ahora, cómo mantener estas "listas de interés" puede no ser tan obvio. En cuanto al contenedor de datos, std::vector<entity*> targetsla clase interna del sistema es perfectamente suficiente. Ahora lo que hago es esto:

  • La entidad está vacía en la creación y no pertenece a ningún sistema.
  • Cada vez que agrego un componente a una entidad:

    • obtener su firma de bit actual ,
    • mapear el tamaño del componente al grupo mundial de tamaño de fragmento adecuado (personalmente uso boost :: pool) y asignar el componente allí
    • obtener la nueva firma de bits de la entidad (que es solo "firma de bits actual" más el nuevo componente)
    • iterar a través de todos los sistemas del mundo y si hay un sistema cuya firma no coincide con la firma actual de la entidad y coincide con la nueva firma, resulta obvio que deberíamos empujar hacia atrás el puntero a nuestra entidad allí.

          for(auto sys = owner_world.systems.begin(); sys != owner_world.systems.end(); ++sys)
                  if((*sys)->components_signature.matches(new_signature) && !(*sys)->components_signature.matches(old_signature)) 
                          (*sys)->add(this);
      

Eliminar una entidad es completamente análogo, con la única diferencia que eliminamos si un sistema coincide con nuestra firma actual (lo que significa que la entidad estaba allí) y no coincide con la nueva firma (lo que significa que la entidad ya no debería estar allí). )

Ahora puede estar considerando el uso de std :: list porque eliminar del vector es O (n), sin mencionar que tendría que cambiar una gran cantidad de datos cada vez que elimine del medio. En realidad, no tiene que hacerlo, ya que no nos importa procesar el pedido en este nivel, solo podemos llamar a std :: remove y vivir con el hecho de que en cada eliminación solo tenemos que realizar O (n) búsqueda de nuestro entidad a ser eliminada.

std :: list le daría O (1) eliminar pero en el otro lado tiene un poco de sobrecarga de memoria adicional. También recuerde que la mayoría de las veces procesará entidades y no las eliminará, y esto seguramente se hace más rápido usando std :: vector.

Si usted es muy crítico con el rendimiento, puede considerar incluso otro patrón de acceso a datos , pero de cualquier manera mantiene algún tipo de "listas de interés". Sin embargo, recuerde que si mantiene su API de Entity System lo suficientemente abstraída, no debería ser un problema mejorar los métodos de procesamiento de entidades de los sistemas si su tasa de fotogramas disminuye debido a ellos, por lo tanto, por ahora, elija el método que le resulte más fácil de codificar. luego perfile y mejore si es necesario.

Patryk Czachurski
fuente
5

Hay un enfoque que vale la pena considerar donde cada sistema posee los componentes asociados a sí mismo y las entidades solo se refieren a ellos. Básicamente, su Entityclase (simplificada) se ve así:

class Entity {
  std::map<ComponentType, Component*> components;
};

Cuando dice un RigidBodycomponente adjunto a un Entity, lo solicita de su Physicssistema. El sistema crea el componente y permite que la entidad mantenga un puntero sobre él. Su sistema entonces se ve así:

class PhysicsSystem {
  std::vector<RigidBodyComponent> rigidBodyComponents;
};

Ahora, esto puede parecer un poco intuitivo al principio, pero la ventaja radica en la forma en que los sistemas de entidades componentes actualizan su estado. A menudo, iterará a través de sus sistemas y solicitará que actualicen los componentes asociados.

for(auto it = systems.begin(); it != systems.end(); ++it) {
  it->update();
}

La fortaleza de tener todos los componentes que posee el sistema en la memoria contigua es que cuando su sistema itera sobre cada componente y lo actualiza, básicamente solo tiene que funcionar

for(auto it = rigidBodyComponents.begin(); it != rigidBodyComponents.end(); ++it) {
  it->update();
}

No tiene que iterar sobre todas las entidades que potencialmente no tienen un componente que necesitan actualizar y también tiene un potencial para un rendimiento de caché muy bueno porque todos los componentes se almacenarán contiguamente. Esta es una, si no la mayor ventaja de este método. A menudo tendrá cientos y miles de componentes en un momento dado, también podría intentar ser lo más eficiente posible.

En ese punto, sus Worldúnicos bucles a través de los sistemas y los llama updatesin necesidad de iterar entidades también. Es (en mi humilde opinión) un mejor diseño porque las responsabilidades de los sistemas son mucho más claras.

Por supuesto, hay una infinidad de diseños de este tipo, por lo que debe evaluar cuidadosamente las necesidades de su juego y elegir el más apropiado, pero como podemos ver aquí, a veces son los pequeños detalles de diseño los que pueden marcar la diferencia.

pwny
fuente
Buena respuesta, gracias. pero los componentes no tienen funciones (como update ()), solo datos. y el sistema procesa esos datos. Entonces, de acuerdo con su ejemplo, debería agregar una actualización virtual para la clase de componente y un puntero de entidad para cada componente, ¿estoy en lo cierto?
deniz
@deniz Todo depende de tu diseño. Si sus componentes no tienen ningún método sino solo datos, el sistema aún puede iterar sobre ellos y realizar las acciones necesarias. En cuanto a la vinculación de regreso a las entidades, sí, podría almacenar un puntero a la entidad propietaria en el componente mismo o hacer que su sistema mantenga un mapa entre los identificadores y las entidades del componente. Por lo general, sin embargo, desea que sus componentes sean lo más autónomos posible. Un componente que no sabe nada sobre su entidad matriz es ideal. Si necesita comunicación en esa dirección, prefiera eventos y similares.
pwny
Si dices que será mejor para la eficiencia, usaré tu patrón.
deniz
@deniz Asegúrate de que realmente perfiles tu código temprano y con frecuencia para identificar qué funciona y qué no funciona para tu motor en particular :)
pwny
está bien :) voy a hacer una prueba de estrés
deniz
1

En mi opinión, una buena arquitectura es crear una capa de componentes en las entidades y separar la administración de cada sistema en esta capa de componentes. Por ejemplo, el sistema lógico tiene algunos componentes lógicos que afectan a su entidad y almacena los atributos comunes que se comparten con todos los componentes de la entidad.

Después de eso, si desea administrar los objetos de cada sistema en diferentes puntos, o en un orden particular, es mejor crear una lista de componentes activos en cada sistema. Todas las listas de punteros que puede crear y administrar en los sistemas son menos de un recurso cargado.

superarce
fuente