Implementing Component System from Unity in C++

Implementing Component system from Unity in c++

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 #defineing 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 classs, 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 Vec3s 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:

  1. Even with the hierarchy traversal using std::tr2::direct_bases the end result is still duplicates of the same pointer in the map.
  2. 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.
  3. No uncertain and slow dynamic_cast is needed, just a straight static_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:\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;
}


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.

C++ Generic component system

dynamic_cast requires that the expression being casted, and the type being casted to, are pointers (or references).

You can't use dynamic_cast without using either pointers or references.

So the solution is probably:

if (dynamic_cast<T*>(component) != nullptr) 

Using Unity's new Entity Component System

My logic is telling me, that I should use it only when I need to
control a larger amount of objects.

Not really but it helps when it's used to control many objects. This is not the only or main objective of ECS. See the list reasons why ECS should be used below:

  • Performance
  • Memory Management
  • Build Size

It's not only used when you are working with many objects, it's also used to manage the memory. It reduces the amount of memory required when ECS is used than when the Unity's component system is used. Also, it reduces the build size too. With ECS, you can reduce the build size of your project. ECS makes it possible to use Unity's API as modules, You can decide which modules to include in the build therefore reducing the project size. For example, you can decide to remove the physics system if you don't need it in your project.

Finally, ECS is really important when it comes to building lightwight projects. For example, if you want to build a lightweight game or interactive program that would run on other smaller devices as an Ad, ECS should be used here since it will reduce the size of the Ad, loading time, the memory footprint. You should watch this video and visit the Unity's ECS page to learn more about it.

So,when should you use ECS?

When you need performance, want to conserve memory, reduce the build size of your project or create a lightweight program. Of-course there are many other uses of it. It's still new and we will find out more about it very soon.

How to improve a GetComponent<> method

Potential pitfall:

Do not use this method to store persistent (e.g on disk) type metadata, because typeid might change between different invocations of the same program.


As a side note: in Unity the GetComponent method is indeed expensive, so it is not recommended to call it frequently (i.e. several times per frame). Instead, you should get it and cache the result somewhere else.


So to implement this you can use the typeid operator and std::type_index provided by the standard. std::type_index is unique for every type and can be used as a map key (see this reference page).

Here is an example implementation:

#include <typeindex>
#include <map>
#include <cassert>

//Base class of all components
class Component {
public:
virtual ~Component() = default;
Component(const Component& rhs) = delete;
protected:
Component() = default;
};

class Rigidbody : public Component {

};

class MeshRenderer : public Component {

};

class Text : public Component {
public:
std::string text;
};

class GameObject {
private:
std::multimap<std::type_index, Component*> m_components;
public:
template<typename T,
typename = std::enable_if_t<std::is_base_of_v<Component, T>>>
T* add_component() {
return dynamic_cast<T*>(m_components.insert(std::make_pair(std::type_index(typeid(T)), new T))->second);
}

template<typename T,
typename = std::enable_if_t<std::is_base_of_v<Component, T>>>
T* get_component() {
auto it = m_components.find(std::type_index(typeid(T)));
if (it == m_components.end()) return nullptr;
return dynamic_cast<T*>(it->second);
}
};

int main() {
GameObject gm;
gm.add_component<MeshRenderer>();
auto& text = *gm.add_component<Text>();
assert(gm.get_component<MeshRenderer>() != nullptr);
assert(gm.get_component<Rigidbody>() == nullptr);
text.text = "Hello, World!";
std::cout << gm.get_component<Text>()->text << std::endl;
return 0;
}

The output should be: Hello, World!

Component Based Design (C# Game Engine)

However, I've realised that some components, rely on other components
to work.

In the case I mentioned above the physics component, needs to access
the transform component to update and know the balls position,
rotation, etc.

Yes its true, but it is not the problem. It is a requirement which cannot be eliminated as some time two or more compoennts work together to achieve an objective. But you can make it more sensible through adding dependent component from your code as Require Component attribute usually use in Unity.

You should need to keeping mind that beside several advantages of this component based model complete independce cannot be achived.

The components should be appropriate for deployment into any suitable
environments, thus they should possess minimal dependencies related to
other components[They should be, but cannot possible in every circumstances like in your case or unity game-engin]
. This ensures that the deployment of a particular
component do not affect the whole system in any way.(more)

Note: you have right to disagree with the answer or provide more relent answer that ensure decoupling.

How does Unity decide which Component to return when calling GetComponent<T> on a GameObject with multiple components of type T?

Every object and it's subobjects in Unity are hierarchically placed in a collection. You can call it a "WYSIWYG" because as you see every object in the scene hierarchy is loaded in the same order they are displayed on that list. The same thing applies to the components and it's the main reason why Transform component is placed on top of every other component.

GetComponent will return the first item from the collection of Components seen from the top of the list. To be exactly sure which component will be returned you can order them as you wish in the inspector view.


From the documentation page:

The order you give to components in the Inspector window is the order you need to use when querying components in your user scripts. If you query the components programmatically, you’ll get the order you see in the Inspector.



Related Topics



Leave a reply



Submit