問題描述
我一直在嘗試制作一個類似于 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.
這篇關于在 Unity 中用 C++ 實現組件系統的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!