Criando um jogo baseado em componentes

16

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:

  1. Cada elemento do jogo (dirigível, projétil, powerup, inimigo) é uma Entidade

  2. 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:

  1. Dado um componente, obtenha a entidade à qual ele pertence

  2. Dada uma entidade, obtenha o componente do tipo "type"

  3. Para todas as entidades, faça algo

  4. 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?

Emiliano
fonte
2
por que um voto negativo? O que há de errado com esta pergunta?
Emiliano

Respostas:

6

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í.

Skyler York
fonte
o que você quer dizer com "orientado a dados"?
Emiliano
Há muitas informações no Google, mas aqui está um artigo decente que deve fornecer uma visão geral de alto nível, seguida de uma discussão relacionada aos sistemas de componentes: gamesfromwithin.com/data-oriented-design , gamedev. net / topic /…
Skyler York
Não posso concordar com tudo o que está escrito sobre o DOD, já que acho que ele não pode ser completo, quero dizer que apenas o DOD pode sugerir uma abordagem muito boa para armazenar dados, mas para chamar funções e procedimentos que você precisa para usar procedimentos ou procedimentos. OOP, quero dizer, o problema é como combinar esses dois métodos para obter o máximo benefício, tanto para desempenho quanto para facilidade de codificação, por exemplo. na estrutura, sugiro que haverá um problema de desempenho quando todas as entidades não compartilharem alguns componentes, mas ele pode ser facilmente resolvido usando o DOD, você só precisa criar matrizes diferentes para diferentes tipos de entidades.
Ali1S232 14/05
Isso não responde diretamente à minha pergunta, mas é muito informativo. Lembrei-me de algo sobre o Dataflows nos meus dias de universidade. É a melhor resposta até agora e "vence".
Emiliano
-1

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:

// declare components here------------------------------
class component
{
};

class health:public component
{
public:
    int value;
};

class boundingbox:public component
{
public :
    int left,right,top,bottom;
    bool collision(boundingbox& other)
    {
        if (left < other.right || right > other.left)
            if (top < other.bottom || bottom > other.top)
                return true;
        return false;
    }
};

class damage : public component
{
public:
    int value;
};

// declare enteties here------------------------------

class entity
{
    virtual int id() = 0;
    virtual int size() = 0;
};

class aircraft :public entity, public health,public boundingbox
{
    virtual int id(){return 1;}
    virtual int size() {return sizeof(*this);};
};

class bullet :public entity, public damage, public boundingbox
{
    virtual int id(){return 2;}
    virtual int size() {return sizeof(*this);};
};

int main()
{
    entity* gameobjects[3];
    gameobjects[0] = new aircraft;
    gameobjects[1] = new bullet;
    gameobjects[2] = new bullet;
    for (int i=0;i<3;i++)
        for(int j=0;j<3;j++)
            if (dynamic_cast<boundingbox*>(gameobjects[i]) && dynamic_cast<boundingbox*>(gameobjects[j]) &&
                dynamic_cast<boundingbox*>(gameobjects[i])->collision(*dynamic_cast<boundingbox*>(gameobjects[j])))
                if (dynamic_cast<health*>(gameobjects[i]) && dynamic_cast<damage*>(gameobjects[j]))
                    dynamic_cast<health*>(gameobjects[i])->value -= dynamic_cast<damage*>(gameobjects[j])->value;
}

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, se entityhouver um componente de dano, ele retornará o valor. se você não entitytiver certeza se tem ou não algum dano ao componente, é possível verificar facilmente o if (dynamic_cast<damage*> (entity))valor de retorno dynamic_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 o entitiesque tem alguns, componentvocê pode fazê-lo como abaixo

for (int i=0;i<enteties.size();i++)
    if (dynamic_cast<component*>(enteties[i]))
        //do somthing here

se houver outras perguntas, terei prazer em responder.

Ali1S232
fonte
por que eu recebi o voto negativo? o que havia de errado com a minha solução?
11111 Ali1S232
3
Sua solução não é realmente uma solução baseada em componentes, pois os componentes não são separados das suas classes de jogo. Todas as suas instâncias dependem da relação IS A (herança) em vez de uma relação HAS A (composição). Fazer isso da maneira de composição (as entidades abordam vários componentes) oferece muitas vantagens sobre um modelo de herança (que é geralmente o motivo pelo qual você usa componentes). Sua solução não oferece nenhum dos benefícios de uma solução baseada em componentes e apresenta algumas peculiaridades (herança múltipla etc.). Sem localidade de dados, sem atualização de componente separada. Nenhuma modificação de tempo de execução dos componentes.
void
Em primeiro lugar, a pergunta solicita à estrutura que cada instância do componente esteja relacionada apenas a uma entidade e você pode ativar e desativar componentes adicionando apenas uma bool isActiveclasse 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 como dynamic_cast<componnet*>(entity)->update().
Ali1S232
e eu concordo que ainda haverá um problema quando ele quiser ter um componente que possa compartilhar dados, mas considerando o que ele pediu, acho que não haverá um problema para isso, e novamente existem alguns truques para esse problema também, se você Quero que eu possa explicar.
21111 Ali1S232
Embora eu concorde que é possível implementá-lo dessa maneira, não acho que seja uma boa ideia. Seus designers não podem compor objetos eles mesmos, a menos que você tenha uma classe superior que herda todos os componentes possíveis. E embora você possa chamar a atualização em apenas um componente, ele não terá um bom layout de memória, em um modelo composto, todas as instâncias de componentes do mesmo tipo podem ser mantidas próximas à memória e iteradas sem falhas no cache. Você também conta com o RTTI, que geralmente é desativado nos jogos por motivos de desempenho. Um bom layout de objeto classificado corrige isso principalmente.
void