Adrian Albert Koch
Adrian Albert Koch

Reputation: 361

Implementing polymorphic components for entity-component system

Without polymorphism

I have implemented an entity-component system that useses templates to get the components. An id is generated for each type. The function size_t GetComponentTypeId<T>() will always return the same id for a given type T.

For better understanding, here the function to add components

template <typename TComponent, typename... TArguments>
inline TComponent & Entity::AddComponent(TArguments&&... arguments)
{
    // Check whether the component doesn't already exist
    assert(componentBitSet[detail::GetComponentTypeID<TComponent>()] == false && "The component already exists");
    assert(componentArray[detail::GetComponentTypeID<TComponent>()] == nullptr && "The component already exists");

    TComponent * c = new TComponent(*this, std::forward<TArguments>(arguments)...);

    Component::UPtr uPtr{ c };
    componentList.emplace_back(std::move(uPtr));


    // set the component * in the array
    componentArray[detail::GetComponentTypeID<TComponent>()] = c;
    // set the according component flag to true
    componentBitSet[detail::GetComponentTypeID<TComponent>()] = true;

    return *c;
}

And here the function to get components

template<typename TComponent>
inline TComponent & Entity::GetComponent() const
{
    Component * component = componentArray[getComponentTypeID<TComponent>()];

    if (component == nullptr)
        throw std::runtime_error("Entity: This entity does not have the requested component");

    return *static_cast<TComponent*>(component);
}

Nothing special here

Also my current implementation if the GetComponentTypeID() method:

namespace detail
{
    typedef std::size_t ComponentTypeID;

    /// @brief Returns a unique number (for each function call) of type std::size_t
    inline ComponentTypeID GetComponentID() noexcept
    {
        // This will only be initialised once
        static ComponentTypeID lastId = 0;

        // After the first initialisation a new number will be returned for every function call
        return lastId++;
    }

    /// @brief Returns a unique number (of type std::size_t) for each type T
    /// @details Each component type will have its own unique id.
    /// The id will be the same for every instance of that type
    /// @tparam T The type for which the id is generated
    template <typename T>
    inline ComponentTypeID GetComponentTypeID() noexcept
    {
        // There will be only one static variable for each template type
        static ComponentTypeID typeId = GetComponentID();
        return typeId;
    }
} // namespace detail

Adding polymorphism

Now I want to add polymorphic behaivour to my classes. E.g. there might be a SpriteRenderComponent which inherits from RenderComponent (which of course inherits Component). The RenderComponent would have a virtual draw method that is implemented in the SpriteRenderComponent. I want to be able to only add the sprite component and still be able to get the a reference to the renderComponent by calling entity.GetComponent<RenderComponent>() on the entity to which the sprite component has been added to. Calling the draw method on the returned render component reference should call SpriteRenderComponent.draw(). Also I should not be able to add any other components that inherit from render component.

Some of my thoughts

I think, the basic solution would be to add a pointer of the one instance of the SpriteRenderComponent for both ids; RenderComponent and SpriteRenderComponent. This would also prevent the user from adding multiple components that inherit from RenderComponent. The component itself would only be added once to the componentList and so only be updated once per frame (as wanted)

Problem: Making it typesafe

My problem is that I am struggeling to make it typesafe. Also I want to include some kind of check that makes sure that SpriteRenderComponent actually inherits from RenderComponent. My favourite solution would be one that 'automatically adds gets the superclass's ids and adds the component pointer for them as well. I am quite new to this kind of meta programming (perhaps the wrong word) so help would be much appreciated.

Update

One possible solution I found was to add an AddPolymorphism<class TDerivedComponent, class TBaseComponent>() method to the entity class. Here is the implementation:

template<class TDerivedComponent, class TBaseComponent>
    inline void Entity::AddPolymorphism()
    {
        // Needed since std::is_base_of<T, T> == true
        static_assert(std::is_base_of<Component, TBaseComponent>::value, "Entity: TBaseComponent must inherit from Component");
        static_assert(std::is_same<Component, TBaseComponent>::value == false, "Entity: TBaseComponent must inherit from Component");
        static_assert(std::is_base_of<TBaseComponent, TDerivedComponent>::value, "Entity: TDerivedComponent must inherit from TBaseComponent");
        static_assert(std::is_same<Component, TBaseComponent>::value == false, "Entity: TBaseComponent must inherit from Component");
        assert(this->HasComponent<TDerivedComponent>() && "Entity: The entity must have the derived component");

        auto derivedComponentPtr = componentDictionary.find(detail::GetComponentTypeID<TDerivedComponent>())->second.lock();
        componentDictionary.insert(std::make_pair(detail::GetComponentTypeID<TBaseComponent>(), derivedComponentPtr));
    }

