¿Cómo puedo acceder correctamente a los componentes en mis sistemas de componentes de entidad C ++?

18

(Lo que describo se basa en este diseño: ¿Qué es un marco de sistema de entidad? Desplácese hacia abajo y lo encontrará)

Tengo algunos problemas para crear un sistema de entidad-componente en C ++. Tengo mi clase de componente:

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

Que en realidad es una interfaz para crear otros componentes. Entonces, para crear un componente personalizado, simplemente implemento la interfaz y agrego los datos que se usarán en el juego:

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

Estos componentes se almacenan dentro de una clase Entity, que le da a cada instancia de Entity un ID único:

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

Los componentes se agregan a la entidad haciendo un hash del nombre del componente (probablemente esta no sea una gran idea). Cuando agrego un componente personalizado, se almacena como un Tipo de componente (clase base).

Ahora, por otro lado, tengo una interfaz de sistema, que usa una interfaz de nodo dentro. La clase Node se usa para almacenar algunos de los componentes de una sola entidad (ya que el Sistema no está interesado en usar todos los componentes de la entidad). Cuando el sistema tiene que hacerlo update(), solo necesita iterar a través de los nodos que almacenó creados a partir de diferentes entidades. Entonces:

/* 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 */
};

Ahora el problema: digamos que construyo los SampleNodes pasando una entidad al SampleSystem. El SampleNode luego "verifica" si la entidad tiene los componentes necesarios para ser utilizados por el SampleSystem. El problema aparece cuando necesito acceder al componente deseado dentro de la Entidad: el componente se almacena en una Componentcolección (clase base), por lo que no puedo acceder al componente y copiarlo en el nuevo nodo. He resuelto temporalmente el problema al convertirlo Componenten un tipo derivado, pero quería saber si hay una mejor manera de hacerlo. Entiendo si esto significaría rediseñar lo que ya tengo. Gracias.

Federico
fuente

Respuestas:

23

Si va a almacenar los Components en una colección todos juntos, debe usar una clase base común como el tipo almacenado en la colección, y por lo tanto debe convertir al tipo correcto cuando intente acceder a los Components en la colección. typeidSin embargo, los problemas de intentar convertir a la clase derivada incorrecta se pueden eliminar mediante el uso inteligente de las plantillas y la función:

Con un mapa declarado así:

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

una función addComponent como:

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

y un getComponent:

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

No recibirá un error de difusión. Esto se debe a typeidque devolverá un puntero a la información de tipo del tipo de tiempo de ejecución (el tipo más derivado) del componente. Dado que el componente se almacena con esa información de tipo como clave, el reparto no puede causar problemas debido a tipos no coincidentes. También puede verificar el tipo de tiempo de compilación en el tipo de plantilla, ya que tiene que ser un tipo derivado de Componente o static_cast<T*>tendrá tipos no coincidentes con el unordered_map.

Sin embargo, no es necesario almacenar los componentes de diferentes tipos en una colección común. Si abandona la idea de una s que Entitycontiene Component, y en su lugar tiene cada Componenttienda un Entity(en realidad, probablemente será solo una ID entera), entonces puede almacenar cada tipo de componente derivado en su propia colección del tipo derivado en lugar de como tipo base común, y encuentre la Components "perteneciente a" a Entitytravés de esa ID.

Esta segunda implementación es un poco menos intuitiva para pensar que la primera, pero probablemente podría ocultarse como detalles de implementación detrás de una interfaz para que los usuarios del sistema no tengan que preocuparse. No comentaré cuál es mejor, ya que realmente no he usado el segundo, pero no veo el uso de static_cast como un problema con una garantía tan fuerte sobre los tipos como la primera implementación proporciona. Tenga en cuenta que requiere RTTI que puede o no ser un problema dependiendo de la plataforma y / o convicciones filosóficas.

Chemball masticable
fuente
3
He estado usando C ++ durante casi 6 años, pero cada semana aprendo un nuevo truco.
knight666
Gracias por responder. Intentaré usar el primer método primero, y si tal vez más tarde pensaré en una forma de usar el otro. Pero, ¿no addComponent()necesitaría el método ser también un método de plantilla? Si defino a addComponent(Component* c), cualquier subcomponente que agregue se almacenará en un Componentpuntero y typeidsiempre se referirá a la Componentclase base.
Federico
2
Typeid le dará el tipo real del objeto al que apunta, incluso si el puntero es de una clase base
Chewy Gumball
Realmente me gustó la respuesta de chewy, así que probé una implementación en mingw32. Me encontré con el problema mencionado por fede rico donde addComponent () almacena todo como un componente porque typeid está devolviendo el componente como el tipo para todo. Alguien aquí mencionó que typeid debería dar el tipo real del objeto al que se apunta, incluso si el puntero está en una clase base, pero creo que puede variar según el compilador, etc. ¿Alguien más puede confirmar esto? Estaba usando g ++ std = c ++ 11 mingw32 en Windows 7. Terminé simplemente modificando getComponent () para que fuera una plantilla, luego
guardé
Esto no es específico del compilador. Probablemente no tuvo la expresión correcta como argumento para la función typeid.
Chewy Gumball
17

Chewy tiene razón, pero si estás usando C ++ 11 tienes algunos tipos nuevos que puedes usar.

En lugar de usarlo const std::type_info*como clave en su mapa, puede usar std::type_index( consulte cppreference.com ), que es un contenedor alrededor del std::type_info. ¿Por qué lo usarías? En std::type_indexrealidad, almacena la relación con el std::type_infocomo puntero, pero ese es un puntero menos por el que debe preocuparse.

Si realmente está usando C ++ 11, recomendaría almacenar las Componentreferencias dentro de punteros inteligentes. Entonces el mapa podría ser algo como:

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

Agregar una nueva entrada podría hacerse así:

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

donde componentes de tipo std::shared_ptr<Component>. La recuperación de una referencia a un tipo determinado Componentpodría verse así:

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
    }
}

Tenga en cuenta también el uso de en static_pointer_castlugar de static_cast.

vijoc
fuente
1
De hecho, estoy usando este tipo de enfoque en mi propio proyecto.
vijoc
Esto es realmente bastante conveniente, ya que he estado aprendiendo C ++ usando el estándar C ++ 11 como referencia. Sin embargo, una cosa que he notado es que todos los sistemas de componentes de entidad que he encontrado en la web usan algún tipo de cast. Estoy empezando a pensar que sería imposible implementar esto, o un diseño de sistema similar sin moldes.
Federico
@Fede El almacenamiento de Componentpunteros en un solo contenedor requiere necesariamente convertirlos al tipo derivado. Pero, como señaló Chewy, tiene otras opciones disponibles, que no requieren lanzamiento. Yo mismo no veo nada "malo" en tener este tipo de modelos en el diseño, ya que son relativamente seguros.
vijoc
@vijoc A veces se los considera malos debido al problema de coherencia de memoria que pueden presentar.
akaltar