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

Implementing polymorphic components for entity-component system

本文关键字:组件 多态 实现 系统 实体      更新时间:2023-10-16

没有多态性

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

添加多态性

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

我的一些想法

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

问题:使其类型安全

我的问题是我正在努力使其类型安全。我还想包括某种检查,以确保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));
}

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

关于确保它继承自:

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

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


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

我有一些时间做一些事情;我不确定这是否是你要找的;但这是我以前使用过的storage-manager型系统。它确实支持类的多态行为。所以也许这种结构会对你有所帮助。

主.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

福基地.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

衍生福斯.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

福经理.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。这是因为一旦我们将该指针添加到管理器类,它就会接管并为我们处理所有内存,因为它会将它们变成shared_ptr<T>.此管理器类还支持多态行为。这只是一个基本的外壳或结构。

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

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

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

您的代码现在可以按编写的方式工作。