Implementing Component system from Unity in c++

后端 未结 4 1679
有刺的猬
有刺的猬 2020-12-04 18:23

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 implement

4条回答
  •  独厮守ぢ
    2020-12-04 19:22

    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()
        {
            CastHelper castHelper = default(CastHelper);
            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() 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

提交回复
热议问题