I guess its kinda typesafe but for me it has one major issue. It requires me to call this function every single time I add an component that has polymorphic behaivour. Although this is a solution (kinda) I'd much prefer a static way to specify this behaivour.

Upvotes: 1

Views: 547

Answers (2)

Francis Cugler
Francis Cugler

Reputation: 7925

For the part about making sure it inherits from:

template<typename T>
struct Foo {
   static_assert(is_base_of<Base, T>::value, "T must inherit from Base");
};

Might help you out there; as for the other questions; I will need more time as I have to soon leave... I'll come back to this later on when I get the chance to update this answer.


EDIT - Added some additional classes and show their uses.

I've had some time to work on something; I don't know for sure if this is what you are looking for; but this is a storage-manager type system that I've used before. It does support polymorphic behavior of classes. So maybe this structure will help you out.

main.cpp

#include <iostream>
#include <string>
#include <memory>

#include "FooManager.h"
#include "DerivedFoos.h"

int main() {    
    try {
        std::unique_ptr<FooManager>  pFooManager;
        pFooManager.reset( new FooManager() );    

        for ( unsigned i = 0; i < 10; i++ ) {
            DerivedA* pA = new DerivedA();
            DerivedB* pB = new DerivedB();
            pFooManager->add( pA, FOO_A );
            pFooManager->add( pB, FOO_B );
        }    

        pFooManager.reset();

    } catch ( std::exception& e ) {
        std::cout << e.what() << std::endl;
        std::cout << "\nPress any key to quit.\n";
        std::cin.get();
        return -1;
    } catch ( std::string str ) {
        std::cout << str << std::endl;
        std::cout << "\nPress any key to quit.\n";
        std::cin.get();
        return -1;
    } catch ( ... ) {
        std::cout << __FUNCTION__ << " caught unknown exception." << std::endl;
        std::cout << "\nPress any key to quit.\n";
        std::cin.get();
        return -1;
    }    

    std::cout << "\nPress any key to quit.\n";
    std::cin.get();
    return 0;
}

FooBase.h

#ifndef FOO_BASE_H
#define FOO_BASE_H

enum FooTypes {
    FOO_A,
    FOO_B,

    FOO_UNKNOWN // MUST BE LAST!!!
};

class FooBase {
protected:
    std::string _nameAndId;
private:
    std::string _id;
    static int _baseCounter;

public:
    std::string idOfBase();
    virtual std::string idOf() const = 0;

protected:
    FooBase();    
};

#endif // !FOO_BASE_H

FooBase.cpp

#include "FooBase.h"
#include <iostream>
#include <string>    

int FooBase::_baseCounter = 0;

FooBase::FooBase() {
    _id = std::string( __FUNCTION__ ) + std::to_string( ++_baseCounter );
    std::cout << _id << " was created." << std::endl;
}

std::string FooBase::idOfBase() {
    return _id;
}

std::string FooBase::idOf() const {
    return "";
} // empty

DerivedFoos.h

#ifndef DERIVED_FOOS_H
#define DERIVED_FOOS_H

#include "FooBase.h"

class DerivedA : public FooBase {
private:    
    static int _derivedCounter;

public:
    DerivedA();

    std::string idOf() const override;
};

class DerivedB : public FooBase {
private:
    static int _derivedCounter;

public:
    DerivedB();

    std::string idOf() const override;
};

#endif // !DERIVED_FOOS_H

DerivedFoos.cpp

#include "DerivedFoos.h"
#include <iostream>
#include <string>

int DerivedA::_derivedCounter = 0;
int DerivedB::_derivedCounter = 0;

DerivedA::DerivedA() : FooBase() {
    _nameAndId = std::string( __FUNCTION__ ) + std::to_string( ++DerivedA::_derivedCounter );
    std::cout << _nameAndId << " was created." << std::endl;
}

std::string DerivedA::idOf() const {
    return _nameAndId;
}    

DerivedB::DerivedB() : FooBase() {
    _nameAndId = std::string( __FUNCTION__ ) + std::to_string( ++DerivedB::_derivedCounter );
    std::cout << _nameAndId << " was created." << std::endl;
}

