在 Unity 中用 C++ 实现组件系统
我一直在尝试制作一个类似于 Unity 的基于组件的系统,但使用的是 C++.我想知道 Unity 实现的 GetComponent()
方法是如何工作的.这是一个非常强大的功能.具体来说,我想知道它使用什么样的容器来存储其组件.
我在此函数的克隆中需要的两个条件如下.1. 我还需要返回任何继承的组件.例如,如果SphereCollider
继承了Collider,GetComponent
将返回附加到GameObject
的SphereCollider
,但 GetComponent
不会返回任何附加的 Collider
.2.我需要快速的功能.最好是使用某种哈希函数.
对于标准一,我知道我可以使用类似于以下实现的东西
std::vector组件模板 T* GetComponent(){对于每个(组件中的组件* c)if (dynamic_cast(*c))返回 (T*)c;返回 nullptr;}
但这不符合快速的第二个标准.为此,我知道我可以做这样的事情.
std::unordered_map组件模板 T* GetComponent(){返回 (T*)components[typeid(T)];}
但同样,这不符合第一个标准.
如果有人知道结合这两个功能的某种方法,即使它比第二个示例慢一点,我也愿意牺牲一点.谢谢!
解决方案由于我正在编写自己的游戏引擎并采用相同的设计,所以我想我会分享我的结果.
概述
我为我想用作GameObject
实例的Components
的类编写了自己的RTTI.通过 #define
两个宏来减少输入量:CLASS_DECLARATION
和 CLASS_DEFINITION
CLASS_DECLARATION
声明了唯一的 static const std::size_t
,用于识别 class
类型(Type
code>),以及一个 virtual
函数,它允许对象通过调用同名的父类函数来遍历它们的 class
层次结构 (IsClassType
).
CLASS_DEFINITION
定义了这两件事.即 Type
被初始化为 class
名称的字符串化版本的散列(使用 TO_STRING(x) #x
),这样 Type
比较只是一个 int 比较,而不是一个字符串比较.
std::hash<std::string>
是使用的哈希函数,它保证相等的输入产生相等的输出,并且冲突次数接近于零.
除了散列冲突的低风险之外,这个实现还有一个额外的好处,它允许用户使用这些宏创建他们自己的Component
类,而无需参考|扩展一些主包含
文件,或者使用enum class
s的typeid
(只提供运行时类型,不提供父类).
添加组件
这个自定义 RTTI 将 Add|Get|RemoveComponent
的调用语法简化为仅指定 template
类型,就像 Unity 一样.
AddComponent
方法完美地将通用引用可变参数包转发到用户的构造函数.因此,例如,用户定义的 Component
派生的 class CollisionModel
可以具有构造函数:
CollisionModel(GameObject * owner, const Vec3 & size, const Vec3 & offset, bool active );
然后用户只需调用:
myGameObject.AddComponent(this, Vec3( 10, 10, 10 ), Vec3( 0, 0, 0 ), true );
请注意 Vec3
的显式构造,因为如果使用像 { 10, 10, 10 } 这样推导出的初始化列表语法,完美转发可能无法链接
不管 Vec3
的构造函数声明.
此自定义 RTTI 还解决了 std::unordered_map<std::typeindex,...>
解决方案的 3 个问题:
- 即使使用
std::tr2::direct_bases
进行层次遍历,最终结果仍然是映射中相同指针的重复项. - 用户不能添加多个等效类型的组件,除非使用的映射允许/解决冲突而不覆盖,这会进一步减慢代码速度.
- 不需要不确定和缓慢的
dynamic_cast
,只需直接的static_cast
.
获取组件
GetComponent
只是使用 template
类型的 static const std::size_t Type
作为 virtual bool IsClassType 的参数
方法并迭代 std::vector<;std::unique_ptr<组件>
寻找第一个匹配项.
我还实现了一个 GetComponents
方法,可以获取请求类型的所有组件,同样包括从父类获取.
请注意,static
成员 Type
可以在有和没有类实例的情况下访问.
另请注意,Type
是 public
,为每个 Component
派生类声明,...并大写以强调其灵活使用,尽管是 POD 会员.
移除组件
最后,RemoveComponent
使用 C++14
的 init-capture 来传递相同的 static const std::size_t Type
template
输入一个 lambda,所以它基本上可以进行相同的向量遍历,这次是获取一个 iterator
到第一个匹配元素.
代码中有一些关于更灵活实现的想法的注释,更不用说所有这些的 const
版本也可以轻松实现.
代码
类.h
#ifndef TEST_CLASSES_H#define TEST_CLASSES_H#include <字符串>#include <功能>#include <向量>#include <内存>#include <算法>#define TO_STRING( x ) #x//****************//类声明////这个宏必须包含在 Component 的任何子类的声明中.//它声明了用于类型检查的变量.//****************#define CLASS_DECLARATION( 类名) 上市: 静态常量 std::size_t 类型;virtual bool IsClassType( const std::size_t classType ) const override;//****************//类定义////这个宏必须包含在类定义中才能正确初始化//用于类型检查的变量.请特别注意以确保//指示正确的父类或运行时类型信息//不正确.仅适用于单继承 RTTI.//****************#define CLASS_DEFINITION( parentclass, childclass ) const std::size_t childclass::Type = std::hash<std::string >()( TO_STRING( childclass ) );ool childclass::IsClassType( const std::size_t classType ) const { if ( classType == childclass::Type ) 返回真;返回 parentclass::IsClassType( classType );} 命名空间 rtti {//****************//零件//基类//****************类组件{上市:静态常量 std::size_t 类型;virtual bool IsClassType( const std::size_t classType ) const {返回类类型 == 类型;}上市:虚拟 ~Component() = 默认值;组件( std::string && initialValue ):值(初始值){}上市:std::string 值 =未初始化";};//****************//碰撞器//****************类碰撞器:公共组件{CLASS_DECLARATION(碰撞器)上市:碰撞器( std::string && initialValue ): 组件( std::move(initialValue ) ) {}};//****************//盒子碰撞器//****************类 BoxCollider : 公共碰撞器 {CLASS_DECLARATION(BoxCollider)上市:BoxCollider( std::string && initialValue ): Collider( std::move(initialValue ) ) {}};//****************//渲染图像//****************类RenderImage:公共组件{CLASS_DECLARATION(渲染图像)上市:RenderImage( std::string && initialValue ): 组件( std::move(initialValue ) ) {}};//****************//游戏对象//****************类游戏对象{上市:std::vector获取组件();模板<类组件类型 >int RemoveComponents();};//****************//游戏对象::添加组件//使用匹配的参数列表将所有参数完美转发到 ComponentType 构造函数//DEBUG: 务必将这个 fn 的参数与所需的构造函数进行比较,以避免完美转发失败的情况//EG:推导的初始化列表、仅声明的静态 const int 成员、0|NULL 代替 nullptr、重载的 fn 名称和位域//****************模板(std::forward(params)...));}//****************//游戏对象::获取组件//返回匹配模板类型的第一个组件//或者是从模板类型派生的//EG:如果模板类型是Component,components[0]类型是BoxCollider//然后将返回 components[0] 因为它派生自 Component//****************模板<类组件类型 >组件类型游戏对象::GetComponent() {for(自动&&组件:组件){if ( 组件-> IsClassType( ComponentType::Type ) )返回 *static_cast<组件类型 * >( component.get() );}返回 *std::unique_ptr<组件类型 >( nullptr );}//****************//游戏对象::移除组件//删除成功返回真//如果组件为空,或者不存在这样的组件,则返回 false//****************模板<类组件类型 >bool GameObject::RemoveComponent() {如果 ( components.empty() )返回假;汽车&index = std::find_if( components.begin(),组件.end(),[ classType = ComponentType::Type ]( auto & component ) {返回组件-> IsClassType( classType );});bool 成功 = 索引 != components.end();如果(成功)组件.擦除(索引);返回成功;}//****************//游戏对象::GetComponents//遵循与 GetComponent 相同的匹配标准,返回指向所请求组件模板类型的指针向量//注意:编译器可以选择复制省略或移动构造 componentsOfType 到这里的返回值中//TODO:传入所需的元素数量(例如:最多 7 个,或仅前 2 个),这将允许 std::array 返回值,//除非用户不知道 GameObject 有多少这样的组件,否则需要一个单独的 fn 来获取它们 *all*//TODO:定义一个可以直接抓取到请求类型的第n个组件的GetComponentAt()//****************模板<类组件类型 >std::vector<组件类型 * >游戏对象::GetComponents() {std::vector<组件类型 * >组件类型;for(自动&&组件:组件){if ( 组件-> IsClassType( ComponentType::Type ) )componentsOfType.emplace_back(static_cast(component.get()));}返回 componentsOfType;}//****************//游戏对象::移除组件//返回成功删除的次数,如果没有删除则返回 0//****************模板<类组件类型 >int GameObject::RemoveComponents() {如果 ( components.empty() )返回0;int numRemoved = 0;布尔成功=假;做 {汽车&index = std::find_if( components.begin(),组件.end(),[ classType = ComponentType::Type ]( auto & component ) {返回组件-> IsClassType( classType );});成功 = 索引 != components.end();如果(成功){组件.擦除(索引);++numRemoved;}} while ( 成功 );返回 numRemoved;}}/* rtti */#endif/* TEST_CLASSES_H */
类.cpp
#include "Classes.h";使用命名空间 rtti;const std::size_t Component::Type = std::hash()(TO_STRING(Component));CLASS_DEFINITION(组件,碰撞器)CLASS_DEFINITION(碰撞器,BoxCollider)CLASS_DEFINITION(组件,渲染图像)
main.cpp
#include #include "Classes.h";#define MORE_CODE 0int main( int argc, const char * argv ) {使用命名空间 rtti;游戏对象测试;//添加组件测试test.AddComponent<组件 >(组件");test.AddComponent<对撞机>(对撞机");test.AddComponent(渲染图像");#万一std::cout <<添加:
------
Component (1)
Collider (1)
BoxCollider (2)
RenderImage (0)
";//获取组件测试汽车&componentRef = test.GetComponent<组件 >();汽车&colliderRef = test.GetComponent<对撞机>();汽车&boxColliderRef1 = test.GetComponent();//使用 MORE_CODE 0 获取 &nullptrstd::cout <<值:
-------
componentRef: ";<<组件引用值<<
colliderRef: "<<colliderRef.value<<
boxColliderRef1: "<<boxColliderRef1.value<<
boxColliderRef2: "<<boxColliderRef2.value<<
renderImageRef: "<<( &renderImageRef != nullptr ? renderImageRef.value : nullptr");//获取组件测试auto allColliders = test.GetComponents<对撞机>();std::cout <<"
有 (<< allColliders.size() << ") 碰撞器组件附加到测试游戏对象:
";for ( auto && c : allColliders ) {std::cout <<c->值<<'
';}//移除组件测试test.RemoveComponent();#万一std::cout <<
从测试游戏对象中成功删除(<<删除<<)组件
";系统(暂停");返回0;}
输出
添加:------组件 (1)对撞机 (1)BoxCollider (2)渲染图像 (0)价值观:-------componentRef: 组件colliderRef: 碰撞器boxColliderRef1:BoxCollider_AboxColliderRef2:BoxCollider_ArenderImageRef: nullptr有 (3) 个碰撞器组件附加到测试游戏对象:对撞机BoxCollider_ABoxCollider_B删除了第一个 BoxCollider 实例boxColliderRef3:BoxCollider_B从测试游戏对象中成功移除 (3) 个组件
旁注:Unity 使用 Destroy(object)
而不是 RemoveComponent
,但我的版本现在适合我的需求.>
I've been experimenting with making a component based system similar to Unity's, but in C++. I'm wondering how the GetComponent()
method that Unity implements works. It is a very powerful function. Specifically, I want to know what kind of container it uses to store its components.
The two criteria I need in my clone of this function are as follows. 1. I need any inherited components to be returned as well. For example, if SphereCollider
inherits Collider, GetComponent<Collider>()
will return the SphereCollider
attached to the GameObject
, but GetComponent<SphereCollider>()
will not return any Collider
attached. 2. I need the function to be fast. Preferably, it would use some kind of hash function.
For criteria one, I know that I could use something similar to the following implementation
std::vector<Component*> components
template <typename T>
T* GetComponent()
{
for each (Component* c in components)
if (dynamic_cast<T>(*c))
return (T*)c;
return nullptr;
}
But that doesn't fit the second criteria of being fast. For that, I know I could do something like this.
std::unordered_map<type_index, Component*> components
template <typename T>
T* GetComponent()
{
return (T*)components[typeid(T)];
}
But again, that doesn't fit the first criteria.
If anybody knows of some way to combine those two features, even if it's a little slower than the second example, I would be willing to sacrifice a little bit. Thank you!
解决方案Since I'm writing my own game engine and incorporating the same design, I thought I'd share my results.
Overview
I wrote my own RTTI for the classes I cared to use as Components
of my GameObject
instances. The amount of typing is reduced by #define
ing the two macros: CLASS_DECLARATION
and CLASS_DEFINITION
CLASS_DECLARATION
declares the unique static const std::size_t
that will be used to identify the class
type (Type
), and a virtual
function that allows objects to traverse their class
hierarchy by calling their parent-class function of the same name (IsClassType
).
CLASS_DEFINITION
defines those two things. Namely the Type
is initialized to a hash of a stringified version of the class
name (using TO_STRING(x) #x
), so that Type
comparisons are just an int compare and not a string compare.
std::hash<std::string>
is the hash function used, which guarantees equal inputs yield equal outputs, and the number of collisions is near-zero.
Aside from the low risk of hash collisions, this implementation has the added benefit of allowing users to create their own Component
classes using those macros without ever having to refer to|extend some master include
file of enum class
s, or use typeid
(which only provides the run-time type, not the parent-classes).
AddComponent
This custom RTTI simplifies the call syntax for Add|Get|RemoveComponent
to just specifying the template
type, just like Unity.
The AddComponent
method perfect-forwards a universal reference variadic parameter pack to the user's constructor. So, for example, a user-defined Component
-derived class CollisionModel
could have the constructor:
CollisionModel( GameObject * owner, const Vec3 & size, const Vec3 & offset, bool active );
then later on the user simply calls:
myGameObject.AddComponent<CollisionModel>(this, Vec3( 10, 10, 10 ), Vec3( 0, 0, 0 ), true );
Note the explicit construction of the Vec3
s because perfect-forwarding can fail to link if using deduced initializer-list syntax like { 10, 10, 10 }
regardless of Vec3
's constructor declarations.
This custom RTTI also resolves 3 issues with the std::unordered_map<std::typeindex,...>
solution:
- Even with the hierarchy traversal using
std::tr2::direct_bases
the end result is still duplicates of the same pointer in the map. - A user can't add multiple Components of equivalent type, unless a map is used that allows/solves collisions without overwriting, which further slows down the code.
- No uncertain and slow
dynamic_cast
is needed, just a straightstatic_cast
.
GetComponent
GetComponent
just uses the static const std::size_t Type
of the template
type as an argument to the virtual bool IsClassType
method and iterates over std::vector< std::unique_ptr< Component > >
looking for the first match.
I've also implemented a GetComponents
method that can get all components of the requested type, again including getting from the parent-class.
Note that the static
member Type
can be accessed both with and without an instance of the class.
Also note that Type
is public
, declared for each Component
-derived class, ...and capitalized to emphasize its flexible use, despite being a POD member.
RemoveComponent
Lastly, RemoveComponent
uses C++14
's init-capture to pass that same static const std::size_t Type
of the template
type into a lambda so it can basically do the same vector traversal, this time getting an iterator
to the first matching element.
There are a few comments in the code about ideas for a more flexible implementation, not to mention const
versions of all these could also easily be implemented.
The Code
Classes.h
#ifndef TEST_CLASSES_H
#define TEST_CLASSES_H
#include <string>
#include <functional>
#include <vector>
#include <memory>
#include <algorithm>
#define TO_STRING( x ) #x
//****************
// CLASS_DECLARATION
//
// This macro must be included in the declaration of any subclass of Component.
// It declares variables used in type checking.
//****************
#define CLASS_DECLARATION( classname )
public:
static const std::size_t Type;
virtual bool IsClassType( const std::size_t classType ) const override;
//****************
// CLASS_DEFINITION
//
// This macro must be included in the class definition to properly initialize
// variables used in type checking. Take special care to ensure that the
// proper parentclass is indicated or the run-time type information will be
// incorrect. Only works on single-inheritance RTTI.
//****************
#define CLASS_DEFINITION( parentclass, childclass )
const std::size_t childclass::Type = std::hash< std::string >()( TO_STRING( childclass ) );
bool childclass::IsClassType( const std::size_t classType ) const {
if ( classType == childclass::Type )
return true;
return parentclass::IsClassType( classType );
}
namespace rtti {
//***************
// Component
// base class
//***************
class Component {
public:
static const std::size_t Type;
virtual bool IsClassType( const std::size_t classType ) const {
return classType == Type;
}
public:
virtual ~Component() = default;
Component( std::string && initialValue )
: value( initialValue ) {
}
public:
std::string value = "uninitialized";
};
//***************
// Collider
//***************
class Collider : public Component {
CLASS_DECLARATION( Collider )
public:
Collider( std::string && initialValue )
: Component( std::move( initialValue ) ) {
}
};
//***************
// BoxCollider
//***************
class BoxCollider : public Collider {
CLASS_DECLARATION( BoxCollider )
public:
BoxCollider( std::string && initialValue )
: Collider( std::move( initialValue ) ) {
}
};
//***************
// RenderImage
//***************
class RenderImage : public Component {
CLASS_DECLARATION( RenderImage )
public:
RenderImage( std::string && initialValue )
: Component( std::move( initialValue ) ) {
}
};
//***************
// GameObject
//***************
class GameObject {
public:
std::vector< std::unique_ptr< Component > > components;
public:
template< class ComponentType, typename... Args >
void AddComponent( Args&&... params );
template< class ComponentType >
ComponentType & GetComponent();
template< class ComponentType >
bool RemoveComponent();
template< class ComponentType >
std::vector< ComponentType * > GetComponents();
template< class ComponentType >
int RemoveComponents();
};
//***************
// GameObject::AddComponent
// perfect-forwards all params to the ComponentType constructor with the matching parameter list
// DEBUG: be sure to compare the arguments of this fn to the desired constructor to avoid perfect-forwarding failure cases
// EG: deduced initializer lists, decl-only static const int members, 0|NULL instead of nullptr, overloaded fn names, and bitfields
//***************
template< class ComponentType, typename... Args >
void GameObject::AddComponent( Args&&... params ) {
components.emplace_back( std::make_unique< ComponentType >( std::forward< Args >( params )... ) );
}
//***************
// GameObject::GetComponent
// returns the first component that matches the template type
// or that is derived from the template type
// EG: if the template type is Component, and components[0] type is BoxCollider
// then components[0] will be returned because it derives from Component
//***************
template< class ComponentType >
ComponentType & GameObject::GetComponent() {
for ( auto && component : components ) {
if ( component->IsClassType( ComponentType::Type ) )
return *static_cast< ComponentType * >( component.get() );
}
return *std::unique_ptr< ComponentType >( nullptr );
}
//***************
// GameObject::RemoveComponent
// returns true on successful removal
// returns false if components is empty, or no such component exists
//***************
template< class ComponentType >
bool GameObject::RemoveComponent() {
if ( components.empty() )
return false;
auto & index = std::find_if( components.begin(),
components.end(),
[ classType = ComponentType::Type ]( auto & component ) {
return component->IsClassType( classType );
} );
bool success = index != components.end();
if ( success )
components.erase( index );
return success;
}
//***************
// GameObject::GetComponents
// returns a vector of pointers to the the requested component template type following the same match criteria as GetComponent
// NOTE: the compiler has the option to copy-elide or move-construct componentsOfType into the return value here
// TODO: pass in the number of elements desired (eg: up to 7, or only the first 2) which would allow a std::array return value,
// except there'd need to be a separate fn for getting them *all* if the user doesn't know how many such Components the GameObject has
// TODO: define a GetComponentAt<ComponentType, int>() that can directly grab up to the the n-th component of the requested type
//***************
template< class ComponentType >
std::vector< ComponentType * > GameObject::GetComponents() {
std::vector< ComponentType * > componentsOfType;
for ( auto && component : components ) {
if ( component->IsClassType( ComponentType::Type ) )
componentsOfType.emplace_back( static_cast< ComponentType * >( component.get() ) );
}
return componentsOfType;
}
//***************
// GameObject::RemoveComponents
// returns the number of successful removals, or 0 if none are removed
//***************
template< class ComponentType >
int GameObject::RemoveComponents() {
if ( components.empty() )
return 0;
int numRemoved = 0;
bool success = false;
do {
auto & index = std::find_if( components.begin(),
components.end(),
[ classType = ComponentType::Type ]( auto & component ) {
return component->IsClassType( classType );
} );
success = index != components.end();
if ( success ) {
components.erase( index );
++numRemoved;
}
} while ( success );
return numRemoved;
}
} /* rtti */
#endif /* TEST_CLASSES_H */
Classes.cpp
#include "Classes.h"
using namespace rtti;
const std::size_t Component::Type = std::hash<std::string>()(TO_STRING(Component));
CLASS_DEFINITION(Component, Collider)
CLASS_DEFINITION(Collider, BoxCollider)
CLASS_DEFINITION(Component, RenderImage)
main.cpp
#include <iostream>
#include "Classes.h"
#define MORE_CODE 0
int main( int argc, const char * argv ) {
using namespace rtti;
GameObject test;
// AddComponent test
test.AddComponent< Component >( "Component" );
test.AddComponent< Collider >( "Collider" );
test.AddComponent< BoxCollider >( "BoxCollider_A" );
test.AddComponent< BoxCollider >( "BoxCollider_B" );
#if MORE_CODE
test.AddComponent< RenderImage >( "RenderImage" );
#endif
std::cout << "Added:
------
Component (1)
Collider (1)
BoxCollider (2)
RenderImage (0)
";
// GetComponent test
auto & componentRef = test.GetComponent< Component >();
auto & colliderRef = test.GetComponent< Collider >();
auto & boxColliderRef1 = test.GetComponent< BoxCollider >();
auto & boxColliderRef2 = test.GetComponent< BoxCollider >(); // boxColliderB == boxColliderA here because GetComponent only gets the first match in the class hierarchy
auto & renderImageRef = test.GetComponent< RenderImage >(); // gets &nullptr with MORE_CODE 0
std::cout << "Values:
-------
componentRef: " << componentRef.value
<< "
colliderRef: " << colliderRef.value
<< "
boxColliderRef1: " << boxColliderRef1.value
<< "
boxColliderRef2: " << boxColliderRef2.value
<< "
renderImageRef: " << ( &renderImageRef != nullptr ? renderImageRef.value : "nullptr" );
// GetComponents test
auto allColliders = test.GetComponents< Collider >();
std::cout << "
There are (" << allColliders.size() << ") collider components attached to the test GameObject:
";
for ( auto && c : allColliders ) {
std::cout << c->value << '
';
}
// RemoveComponent test
test.RemoveComponent< BoxCollider >(); // removes boxColliderA
auto & boxColliderRef3 = test.GetComponent< BoxCollider >(); // now this is the second BoxCollider "BoxCollider_B"
std::cout << "
First BoxCollider instance removed
boxColliderRef3: " << boxColliderRef3.value << '
';
#if MORE_CODE
// RemoveComponent return test
int removed = 0;
while ( test.RemoveComponent< Component >() ) {
++removed;
}
#else
// RemoveComponents test
int removed = test.RemoveComponents< Component >();
#endif
std::cout << "
Successfully removed (" << removed << ") components from the test GameObject
";
system( "PAUSE" );
return 0;
}
Output
Added:
------
Component (1)
Collider (1)
BoxCollider (2)
RenderImage (0)
Values:
-------
componentRef: Component
colliderRef: Collider
boxColliderRef1: BoxCollider_A
boxColliderRef2: BoxCollider_A
renderImageRef: nullptr
There are (3) collider components attached to the test GameObject:
Collider
BoxCollider_A
BoxCollider_B
First BoxCollider instance removed
boxColliderRef3: BoxCollider_B
Successfully removed (3) components from the test GameObject
Side-note: granted Unity uses Destroy(object)
and not RemoveComponent
, but my version suits my needs for now.
相关文章