12

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!

Neuron
  • 5,141
  • 5
  • 38
  • 59
RedstoneRusty
  • 125
  • 1
  • 8
  • I don't know the answer, but I'm curious, why do you want/need to do this? :) – Itération 122442 May 22 '17 at 06:00
  • @Andromelus I'm mostly just trying to understand how Unity may have implemented it. I've been pretty interested in engine architecture for a while. – RedstoneRusty May 22 '17 at 06:12
  • Ok, then I think I'll keep chcking your thread ;), good luck – Itération 122442 May 22 '17 at 06:17
  • @Andromelus: someone might want to make their own component system, but in c++ where everything can be compiled nice and fast. Unity compiles your C# scripts but they have a pre-compiled core to the program, which has a lot of wasted stuff in there that you aren't using. But it makes sense to learn from what Unity did as well – Jason Lang May 22 '17 at 06:22
  • you can't use just a map btw, as there can be more than one component of a given type. – Jason Lang May 22 '17 at 06:25
  • Interesting question! – Farhan May 22 '17 at 06:31
  • You could put an if statement into the second implementation, so that it only does the iterated search when it doesn't find an exact match for the type. – Jason Lang May 22 '17 at 06:40
  • Since you are representing hierarchies of objects, why don't you do it with a tree? Traverse down a branch until a exact match, otherwise, give all descendants – Passer By May 22 '17 at 07:04
  • @JasonLang you can use a map, you just have to use Type as the key and a List of component for the value. Then you can simply ask the dictionary if the type is already in, then returns the first item of the list which is what Unity probably does actually. Then you can extend that to children as well. – Everts May 22 '17 at 08:09
  • Having a dictionary enables a whole lot of features, like getting all component types on a game object => dict.Keys or all components on it with iterating through keys and concatenating values into one array. I would definitely opt for a Dictionary in this case. – Everts May 22 '17 at 08:13
  • 2
    As far as I know most C++ component systems use dynamic cast as component selector method. Please note that until you reach 20+ components per object a map with a string key may be slower that an iteration on all the items and a dynamic cast. – gabry May 22 '17 at 08:15
  • 2
    Just a fun FYI, the decompiled source of GetComponent is `public T GetComponent() where T : Component { return this.GetComponent(typeof (T)) as T; }` and that overload it is calling is implemented as `public extern Component GetComponent(System.Type type);` so it is implementing it in C++ too. – Scott Chamberlain May 22 '17 at 23:57
  • @Scott Chamberlain Thank. As another fun FYI, `typeof` in C# is kinda (too) slow for (big) game. https://gamedev.stackexchange.com/questions/46038/why-would-typeoft-kill-performance – cppBeginner Aug 23 '17 at 02:03

5 Answers5

7

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.

Community
  • 1
  • 1
