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!
I know this post is already answered, but if you look into Game Programming Patterns, in this book he has a design pattern called Service Locator, and at the end, it says Unity uses this pattern together with the Component Pattern. I wish I could answer into more specifics, but this could be another way to approach this.
Since I'm writing my own game engine and incorporating the same design, I thought I'd share my results.
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).
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:
std::tr2::direct_bases
the end result is still duplicates of the same pointer in the map.dynamic_cast
is needed, just a straight static_cast
.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.
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.
#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 */
#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)
#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:\n------\nComponent\t(1)\nCollider\t(1)\nBoxCollider\t(2)\nRenderImage\t(0)\n\n";
// 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:\n-------\ncomponentRef:\t\t" << componentRef.value
<< "\ncolliderRef:\t\t" << colliderRef.value
<< "\nboxColliderRef1:\t" << boxColliderRef1.value
<< "\nboxColliderRef2:\t" << boxColliderRef2.value
<< "\nrenderImageRef:\t\t" << ( &renderImageRef != nullptr ? renderImageRef.value : "nullptr" );
// GetComponents test
auto allColliders = test.GetComponents< Collider >();
std::cout << "\n\nThere are (" << allColliders.size() << ") collider components attached to the test GameObject:\n";
for ( auto && c : allColliders ) {
std::cout << c->value << '\n';
}
// RemoveComponent test
test.RemoveComponent< BoxCollider >(); // removes boxColliderA
auto & boxColliderRef3 = test.GetComponent< BoxCollider >(); // now this is the second BoxCollider "BoxCollider_B"
std::cout << "\n\nFirst BoxCollider instance removed\nboxColliderRef3:\t" << boxColliderRef3.value << '\n';
#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 << "\nSuccessfully removed (" << removed << ") components from the test GameObject\n";
system( "PAUSE" );
return 0;
}
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.
Apologies if this is not what you are looking for, but I had an idea to use the unordered map with a type index and, with the help of some metaprogramming and TR2, place multiple pointers to the component into the map, including its direct base classes as additional keys. So getComponent<SphereCollider>()
and getComponent<Collider>()
along with a down-cast will have the same pointee.
#include <tr2/type_traits>
#include <tuple>
#include <typeindex>
#include <unordered_map>
#include <iostream>
class Component {
public:
virtual ~Component() {}
};
class GameObject {
public:
template <typename T>
void addComponent(T *component);
template <typename T>
T *getComponent();
std::unordered_map<std::typeindex, Component *> components;
};
template <typename>
struct direct_bases_as_tuple {};
template <typename... Types>
struct direct_bases_as_tuple<std::tr2::__reflection_typelist<Types...>> {
typedef std::tuple<Types...> type;
};
template <std::size_t N, typename ComponentBases, typename ComponentType>
struct AddComponent {
GameObject *owner;
explicit AddComponent(GameObject *owner) : owner(owner) {}
void operator()(ComponentType *component) {
AddComponent<N-1, ComponentBases, ComponentType>{owner}(component);
using BaseType = std::tuple_element<N-1, ComponentBases>::type;
owner->components[typeid(BaseType)] = component;
}
};
template <typename ComponentBases, typename ComponentType>
struct AddComponent<0u, ComponentBases, ComponentType> {
GameObject *owner;
explicit AddComponent(GameObject *owner) : owner(owner) {}
void operator()(ComponentType *component) {
return;
}
};
template <typename T>
void GameObject::addComponent(T *component) {
using ComponentBases = direct_bases_as_tuple<std::tr2::direct_bases<ComponentType>::type>::type;
constexpr classCount = std::tuple_size<ComponentBases>::value;
AddComponent<classCount, ComponentBases, T>{this}(component);
components[typeid(T)] = component;
}
template <typename T>
T * GameObject::getComponent() {
auto iter = components.find(typeid(T));
if (iter != std::end(components)) {
return dynamic_cast<T *>(iter->second);
}
return nullptr;
}
class Collider : public Component {};
class SphereCollider : public Collider {};
int main() {
GameObject gameObject;
gameObject.addComponent(new SphereCollider);
//get by derived class
SphereCollider *sphereColliderA = gameObject.getComponent<SphereCollider>();
//get by subclass
SphereCollider *sphereColliderB = dynamic_cast<SphereCollider *>(
gameObject.getComponent<Collider>()
);
if (sphereColliderA == sphereColliderB) {
std::cout << "good" << std::endl;
}
}
I created the AddComponent
struct to recurse through the component base classes at compile-time and insert the pointer (value) with the corresponding class (key) each iteration. The helper struct direct_bases_as_tuple
was inspired by Andy Prowl's answer to change the direct bases into a tuple. I compiled this using GCC 4.9.2 using C++11 features.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With