Search code examples
c++inheritanceparent-childsubclassentity-component-system

C++ How to make an unordered_map with subclasses


I'm making a game engine and currently I'm working on Entity Component System. Here is some code for better understanding:

class Entity {
    public:
        ...
        void AddComponent(EntityComponent component, unsigned int id){
        m_Components.insert({id, component});}

        EntityComponent GetComponent(unsigned int id)
        {
        auto i = m_Components.find(id);
        ASSERT(i != m_Components->end()); // ASSERT is a custom macro
        return i->second;
        }

    private:
        ...
        std::unordered_map<unsigned int, EntityComponent> m_Components;
    };

Also a simple parent class EntityComponent, that deals only with IDs (I skipped some code, because it doesn't matter in this problem):

class EntityComponent {
    public:
        ...

    private:

        unsigned int m_EntityID;
        size_t m_PoolIndex;
    };

And it's children. One of them would be a TransformComponent:

class TransformComponent : public EntityComponent {

    public:

        TransformComponent(unsigned int entityID, unsigned int poolID = 0, glm::vec2 position = glm::vec2(1.0,1.0), glm::vec2 scale = glm::vec2(1.0,1.0), float rotation = 0) : EntityComponent(entityID, poolID),m_Rotation(rotation), m_Scale(scale), m_Position(position){}

        inline glm::vec2 GetPosition() { return m_Position; }
        inline glm::vec2 GetScale() { return m_Scale; }

    private:

        glm::vec2 m_Position;
        float m_Rotation;
        glm::vec2 m_Scale;

    };

So the problem is that I want to create an Entity, add a TransformComponent to it and use its functions GetPosition(), GetScale().

Creating an entity and adding TransformComponent works, but when I want to use GetComponent(id) it returns the parent class EntityComponent, so it means that I cannot use functions like GetPosition() etc.

How can I change the code, so I can add different children of EntityComponent to the unordered_map and receive them using their ID and use their public methods?


Solution

  • As mentioned above, the first problem is that you should store EntityComponent* or a smart pointer of EntityComponent to preserve derived object data from erasing when assigning to the base class. So, the container type would be one of:

    std::unordered_map<int, EntityComponent*>
    std::unordered_map<int, std::unique_ptr<EntityComponent>>
    std::unordered_map<int, std::shared_ptr<EntityComponent>> 
    

    Also, virtual destructor should be added to EntityComponent to properly delete the derived classes:

    class EntityComponent
    {
        virtual ~EntityComponent() = default;
        // other stuff ...     
    };
    

    When you can use dynamic_cast to check if EntityComponent is actually a TransformComponent:

    EntityComponent* ec = GetComponentById(id); // suppose that function returns TransformComponent or EntityComponent
    TransformComponent* tc = dynamic_cast<TransformComponent*>(ec);
    if (tc != nullptr)
    {
        std::cout << "tc is transform component" << std::endl;
        tc->GetPosition();
        tc->GetScale();
    }
    

    But probably for a game engine dynamic_cast can be slow in some cases. If you know all your classes which derive from EntityComponent, you can add Type field to check the actual object type and then cast to it using static_cast which works at compile time:

    enum class ComponentType
    {
        BASE, 
        TRANSFORM,
        // other types
    };
    
    class EntityComponent
    {
        ComponentType _type;
    public:
        inline ComponentType GetType() const { return this->_type; }
    
        inline EntityComponent(ComponentType type = ComponentType::BASE) : _type(type) { }
        virtual ~EntityComponent() = default;
        // other staff
    };
    
    class TransformComponent : public EntityComponent
    {
    public:
        inline TransformComponent() : EntityComponent(ComponentType::TRANSFORM) { }
        // other staff
    };
    
    int main()
    {
        EntityComponent* ec = GetComponentById(0);
        if (ec->GetType() == ComponentType::TRANSFORM)
        {
            std::cout << "ec is transform component" << std::endl;
            TransformComponent* tc = static_cast<TransformComponent*>(ec);
            tc->GetPosition();
            tc->GetScale();
        }
    }
    

    There are several ways to add TransformComponent to map as std::unique_ptr<EntityComponent>:

    std::unordered_map<int, std::unique_ptr<EntityComponent>> components;
    
    // emplace raw pointer
    int id = 0;
    components.emplace(id, new TransformComponent());
    
    // release unique_ptr of derived class and emplace
    auto tc = std::make_unique<TransformComponent>();
    components.emplace(id, tc.release());
    
    // create unique_ptr of base class and move it to map
    std::unique_ptr<EntityComponent> ec(new TransformComponent());
    components.insert(id, std::move(ec));