为实体组件系统实现多态组件

Implementing polymorphic components for entity-component system

没有多态性

我实现了一个使用模板获取组件的实体组件系统。为每种类型生成一个 id。对于给定类型 T,函数 size_t GetComponentTypeId<T>() 将始终 return 相同的 ID。

为了更好理解,这里是添加组件的函数

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

这里是获取组件的函数

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

没什么特别的

还有我当前的实现,如果 GetComponentTypeID() 方法:

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

添加多态性

现在我想向我的 classes 添加多态行为。例如。可能有一个 SpriteRenderComponent 继承自 RenderComponent(它当然继承了 Component)。 RenderComponent 将有一个在 SpriteRenderComponent 中实现的虚拟绘制方法。我希望能够仅添加 sprite 组件,并且仍然能够通过在已添加 sprite 组件的实体上调用 entity.GetComponent<RenderComponent>() 来获取对 renderComponent 的引用。在 returned 渲染组件引用上调用 draw 方法应该调用 SpriteRenderComponent.draw()。此外,我不应该能够添加任何其他继承自渲染组件的组件。

我的一些想法

我认为,基本的解决方案是为两个 id 添加一个 SpriteRenderComponent 的一个实例的指针; RenderComponent 和 SpriteRenderComponent。这也会阻止用户添加多个继承自 RenderComponent 的组件。组件本身只会被添加到 componentList 一次,因此每帧只会更新一次(根据需要)

问题:使其类型安全

我的问题是我正在努力使其类型安全。我还想包括某种检查,以确保 SpriteRenderComponent 实际上继承自 RenderComponent。我最喜欢的解决方案是“自动添加获取超级 class 的 ID 并为它们添加组件指针的解决方案。我对这种元编程很陌生(可能是用错了词)所以非常感谢帮助。

更新

我找到的一个可能的解决方案是向实体 class 添加一个 AddPolymorphism<class TDerivedComponent, class TBaseComponent>() 方法。这是实现:

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

我想它有点类型安全,但对我来说它有一个主要问题。它要求我每次都调用此函数 我添加了一个具有多态行为的组件。尽管这是一个解决方案(有点),但我更喜欢静态方式来指定此行为。

关于确保它继承自的部分:

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

可能会帮助你;至于其他问题;我将需要更多时间,因为我很快就要离开了……我稍后会在有机会更新此答案时再回来讨论这个问题。


编辑 - 添加了一些额外的 classes 并展示了它们的用途。

我有时间做某事;我不确定这是否是您要找的东西;但这是我以前用过的 storage-manager 类型系统。它确实支持 classes 的多态行为。所以也许这个结构可以帮助你。

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 - 我不会更改此 class 的代码来替换它的名称。看了一会儿之后;很明显,FooStoreStorage 之类的名称更适合此 class。除了从其成员容器中添加和删除对象外,它实际上不管理任何其他内容。如果您决定添加更多功能,而不仅仅是添加和删除对象,则可以保留其名称。

#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 );
}

这是一种动态存储项目的好方法,是的,您可以看到我在 main 中的指针上使用 new,但您永远不会看到我使用 delete。这是因为一旦我们将该指针添加到管理器 class,它就会接管并为我们处理所有内存,因为它将把它们变成 shared_ptr<T>. 这个管理器 class 也支持多态行为.这只是一个基本的 shell 或结构。

然后从这里开始。您可以编写另一个 class 来保存指向此存储或管理器 class 的指针,然后它将从这些容器中添加和删除项目。另一个class负责查找这个存储中的对象,然后调用内部存储对象的方法;或者您可以将所有这些功能直接添加到这个 class 中。我有点喜欢尝试将事物的存储与事物的实现分开。我希望这个结构能帮助你,或者给你一些想法来解决问题。您可以看到我确实在 class 中使用了函数模板来访问特定派生 foo 的特定映射。

您应该能够将 classes 的概念整合到 is_derived_from 以及检查特定项目是否已经存在,如果不存在则不添加它。最后注意:您还可以将存储分为两种类型,其中一个容器将能够添加多个组件,这些组件可以每帧渲染多次,而另一个容器可能是限制性的。不确定你能从中获得什么样的好处,也许在粒子发生器或引擎系统中,但如果你需要的话,可以灵活地做到这一点。

你只需要让 detail::GetComponentTypeID<T>() 更聪明。

实际上,您有一个组件类型列表。

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

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

这个列表决定了你的指针和位标志数组的长度。将此列表明确放置在每个人都知道的著名位置。

是的,这意味着如果它发生变化,您必须重建。艰难。

现在你只需要改进detail::GetComponentTypeID<T>()。让它 constexpr 或模板元编程在 ComponentList 中搜索第一个通过 std::is_base_of< ListElement, T > 的类型。

您的代码现在可以正常工作了。