Estoy escribiendo un tirador (como 1942, gráficos 2D clásicos) y me gustaría usar un enfoque basado en componentes. Hasta ahora pensé en el siguiente diseño:
Cada elemento del juego (dirigible, proyectil, powerup, enemigo) es una entidad
Cada entidad es un conjunto de componentes que pueden agregarse o eliminarse en tiempo de ejecución. Los ejemplos son Posición, Sprite, Salud, IA, Daño, BoundingBox, etc.
La idea es que Airship, Projectile, Enemy, Powerup NO sean clases de juego. Una entidad solo se define por los componentes que posee (y que pueden cambiar con el tiempo). Entonces, la aeronave del jugador comienza con los componentes Sprite, Posición, Salud y Entrada. Un powerup tiene Sprite, Position, BoundingBox. Y así.
El bucle principal gestiona la "física" del juego, es decir, cómo interactúan los componentes entre sí:
foreach(entity (let it be entity1) with a Damage component)
foreach(entity (let it be entity2) with a Health component)
if(the entity1.BoundingBox collides with entity2.BoundingBox)
{
entity2.Health.decrease(entity1.Damage.amount());
}
foreach(entity with a IA component)
entity.IA.update();
foreach(entity with a Sprite component)
draw(entity.Sprite.surface());
...
Los componentes están codificados en la aplicación principal de C ++. Las entidades se pueden definir en un archivo XML (la parte IA en un archivo lua o python).
El bucle principal no se preocupa mucho por las entidades: solo administra componentes. El diseño del software debe permitir:
Dado un componente, obtenga la entidad a la que pertenece
Dada una entidad, obtiene el componente de tipo "tipo"
Para todas las entidades, haz algo
Para todos los componentes de la entidad, haga algo (por ejemplo: serializar)
Estaba pensando en lo siguiente:
class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component {...};
class Position : public Component {...};
class IA : public Component {... virtual void update() = 0; };
// I don't remember exactly the boost::fusion map syntax right now, sorry.
class Entity
{
int id; // entity id
boost::fusion::map< pair<Sprite, Sprite*>, pair<Position, Position*> > components;
template <class C> bool has_component() { return components.at<C>() != 0; }
template <class C> C* get_component() { return components.at<C>(); }
template <class C> void add_component(C* c) { components.at<C>() = c; }
template <class C> void remove_component(C* c) { components.at<C>() = 0; }
void serialize(filestream, op) { /* Serialize all componets*/ }
...
};
std::list<Entity*> entity_list;
Con este diseño puedo obtener # 1, # 2, # 3 (gracias a los algoritmos boost :: fusion :: map) y # 4. Además, todo es O (1) (ok, no exactamente, pero sigue siendo muy rápido).
También hay un enfoque más "común":
class Entity;
class Component { Entity* entity; ... virtual void serialize(filestream, op) = 0; ...}
class Sprite : public Component { static const int type_id = 0; };
class Position : public Component { static const int type_id = 1; };
class Entity
{
int id; // entity id
std::vector<Component*> components;
bool has_component() { return components[i] != 0; }
template <class C> C* get_component() { return dynamic_cast<C> components[C::id](); } // It's actually quite safe
...
};
Otro enfoque es deshacerse de la clase Entity: cada tipo de Componente vive en su propia lista. Entonces, hay una lista de Sprite, una lista de salud, una lista de daños, etc. Sé que pertenecen a la misma entidad lógica debido a la identificación de la entidad. Esto es más simple, pero más lento: los componentes de IA necesitan acceso básicamente a todos los componentes de la otra entidad y eso requeriría buscar en la lista de componentes de cada uno en cada paso.
¿Qué enfoque crees que es mejor? ¿El mapa boost :: fusion es adecuado para usarse de esa manera?
fuente
Respuestas:
Descubrí que el diseño basado en componentes y el diseño orientado a datos van de la mano. Usted dice que tener listas homogéneas de componentes y eliminar el objeto de entidad de primera clase (en lugar de optar por una ID de entidad en los componentes mismos) será "más lento", pero eso no es ni aquí ni allá ya que no ha perfilado ningún código real que implementa ambos enfoques para llegar a esa conclusión. De hecho, casi puedo garantizarle que homogeneizar sus componentes y evitar la virtualización pesada tradicional será más rápido debido a las diversas ventajas del diseño orientado a datos: paralelización más fácil, utilización de caché, modularidad, etc.
No digo que este enfoque sea ideal para todo, pero los sistemas de componentes que son básicamente colecciones de datos que necesitan las mismas transformaciones que se realizan en cada cuadro, simplemente gritan para estar orientados a los datos. Habrá momentos en que los componentes necesitan comunicarse con otros componentes de diferentes tipos, pero esto será un mal necesario de cualquier manera. Sin embargo, no debería impulsar el diseño, ya que hay formas de resolver este problema, incluso en el caso extremo de que todos los componentes se procesen en paralelo, como las colas de mensajes y futuros .
Definitivamente, Google busca un diseño orientado a datos en lo que se refiere a sistemas basados en componentes, porque este tema surge mucho y hay bastante discusión y datos anecdóticos.
fuente
si tuviera que escribir un código de este tipo, preferiría usar este enfoque (y no estoy usando ningún impulso si es importante para ti), ya que puede hacer todo lo que quieras, pero el problema es cuando hay demasiadas entretenciones que no comparten algunos componentes, encontrar los que los consuman llevará algún tiempo. Aparte de eso, no hay otro problema que pueda tener:
En este enfoque, cada componente es una base para una entidad, por lo que, dado el componente, su puntero también es una entidad. lo segundo que pide es tener acceso directo a los componentes de algunas entidades, por ejemplo. cuando necesito acceder a daños en una de mis entidades que uso
dynamic_cast<damage*>(entity)->value
, así que sientity
tiene un componente de daño, devolverá el valor. si no está seguro de sientity
tiene daños en los componentes o no, puede verificar fácilmente si elif (dynamic_cast<damage*> (entity))
valor de retornodynamic_cast
es siempre NULL si el lanzamiento no es válido y tiene el mismo puntero pero con el tipo solicitado si es válido. para hacer algo con todo loentities
que tienecomponent
, puede hacerlo como a continuaciónSi hay alguna otra pregunta, estaré encantado de responder.
fuente
bool isActive
clase commponente base. Todavía hay necesidad de introducir componentes utilizables cuando se están definiendo entretenimientos, pero no lo considero un problema, y todavía tiene actualizaciones complejas separadas (recuerde algo asídynamic_cast<componnet*>(entity)->update()
.