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?