std::string DerivedB::idOf() const {
    return _nameAndId;
}

FooManager.h - I'm not going to change the code for this class to replace its name. After looking at this for a little while; it became apparent that something like FooStore or Storage etc. would be a more suitable name for this class. It doesn't really manage anything other than the adding and removing of objects from its member container(s). You could keep its name as is if you decide to add more functionality that does more than just the add and remove objects.

#ifndef FOO_MANAGER_H
#define FOO_MANAGER_H

class FooBase;
class DerivedA;
class DerivedB;
enum FooTypes;

class FooManager final {
private:
    static bool _alreadyExists;

    typedef std::unordered_map<std::string, std::shared_ptr<FooBase>> MapFoos;
    MapFoos   _idsA;    
    MapFoos   _idsB;

    std::vector<std::string> _foosForRemoval;

public:
    FooManager();
    ~FooManager();

    // Foo Objects
    FooBase* getFoo( const std::string& id, FooTypes type ) const;
    void add( FooBase* foo, FooTypes type );
    bool removeFoo( const std::string& id );

    template<typename T>
    bool removeFoo( T* pFoo );  

    void markFooForRemoval( const std::string& id );

private:
    FooBase* getFoo( const std::string& id, const MapFoos& fooMap ) const;
    void     add( FooBase* pFoo, MapFoos& fooMap );
    bool     removeFoo( const std::string& strId, MapFoos& fooMap );

};

template<typename T>
inline bool FooManager::removeFoo( T* pFoo ) {
    return false;
}

#endif // !FOO_MANAGER_H

FooManager.cpp

#include "FooManager.h"
#include "DerivedFoos.h"

#include <iostream>
#include <sstream>
#include <string>
#include <unordered_map>
#include <memory>

bool FooManager::_alreadyExists = false;

FooManager::FooManager() {
    // First check if no other instance is created.
    if ( _alreadyExists ) {
        std::ostringstream strStream;
        strStream << "Failed to create " << __FUNCTION__ << " as it was already created." << std::endl;
        throw strStream.str();
    }

    // Make sure this is last
    _alreadyExists = true;

    std::cout << __FUNCTION__ + std::string( " was created successfully." ) << std::endl;
}

FooManager::~FooManager() {
    // If we are destroying make sure to reset flag
    // So it can be constructed again.
    _idsA.clear();
    _idsB.clear();

    _alreadyExists = false;

    std::cout << __FUNCTION__ + std::string( " was destroyed successfully." ) << std::endl;
}

FooBase* FooManager::getFoo( const std::string& id, FooTypes type ) const {
    switch ( type ) {
        case FOO_A: {
            return getFoo( id, _idsA );
        }
        case FOO_B: {
            return getFoo( id, _idsB );
        }
        default: {
            std::ostringstream strStream;
            strStream << __FUNCTION__ << " Unrecognized FooType = " << type;
            throw strStream.str();
        }
    }
    return nullptr;
}

FooBase* FooManager::getFoo( const std::string& id, const MapFoos& fooMap ) const {
    MapFoos::const_iterator itFoo = fooMap.find( id );
    if ( itFoo == fooMap.cend() ) {
        return nullptr;
    }
    return itFoo->second.get();
}

void FooManager::add( FooBase* pFoo, FooTypes type ) {
    // first check to see foo is valid
    if ( nullptr == pFoo ) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ + std::string( " pFoo == nullptr passed in" );
    }

    // Make Sure Name Is Unique Across All Foo Types
    for ( int i = 0; i < FOO_UNKNOWN; ++i ) {
        if ( getFoo( pFoo->idOf(), (FooTypes)i ) != nullptr ) {
            std::ostringstream strStream;
            strStream << __FUNCTION__ << " attempting to store " << pFoo->idOf() << " multiple times" << std::endl;
            throw strStream.str();
        }
    }

    switch ( type ) {
        case FOO_A: {
            add( pFoo, _idsA );
            break;
        }
        case FOO_B: {
            add( pFoo, _idsB );
            break;
        }
        default: {
            std::ostringstream strStream;
            strStream << __FUNCTION__ << " uncrecognized FooType = " << type;
        }
    }
}

void FooManager::add( FooBase* pFoo, MapFoos& fooMap ) {
    fooMap.insert( MapFoos::value_type( pFoo->idOf(), std::shared_ptr<FooBase>( pFoo ) ) );
}

template<>
bool FooManager::removeFoo( DerivedA* pFoo ) {
    return removeFoo( pFoo->idOf(), _idsA );
}