TOM__
  • 86
  • 1
  • 5
  • 1
    This is perfect. Thank you. My only concern would be for serializing the data of the class for saving scenes. I don't think `TO_STRING(childclass)` would be consistent across separate instances, right? So I guess I would just have to rebuild the hash table every time, which is a small price to pay. – RedstoneRusty Jan 10 '18 at 00:41
  • 1
    @RedstoneRusty Yes, the hash of `TO_STRING(childclass)` would likely be different across multiple executions. I based my RTTI design off idTech4's RTTI design. Take a look at the idTech4 source code being used for Quake4 [here.](https://github.com/tmf7/Quake4-Game-Mod-Midterm-Project/blob/master/game/gamesys/Class.h#L146) I simplified a number of things for my smaller project, their design uses consistent `idTypeInfo` across executions, and serializes each `class` into`idSaveGame` and `idLoadGame` objects via macroed `idClass::*Save` and `idClass::*Restore` function pointer initialization. – TOM__ Jan 10 '18 at 02:46
  • [This HashIndex](https://codereview.stackexchange.com/questions/149724/fast-hashindex-hashtable-template-for-indexes-and-arrays-c) could also be used to reduce the `std::vector` lookup to average case `O(1)` for `Get|RemoveComoponent`, which I've implemented in my engine [here.](https://github.com/tmf7/Engine-of-Evil/blob/componentModel/EngineOfEvil/source/GameObject.h#L116) I decided to combine the `hashmap` and `vector` ideas because the `childclass::Type` is already a hashed `string` key. – TOM__ Jan 10 '18 at 02:54
1

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.

diametralpitch
  • 675
  • 3
  • 5
  • Just a neutral comment. This reminds me of [a solution](https://stackoverflow.com/a/44023677) for another typeid question. Phrase `std::unordered_map` seems to be the key idea. – javaLover May 24 '17 at 03:36
1

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.

MrAbnox
  • 23
  • 6
0

The Unity engine is linked with a forked mono runtime, on which unity scripts are executed.

In UnityEngine.Component

public class Component : Object
  {
    .
    .        
    [TypeInferenceRule(TypeInferenceRules.TypeReferencedByFirstArgument)]
    public Component GetComponent(Type type)
    {
        return this.gameObject.GetComponent(type);
    }

    [GeneratedByOldBindingsGenerator]
    [MethodImpl(MethodImplOptions.InternalCall)]
    internal extern void GetComponentFastPath(Type type, IntPtr oneFurtherThanResultValue);

    [SecuritySafeCritical]
    public unsafe T GetComponent<T>()
    {
        CastHelper<T> castHelper = default(CastHelper<T>);
        this.GetComponentFastPath(typeof(T), new IntPtr((void*)(&castHelper.onePointerFurtherThanT)));
        return castHelper.t;
    }
    .
    .
}

The C# code performs native calls, called Icalls to C++ methods that have been bound to the C# methods using the C# runtime library API. Bodyless (unimplemented) methods need either an extern, abstract or partial specifier as a rule so all internal calls are marked as extern. When the runtime sees a method with the [MethodImpl(MethodImplOptions.InternalCall)] attribute it knows it needs to make an Icall, so it looks up the function it has been bound to and jumps to that address.

An Icall does not need to be static in C# and automatically passes the this MonoObject of the component to the C++ handler function. If they are static then the this object is usually deliberately passed as a parameter using a C# shim method and making the shim method the static Icall. Using Icalls, types are not marshalled unless they are blittable types, meaning all other types are passed as MonoObject, MonoString etc.

Typically the C++ methods are functions or static methods but I think they can be non static methods as well, so long as they aren't virtual, because the address cannot be fixed by the runtime.

in UnityEngine.GameObject

public sealed class GameObject : Object
 {  
    .
    .
    public GameObject(string name)
    {
      GameObject.Internal_CreateGameObject(this, name);
    }

    public GameObject()
    {
      GameObject.Internal_CreateGameObject(this, (string) null);
    }

    [WrapperlessIcall]
    [TypeInferenceRule(TypeInferenceRules.TypeReferencedByFirstArgument)]
    [MethodImpl(MethodImplOptions.InternalCall)]
    public extern Component GetComponent(System.Type type);

    [WrapperlessIcall]
    [MethodImpl(MethodImplOptions.InternalCall)]
    private static extern void Internal_CreateGameObject([Writable] GameObject mono, string name);
    .
    .
 }

The C# constructor for the GameObject contains a call to a native method. The body of the constructor is run after initialisation of the C# object such that there is already a this pointer. Internal_CreateGameObject is the static shim function that is actually called.

Someone's example implementation of their own C++ Internal_CreateGameObject using mono:

bool GameObjectBinding::init()
{
    MonoClass *gameObjectClass = Mono::get().getClass("GameObject");
    gameObject_NativeID_Field = mono_class_get_field_from_name(gameObjectClass, "nativeID");

    MonoClass *transformClass = Mono::get().getClass("Transform");
    transform_NativeID_Field = mono_class_get_field_from_name(transformClass, "nativeID");

    mono_add_internal_call("GameEngine_CS.GameObject::internal_createGameObject", GameObjectBinding::createGameObject);
    mono_add_internal_call("GameEngine_CS.GameObject::internal_deleteGameObject", GameObjectBinding::deleteGameObject);
    mono_add_internal_call("GameEngine_CS.GameObject::internal_getGameObject", GameObjectBinding::getGameObject);

    mono_add_internal_call("GameEngine_CS.GameObject::internal_getTransform", GameObjectBinding::getTransform);

    return true;
}

void GameObjectBinding::createGameObject(MonoObject * monoGameObject)
{
    Object *newObject = LevelManager::get().getCurrentLevel()->createObject(0);
    mono_field_set_value(monoGameObject, gameObject_NativeID_Field, (void*)newObject->getID());
}

mono_add_internal_call has been used to bind this method to GameObjectBinding::createGameObject, to which the this pointer is passed as a MonoObject pointer. A native object is then created to represent the GameObject, and mono_field_set_value is then used to set the NativeID field of the C# object to the ID of the new native object. This way the native object can be accessed from the MonoObject which is the internal implementation of the C# object. The GameObject is represented by 2 objects essentially.

public sealed class GameObject : Object
 {
     .
     .
     private UInt32 nativeID;
     public UInt32 id { get { return nativeID; } }
     .
     .
 }

This field is bound in the runtime using

mono_set_dirs( "/Library/Frameworks/Mono.framework/Home/lib", "/Library/Frameworks/Mono.framework/Home/etc" );
mono_config_parse( nullptr );
const char* managedbinarypath = "C:/Test.dll";
MonoDomain* domain = mono_jit_init(managedbinarypath)
MonoAssembly* assembly = mono_domain_assembly_open (domain, managedbinarypath);
MonoImage* image = mono_assembly_get_image (assembly);
MonoClass* gameobjectclass = mono_class_from_name(image, "ManagedLibrary", "GameObject");
gameObject_NativeID_Field = mono_class_get_field_from_name( gameobjectclass, "nativeID" );

GetComponent<T>() passes typeof(T) to GetComponentFastPath (the native call) which passes the this pointer of the component as well. The native implementation of GetComponentFastPath will receive this as a MonoObject* and a MonoReflectionType* for the type. The bound C++ method will then call mono_reflection_type_get_type() on the MonoReflectionType* to get the MonoType* (here are the primitive types: https://github.com/samneirinck/cemono/blob/master/src/native/inc/mono/mono/metadata/blob.h), or for object types you can get the MonoClass* from MonoType* using mono_class_from_mono_type(). It will then get the game object that is attached to the Component and search the components that the object has in some internal data structure.

Someone's example implementation of their own C++ GetComponent using mono:

id ModuleScriptImporter::RegisterAPI()
{
    //GAMEOBJECT
    mono_add_internal_call("TheEngine.TheGameObject::CreateNewGameObject", (const void*)CreateGameObject);
    mono_add_internal_call("TheEngine.TheGameObject::AddComponent", (const void*)AddComponent);
    mono_add_internal_call("TheEngine.TheGameObject::GetComponent", (const void*)GetComponent);
}

MonoObject* ModuleScriptImporter::GetComponent(MonoObject * object, MonoReflectionType * type)
{
    return current_script->GetComponent(object, type);
}

MonoObject* CSharpScript::GetComponent(MonoObject* object, MonoReflectionType* type)
{
    if (!CheckMonoObject(object))
    {
        return nullptr;
    }

    if (currentGameObject == nullptr)
    {
        return nullptr;
    }

    MonoType* t = mono_reflection_type_get_type(type);
    std::string name = mono_type_get_name(t);

    const char* comp_name = "";

    if (name == "CulverinEditor.Transform")
    {
        comp_name = "Transform";
    }

    MonoClass* classT = mono_class_from_name(App->importer->iScript->GetCulverinImage(), "CulverinEditor", comp_name);
    if (classT)
    {
        MonoObject* new_object = mono_object_new(CSdomain, classT);
        if (new_object)
        {
            return new_object;
        }
    }
    return nullptr;
}

C# methods can be invoked from C++:

MonoMethodDesc* desc = mono_method_desc_new (const char *name, gboolean include_namespace);
MonoClass* class = mono_class_from_name (MonoImage *image, const char* name_space, const char *name);
MonoMethod* method = mono_method_desc_search_in_class (MonoMethodDesc *desc, MonoClass *klass);
MonoMethod* method = mono_method_desc_search_in_image (MonoMethodDesc *desc, MonoImage *image);
MonoObject* obj = mono_runtime_invoke (MonoMethod *method, void *obj, void **params,
                       MonoObject **exc);

See: https://gamedev.stackexchange.com/questions/115573/how-are-methods-like-awake-start-and-update-called-in-unity/183091#183091

Lewis Kelsey
  • 4,129
  • 1
  • 32
  • 42
0

There is a good introduction article to ECS: https://austinmorlan.com/posts/entity_component_system/ made differently than TOM__'s post.