Como posso acessar corretamente os componentes em meus sistemas de componentes de entidade C ++?

18

(O que estou descrevendo é baseado neste design: o que é uma estrutura de sistema de entidades ? , role para baixo e você a encontrará)

Estou tendo problemas para criar um sistema de componente de entidade em C ++. Eu tenho minha classe Component:

class Component { /* ... */ };

Na verdade, é uma interface para outros componentes serem criados. Então, para criar um componente personalizado, eu apenas implementei a interface e adicionei os dados que serão usados ​​no jogo:

class SampleComponent : public Component { int foo, float bar ... };

Esses componentes são armazenados dentro de uma classe Entity, que fornece a cada instância de Entity um ID exclusivo:

class Entity {
     int ID;
     std::unordered_map<string, Component*> components;
     string getName();
     /* ... */
};

Os componentes são adicionados à entidade com hash no nome do componente (provavelmente essa não é uma ótima idéia). Quando adiciono um componente personalizado, ele é armazenado como um tipo de componente (classe base).

Agora, por outro lado, eu tenho uma interface de sistema, que usa uma interface de nó dentro. A classe Node é usada para armazenar alguns dos componentes de uma única entidade (como o Sistema não está interessado em usar todos os componentes da entidade). Quando o sistema precisa update(), ele só precisa percorrer os Nós armazenados, criados a partir de diferentes entidades. Então:

/* System and Node implementations: (not the interfaces!) */

class SampleSystem : public System {
        std::list<SampleNode> nodes; //uses SampleNode, not Node
        void update();
        /* ... */
};

class SampleNode : public Node {
        /* Here I define which components SampleNode (and SampleSystem) "needs" */
        SampleComponent* sc;
        PhysicsComponent* pc;
        /* ... more components could go here */
};

Agora o problema: digamos que eu construa o SampleNodes passando uma entidade para o SampleSystem. O SampleNode "verifica" se a entidade possui os componentes necessários para serem utilizados pelo SampleSystem. O problema aparece quando preciso acessar o componente desejado dentro da Entidade: o componente é armazenado em uma Componentcoleção (classe base), portanto, não consigo acessar o componente e copiá-lo para o novo nó. Resolvi o problema temporariamente convertendo-o Componentem um tipo derivado, mas queria saber se existe uma maneira melhor de fazer isso. Entendo se isso significaria redesenhar o que eu já tenho. Obrigado.

Federico
fonte

Respostas:

23

Se você estiver armazenando os Components em uma coleção todos juntos, deverá usar uma classe base comum como o tipo armazenado na coleção e, portanto, deverá converter para o tipo correto ao tentar acessar os Componentna coleção. Os problemas de tentar converter para a classe derivada errada podem ser eliminados pelo uso inteligente de modelos e da typeidfunção, no entanto:

Com um mapa declarado assim:

std::unordered_map<const std::type_info* , Component *> components;

uma função addComponent como:

components[&typeid(*component)] = component;

e um getComponent:

template <typename T>
T* getComponent()
{
    if(components.count(&typeid(T)) != 0)
    {
        return static_cast<T*>(components[&typeid(T)]);
    }
    else 
    {
        return NullComponent;
    }
}

Você não receberá um erro. Isso ocorre porque typeidretornará um ponteiro para as informações de tipo do tipo de tempo de execução (o tipo mais derivado) do componente. Como o componente é armazenado com essas informações de tipo, é fundamental que a conversão não possa causar problemas devido a tipos incompatíveis. Você também recebe a verificação do tipo de tempo de compilação no tipo de modelo, pois deve ser um tipo derivado de Component ou static_cast<T*>terá tipos incompatíveis com o unordered_map.

Porém, você não precisa armazenar os componentes de diferentes tipos na coleção comum. Se você abandonar a ideia de um s Entitycontendo Componente, em vez disso, possuir um Componentarmazenamento Entity(na realidade, provavelmente será apenas um ID inteiro), poderá armazenar cada tipo de componente derivado em sua própria coleção do tipo derivado, em vez de como o tipo base comum e encontre os Component"pertencentes a" an Entityatravés desse ID.

