Estou escrevendo um shooter (como 1942, gráficos 2D clássicos) e gostaria de usar uma abordagem baseada em componentes. Até agora, pensei no seguinte design:
Cada elemento do jogo (dirigível, projétil, powerup, inimigo) é uma Entidade
Cada entidade é um conjunto de componentes que podem ser adicionados ou removidos em tempo de execução. Exemplos são: Posição, Sprite, Saúde, IA, Dano, Caixa delimitadora etc.
A idéia é que Dirigível, Projétil, Inimigo, Powerup NÃO são classes de jogos. Uma entidade é definida apenas pelos componentes que possui (e que podem mudar com o tempo). Assim, o dirigível do jogador começa com os componentes Sprite, Position, Health e Input. Uma ligação inicial possui a Sprite, a Posição, a BoundingBox. E assim por diante.
O loop principal gerencia o jogo "física", isto é, como os componentes interagem entre si:
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());
...
Os componentes são codificados no aplicativo principal do C ++. As entidades podem ser definidas em um arquivo XML (a parte IA em um arquivo lua ou python).
O loop principal não se importa muito com entidades: ele gerencia apenas componentes. O design do software deve permitir:
Dado um componente, obtenha a entidade à qual ele pertence
Dada uma entidade, obtenha o componente do tipo "type"
Para todas as entidades, faça algo
Para todos os componentes da entidade, faça algo (por exemplo: serializar)
Eu estava pensando no seguinte:
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;
Com esse design, posso obter os números 1, 2, 3 (graças aos algoritmos boost :: fusion :: map) e 4. Também está tudo O (1) (ok, não exatamente, mas ainda é muito rápido).
Há também uma abordagem mais "comum":
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
...
};
Outra abordagem é se livrar da classe Entity: cada tipo de componente vive em sua própria lista. Portanto, há uma lista de Sprite, uma lista de Saúde, uma lista de Danos etc. Eu sei que eles pertencem à mesma entidade lógica por causa do ID da entidade. Isso é mais simples, mas mais lento: os componentes de IA precisam acessar basicamente todos os componentes de outras entidades e isso exigiria uma pesquisa na lista de componentes de cada um a cada etapa.
Qual abordagem você acha que é melhor? O mapa boost :: fusion é adequado para ser usado dessa maneira?
fonte
Respostas:
Descobri que o design baseado em componentes e o orientado a dados andam de mãos dadas. Você diz que ter listas homogêneas de componentes e eliminar o objeto de entidade de primeira classe (em vez de optar por um ID de entidade nos próprios componentes) será "mais lento", mas isso não está aqui nem ali, pois você não definiu o perfil real de nenhum código real que implementa ambas as abordagens para chegar a essa conclusão. De fato, eu quase posso garantir que homogeneizar seus componentes e evitar a virtualização pesada tradicional será mais rápido devido às várias vantagens do design orientado a dados - paralelização mais fácil, utilização de cache, modularidade etc.
Não estou dizendo que essa abordagem seja ideal para tudo, mas os sistemas componentes, que são basicamente coleções de dados que precisam das mesmas transformações realizadas em cada quadro, simplesmente gritam para serem orientados a dados. Haverá momentos em que os componentes precisarão se comunicar com outros componentes de tipos diferentes, mas isso será um mal necessário de qualquer maneira. No entanto, ele não deve conduzir o design, pois existem maneiras de resolver esse problema, mesmo no caso extremo de todos os componentes serem processados em paralelo, como filas de mensagens e futuros .
Definitivamente, procure no Google o design orientado a dados, no que se refere a sistemas baseados em componentes, porque esse tópico aparece muito e há bastante discussão e dados anedóticos por aí.
fonte
se eu escrevesse esse código, preferiria usar esse método (e não estou usando nenhum impulso, se for importante para você), pois ele pode fazer tudo o que você deseja, mas o problema é quando há muitas entradas que não compartilham algum componente, encontrar aqueles que o possuem consumirá algum tempo. Fora isso, não há outro problema que eu possa pensar:
Nesta abordagem, cada componente é uma base para uma entidade, portanto, dado o componente que é ponteiro, também é uma entidade! a segunda coisa que você pede é ter um acesso direto aos componentes de algumas entidades, por exemplo. quando preciso acessar danos em uma das minhas entidades que uso
dynamic_cast<damage*>(entity)->value
, por isso, seentity
houver um componente de dano, ele retornará o valor. se você nãoentity
tiver certeza se tem ou não algum dano ao componente, é possível verificar facilmente oif (dynamic_cast<damage*> (entity))
valor de retornodynamic_cast
é sempre NULL se a conversão não for válida e o mesmo ponteiro, mas com o tipo solicitado, se for válido. Então, para fazer algo com tudo oentities
que tem alguns,component
você pode fazê-lo como abaixose houver outras perguntas, terei prazer em responder.
fonte
bool isActive
classe de componente base. há ainda a necessidade de introdução de componentes utilizáveis quando você está definindo enteties mas eu não considero isso como um problema, e ainda você tem atualizações componnent seprate (lembre-se somthing comodynamic_cast<componnet*>(entity)->update()
.