为实体组件系统

时间:2018-03-05 16:39:01

标签: c++ c++11 entity software-design

没有多态性

我已经实现了一个实体组件系统,它使用模板来获取组件。为每种类型生成一个id。对于给定类型T,函数size_t GetComponentTypeId<T>()将始终返回相同的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

添加多态性

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

我的一些想法

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

问题:使其类型安全

我的问题是,我正在努力使它变得类型安全。另外,我想要包含一些检查,以确保SpriteRenderComponent实际上继承自RenderComponent。我最喜欢的解决方案是自动添加的解决方案获取超类的ID并为它们添加组件指针。我对这种元编程很新(也许是错误的单词),所以非常感谢帮助。

更新

我发现一个可能的解决方案是向实体类添加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));
    }

我猜它有点类型安全,但对我来说它有一个主要问题。它要求我每次都调用这个函数 我添加了一个具有多态行为的组件。虽然这是一种解决方案(有点),但我更倾向于采用静态方式来指定这种行为。

2 个答案:

答案 0 :(得分:1)

关于确保它继承自:

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

可能会帮助你;至于其他问题;我需要更多的时间,因为我必须尽快离开...当我有机会更新这个答案时,我将在稍后回来。


编辑 - 添加了一些其他课程并展示其用途。

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

<强>的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 - 我不会更改此类的代码来替换其名称。看了一会儿之后;显而易见的是FooStoreStorage等类似于此类的更合适的名称。除了从其成员容器中添加和删除对象之外,它并没有真正管理任何其他内容。如果您决定添加的功能不仅仅是添加和删除对象,那么您可以按原样保留其名称。

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

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

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

您应该能够将is_derived_from的概念集成到上述类中,并检查特定项目是否已存在以及是否不添加。最后注意:您还可以将存储分为两种类型,其中一个容器可以添加多个组件,每个帧可以多次渲染,而另一个容器可以是限制性的。不知道你可以获得什么样的好处,可能是在粒子发生器或发动机系统中,但如果你需要,可以灵活地做到这一点。

答案 1 :(得分:1)

你只需要让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 >的第一种类型。

现在你的代码就像写的一样。