Essa segunda implementação é um pouco mais intuitiva de se pensar do que a primeira, mas provavelmente pode estar oculta como detalhes da implementação por trás de uma interface, para que os usuários do sistema não precisem se preocupar. Não vou comentar o que é melhor, pois realmente não usei o segundo, mas não vejo o uso de static_cast como um problema com uma garantia tão forte de tipos quanto a primeira implementação fornece. Observe que ele requer RTTI, que pode ou não ser um problema, dependendo da plataforma e / ou convicções filosóficas.

Chewy Gumball
fonte
3
Eu uso C ++ há quase 6 anos, mas toda semana eu aprendo algum novo truque.
Knight666 #
Obrigado por responder. Tentarei usar o primeiro método primeiro e, se for mais tarde, pensarei em uma maneira de usar o outro. Mas, o addComponent()método não precisaria ser também um modelo? Se eu definir a addComponent(Component* c), qualquer subcomponente que eu adicionar será armazenado em um Componentponteiro e typeidsempre se referirá à Componentclasse base.
Federico
2
Typeid lhe dará o tipo real do objeto que está sendo apontado, mesmo que o ponteiro é de uma classe base
Chewy Gumball
Eu realmente gostei da resposta do chewy, então tentei implementá-la no mingw32. Corri para o problema mencionado por fede rico, onde addComponent () armazena tudo como um componente porque typeid está retornando componente como o tipo para tudo. Alguém aqui mencionou que typeid deve fornecer o tipo real do objeto que está sendo apontado, mesmo que o ponteiro seja para uma classe base, mas acho que pode variar com base no compilador, etc. Alguém mais pode confirmar isso? Eu estava usando g ++ std = C ++ 11 mingw32 em janelas 7. I terminou apenas modificando getComponent () ser um molde, em seguida, salvo o tipo do que em th
shwoseph
Isso não é específico do compilador. Você provavelmente não teve a expressão correta como argumento para a função typeid.
Chewy Gumball
17

O Chewy está certo, mas se você estiver usando o C ++ 11, terá alguns novos tipos que poderá usar.

Em vez de usar const std::type_info*como chave no seu mapa, você pode usar std::type_index( consulte cppreference.com ), que é um invólucro ao redor do std::type_info. Porque você usaria isso? Na std::type_indexverdade, ele armazena o relacionamento com o std::type_infoponteiro, mas esse é um ponteiro a menos para você se preocupar.

Se você estiver realmente usando o C ++ 11, eu recomendaria armazenar as Componentreferências dentro de ponteiros inteligentes. Portanto, o mapa pode ser algo como:

std::map<std::type_index, std::shared_ptr<Component> > components

A adição de uma nova entrada pode ser feita assim:

components[std::type_index(typeid(*component))] = component

onde componenté do tipo std::shared_ptr<Component>. A recuperação de uma referência a um determinado tipo Componentpode ser semelhante a:

template <typename T>
std::shared_ptr<T> getComponent()
{
    std::type_index index(typeid(T));
    if(components.count(std::type_index(typeid(T)) != 0)
    {
        return static_pointer_cast<T>(components[index]);
    }
    else
    {
        return NullComponent
    }
}

Observe também o uso de em static_pointer_castvez de static_cast.

vijoc
fonte
1
Na verdade, estou usando esse tipo de abordagem em meu próprio projeto.
vijoc
Isso é bastante conveniente, pois eu tenho aprendido C ++ usando o padrão C ++ 11 como referência. Uma coisa que notei, porém, é que todos os sistemas de componentes de entidades que encontrei na Web usam algum tipo de cast. Estou começando a pensar que seria impossível implementar isso ou um design de sistema semelhante sem lançamentos.
Federico
@Fede O armazenamento de Componentponteiros em um único contêiner exige necessariamente convertê- los no tipo derivado. Mas, como Chewy apontou, você tem outras opções disponíveis, que não requerem transmissão. Eu mesmo não vejo nada de "ruim" em ter esse tipo de conversão no design, pois é relativamente seguro.
vijoc
@vijoc Às vezes são considerados ruins devido ao problema de coerência de memória que podem apresentar.
akaltar