template<>
bool FooManager::removeFoo( DerivedB* pFoo ) {
    return removeFoo( pFoo->idOf(), _idsB );
}

bool FooManager::removeFoo( const std::string& id ) {
    // Find which type this Foo is in
    for ( int i = 0; i < FOO_UNKNOWN; ++i ) {
        FooBase* pFoo = getFoo( id, (FooTypes)i );
        if ( pFoo != nullptr ) {
            // Found It
            switch ( static_cast<FooTypes>(i) ) {
                case FOO_A: {
                    return removeFoo( pFoo->idOf(), _idsA );
                }
                case FOO_B: {
                    return removeFoo( pFoo->idOf(), _idsB );
                }
                default: {
                    std::ostringstream strStream;
                    strStream << __FUNCTION__ << " uncrecognized FooType = " << i;
                    throw strStream.str();
                }
            }
        }
    }

    std::ostringstream strStream;
    strStream << __FUNCTION__ << " failed. " << id  << " was not found in FooManager";
    // don't throw just display message (typically write to log file).
    std::cout << strStream.str() << std::endl;
    return false;
}

bool FooManager::removeFoo( const std::string& id, MapFoos& fooMap ) {
    MapFoos::iterator itFoo = fooMap.find( id );
    if ( itFoo == fooMap.end() ) {
        std::ostringstream strStream;
        strStream << __FUNCTION__ << " failed. " << id << " was not found in AssetStorage";
        // don't throw just display message (typically write to log file).
        std::cout << strStream.str() << std::endl;
        return false;
    } else {
        // do what ever from Foo's functions to clean up its internals
        // itFoo->second.get()->cleanUp(); // etc.
        fooMap.erase( itFoo );

        // When the above foo was deleted, there might have been some children
        // that were also marked for removal. We can remove them here.
        for ( unsigned i = 0; i < _foosForRemoval.size(); ++i ) {
            itFoo = _idsB.find( _foosForRemoval[i] );
            if ( itFoo != _idsB.end() ) {
                // Remove this Foo
                // do what ever from Foo's functions to clean up its internals.
                // itFoo->second.get()->cleanUp(); // etc.
                _idsB.erase( itFoo );
            } else {
                std::ostringstream strStream;
                strStream << __FUNCTION__ << " failed to find " << _foosForRemoval[i] << " for removal from the _idsB";
                // don't throw just display message (typically write to log file).
                std::cout << strStream.str() << std::endl;
            }
        }
        _foosForRemoval.clear();
        return true;
    }
}

void FooManager::markFooForRemoval( const std::string& id ) {
    _foosForRemoval.push_back( id );
}

It's a nice way to store items dynamically and yes you can see that I'm using new on the pointers in main, but you never see me using delete. This is because once we add that pointer to the manager class, it takes over and handles all the memory for us, since it will turn them into shared_ptr<T>. This manager class also supports polymorphic behavior. This is just a basic shell or structure.

Then from here. You can write another class that holds a pointer to this storage or manager class that will then add and remove items from these containers. The other class would be responsible for finding the objects in this storage and then invoking the methods of the internally stored objects; or you could just add all of that functionality directly into this class. I kind of like to try and keep the storage of things separate from the implementation of things. I hope this structure helps you out, or gives you some ideas to work off of. You can see that I did use function templates within this class to access specific maps of specific derived foos.

You should be able to integrate into the above classes the concept of the is_derived_from as well as checking to see if a specific item already exists and if does don't add it. Final note: you could also split the storage into 2 types where one container will be able to add multiple components that can be rendered multiple times per frame, while the other container can be restrictive. Not sure what kind of benefit you could get with that, maybe in a particle generator or engine system, but the flexibility is there to do that if you need it.

Upvotes: 1

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275810

You just need to make detail::GetComponentTypeID<T>() smarter.

You have, in practice, a list of component types.

template<class...>
struct type_list_t {};

using ComponentList = type_list_t<RenderComponent, PhysicsComponent, CharmComponent>;

this list determines how long your pointer and bit flag arrays are. Place this list explicitly in a famous location that everyone knows about.

Yes, this means you have to rebuild if it changes. Tough.

Now you just have to improve detail::GetComponentTypeID<T>(). Have it constexpr or template metaprogramming search the ComponentList for the first type that passes std::is_base_of< ListElement, T >.

And your code now works as written.

Upvotes: 1

Related Questions