Hazel游戏引擎(120)将C#脚本融入ECS

文中若有代码、术语等错误,欢迎指正

前言

  • 此节目的

    为实现C#脚本WSAD能控制实体的位置变化

  • 如何实现

    使用118节C++内部调用C#的函数功能,实现C++调用C#脚本的OnCreate、OnUpdate函数的调用。

    使用119节C#内部调用C++的函数功能,实现C#的WSAD能调用C++函数修改实体的位置。

实现此节目的思路

C#调用C++的函数

  • C#中写好声明要调用C++函数

    • 根据UUID获取实体位置
    • 根据UUID设置实体位置
    • WSAD按键是否按下
  • C++中定义好(C#中声明的)外部调用的函数

    • 当前场景根据UUID获取到实体得到它的位置,通过指针返回给C#
    • 当前场景根据UUID获取到实体设置它的位置
    • 调用系统已经实现的事件系统判断WSAD按键是否按下
  • C#的OnUpdate函数

    每帧获取实体的当前位置,检测根据WASD事件改变实体的位置,把新位置传给C++

C++调用C#的函数

给出名词解释

struct ScriptEngineData {
    
    
    ....
    ScriptClass EntityClass;		// 存储C#父类Entity的Mono类
	// 所有C#脚本map (脚本map)		,存储封装的Mono类
    std::unordered_map<std::string, Ref<ScriptClass>> EntityClasses;
    // 需要运行的C#脚本map(运行脚本map),存储封装的Mono类对象
    std::unordered_map<UUID, Ref<ScriptInstance>> EntityInstances;	
    ....
};
  • ScriptEngine类

    封装 加载构建Mono环境类

  • ScriptClass类

    封装 加载C#类成Mono类 的类

  • ScriptInstance类

    封装 由Mono类实例化的Mono类对象

具体步骤

  • 找到dll里所有继承Entity的类,表明这是脚本类,得到对应的封装的Mono类(119封装的)

  • 脚本map存储所有封装的mono类

  • 在运行场景开始时

    1. 循环遍历当前所有具有脚本组件的实体

    2. 运行脚本map存储这些封装的mono类对象(用封装的Mono类实例化)

      (key是实体的UUID)

    3. 调用C#类的OnCreate函数,存储OnCreate、OnUpdate函数

    4. C++调用C#父类Entity的构造函数传入当前实体的UUID给C#

      C#脚本有UUID后,C#的一个脚本才能 与 拥有这个C#脚本的C++实体联系在一起

  • 在运行场景的update函数

    循环遍历当前所有具有脚本组件的实体,根据UUID在运行脚本map找到这个封装的mono类对象,并调用C#类的OnUpdate函数

总结复习C++调用C#函数Mono的步骤(118节)

有利于理解本节重点的代码

  1. 初始化Mono准备,需得到MonoDomain、MonoAssembly、MonoImage

    // 0.1设置程序集装配路径(复制的4.5版本的路径)
    mono_set_assemblies_path("mono/lib");
    // 0.2声明根域
    MonoDomain* rootDomian = mono_jit_init("HazelJITRuntime");
    // 存储root domain指针
    s_Data->RootDomain = rootDomian;
    // 0.3创建一个应用 domain
    s_Data->AppDomain = mono_domain_create_appdomain("HazelScriptRuntime", nullptr);
    mono_domain_set(s_Data->AppDomain, true);
    // 0.4加载c#项目导出的dll程序集
    s_Data->CoreAssembly = LoadCSharpAssembly("Resources/Scripts/GameEngine-ScriptCore.dll");
    // 0.5得到MonoImage对象
    MonoImage* assemblyImage = mono_assembly_get_image(s_Data->CoreAssembly);
    
  2. 根据命名空间、类名、MonoImage得到加载C#的Mono类=>可以理解为创建类Class

    MonoClass* monoClass = mono_class_from_name(assemblyImage, "Hazel", "Main");
    
  3. 根据MonoClass和当前应用domain得到MonoObject=>可以理解为Class cls = new Class()得到类的实例,会调用类的构造函数

    MonoObject* instance = mono_object_new(s_Data->AppDomain, monoClass);
    mono_runtime_object_init(instance);// 这里初始化会调用C#类的构造函数
    
  4. 根据MonoClass获取这个类的函数,根据MonoObject和函数名称调用函数=>可以理解为**cls.Func();**调用类的函数

    // 3.1根据MonoClass获取这个类的函数
    MonoMethod* printMessageFunc = mono_class_get_method_from_name(monoClass, "PrintMessage", 0);
    // 3.2根据MonoObject和函数名称调用函数
    mono_runtime_invoke(printMessageFunc, instance, nullptr, nullptr);
    

代码思路+相关代码

代码思路

C#调用C++的函数

比较简单,只需要列出相关的C#代码就能理解

  • Entity父类

    using System;
    using System.Runtime.CompilerServices;
    namespace Hazel
    {
          
          
        public class Entity
        {
          
          
            public readonly ulong ID;   // 实体的UUID
            protected Entity() {
          
           Console.WriteLine("Entity()"); ID = 0; }
            internal Entity(ulong id)
            {
          
          
                Console.WriteLine("Entity(ulong id)"); ID = id; }// C++通过构造函数传入实体的UUID
    
            public Vector3 Translation {
          
          
                get
                {
          
          
                    // Translation get访问器 是调用C++的内部函数 获取 实体的位置
                    InternalCalls.TransformComponent_GetTranslation(ID, out Vector3 result);
                    return result;
                }
                set
                {
          
          
                    // Translation set访问器 是调用C++的内部函数 设置 实体的位置
                    InternalCalls.TransformComponent_SetTranslation(ID, ref value);
                } 
            }
        }
    }
    
  • 脚本类

    using System;
    using Hazel;
    namespace Sandbox{
          
          
        public class Player : Entity {
          
          
            public Player(){
          
          
                Console.WriteLine("Player()");
            }
            void OnCreate(){
          
          
                Console.WriteLine($"Player.OnCreate() - {
            
            ID}");
            }
            void OnUpdate(float ts){
          
          
                //Console.WriteLine($"Player.OnUpdate() - {ts}");
                float speed = 1.0f;
                Vector3 velocity = Vector3.Zero;
    			//
                // 内部调用函数,事件是否触发//
                if (Input.IsKeyDown(KeyCode.W)){
          
          
                    velocity.Y = 1.0f;
                }
                else if (Input.IsKeyDown(KeyCode.S)){
          
          
                    velocity.Y = -1.0f;
                }
                else if (Input.IsKeyDown(KeyCode.A)){
          
          
                    velocity.X = -1.0f;
                }
                else if (Input.IsKeyDown(KeyCode.D)){
          
          
                    velocity.X = 1.0f;
                    Console.WriteLine("press the D key");
                }
                velocity *= speed;
                
                
                // Translation get访问器 是调用C++的内部函数 获取 实体的位置
                Vector3 translation = Translation; 
                translation += velocity * ts;
                // Translation set访问器 是调用C++的内部函数 设置 实体的位置
                Translation = translation;          
            }
        }
    }
    
  • C#声明调用C++内部函数

    using System;
    using System.Runtime.CompilerServices;
    namespace Hazel{
          
          
        public static class InternalCalls{
          
          
            [MethodImplAttribute(MethodImplOptions.InternalCall)]
            internal extern static bool Input_IsKeyDown(KeyCode keycode);
            [MethodImplAttribute(MethodImplOptions.InternalCall)]
            internal extern static void TransformComponent_GetTranslation(ulong entityID, out Vector3 translation);
            [MethodImplAttribute(MethodImplOptions.InternalCall)]
            internal extern static void TransformComponent_SetTranslation(ulong entityID, ref Vector3 translation);
        }
    }
    namespace Hazel{
          
          
        public class Input {
          
          
            public static bool IsKeyDown(KeyCode keyCode) {
          
          
                return InternalCalls.Input_IsKeyDown(keyCode);
            }
        }
    }
    

    对应的C++的内部函数

    static void TransformComponent_GetTranslation(UUID entityID, glm::vec3* outTranslation) {
          
          
        Scene* scene = ScriptEngine::GetSceneContext();// 获取场景
        HZ_CORE_ASSERT(scene);
        Entity entity = scene->GetEntityByUUID(entityID); // 根据C#传入的UUID得到Entity
        HZ_CORE_ASSERT(entity);
    
        *outTranslation = entity.GetComponent<TransformComponent>().Translation;// 返回 Entity的位置
    }
    static void TransformComponent_SetTranslation(UUID entityID, glm::vec3* translation) {
          
          
        Scene* scene = ScriptEngine::GetSceneContext();	// 获取场景
        HZ_CORE_ASSERT(scene);
        Entity entity = scene->GetEntityByUUID(entityID);// 根据C#传入的UUID得到Entity
        HZ_CORE_ASSERT(entity);
    
        entity.GetComponent<TransformComponent>().Translation = *translation;// 设置 Entity的位置
    }
    // 判断按键是否按下
    static bool Input_IsKeyDown(KeyCode keycode) {
          
          
        return Input::IsKeyPressed(keycode);
    }
    

C++调用C#的函数(C++项目的代码)

  • 找到dll里所有继承Entity的类,表明这是脚本类,得到对应的封装的Mono类(119封装的)

    并用脚本map存储所有封装的mono类(用封装的Mono类实例化)

    void ScriptEngine::LoadAssemblyClasses(MonoAssembly* assembly)
    {
          
          
        s_Data->EntityClasses.clear();
    
        MonoImage* image = mono_assembly_get_image(assembly);
        const MonoTableInfo* typeDefinitionsTable = mono_image_get_table_info(image, MONO_TABLE_TYPEDEF);
        int32_t numTypes = mono_table_info_get_rows(typeDefinitionsTable);
        // 1.加载Entity父类
        MonoClass* entityClass = mono_class_from_name(image, "Hazel", "Entity");
    
        for (int32_t i = 0; i < numTypes; i++)
        {
          
          
            uint32_t cols[MONO_TYPEDEF_SIZE];
            mono_metadata_decode_row(typeDefinitionsTable, i, cols, MONO_TYPEDEF_SIZE);
    
            const char* nameSpace = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAMESPACE]);
            const char* name = mono_metadata_string_heap(image, cols[MONO_TYPEDEF_NAME]);
            std::string fullName;
            if (strlen(nameSpace) != 0) {
          
          
                fullName = fmt::format("{}.{}", nameSpace, name);
            }
            else {
          
          
                fullName = name;
            }
            // 2.加载Dll中所有C#类
            MonoClass* monoClass = mono_class_from_name(image, nameSpace, name);
            if (monoClass == entityClass) {
          
          // entity父类不保存
                continue;
            }
            // 3.判断当前类是否为Entity的子类
            bool isEntity = mono_class_is_subclass_of(monoClass, entityClass, false); // 这个c#类是否为entity的子类
            if (isEntity) {
          
          
                // 存入封装的Mono类对象
                // 3.1是就存入脚本map中
                s_Data->EntityClasses[fullName] = CreateRef<ScriptClass>(nameSpace, name);
            }
        }
    }
    
  • 在运行场景开始前,循环遍历当前所有具有脚本组件的实体

    void Scene::OnRuntimeStart()
    {
          
          
        OnPhysics2DStart();
        {
          
          // 脚本
            ScriptEngine::OnRuntimeStart(this);
            
            auto view = m_Registry.view<ScriptComponent>();
            for (auto e : view) {
          
          
                Entity entity = {
          
           e, this };
                ScriptEngine::OnCreateEntity(entity);// 实例化实体拥有的C#脚本
            }
        }
    }
    

    运行脚本map存储这些封装的mono类对象(用封装的Mono类实例化)

    • key是实体的UUID

    再调用C#类的OnCreate函数(初始化)

    void ScriptEngine::OnCreateEntity(Entity entity)
    {
          
          
        const auto& sc = entity.GetComponent<ScriptComponent>();		// 得到这个实体的组件
        if (ScriptEngine::EntityClassExists(sc.ClassName)) {
          
          			// 组件的脚本名称是否正确
            Ref<ScriptInstance> instance = CreateRef<ScriptInstance>(s_Data->EntityClasses[sc.ClassName], entity);// 实例化类对象,并存储OnCreate、OnUpdate函数,调用父类Entity的构造函数,传入实体的UUID
            s_Data->EntityInstances[entity.GetUUID()] = instance;	// 运行脚本map存储这些ScriptInstance(类对象)
            instance->InvokeOncreate();								// 调用C#的OnCreate函数
        }
    }
    

    存储OnCreate、OnUpdate函数,并调用C#父类Entity的构造函数传入当前实体的UUID给C#

    (C#脚本有UUID后,C#的一个脚本才能 与 拥有这个C#脚本的C++实体联系在一起)

    ScriptInstance::ScriptInstance(Ref<ScriptClass> scriptClass, Entity entity)
    :m_ScriptClass(scriptClass)
    {
          
          
        // 获取Sandbox Player类构成的MonoObject对象,相当于new Sandbox.Player()
        m_Instance = scriptClass->Instantiate();	
    
        m_Constructor = s_Data->EntityClass.GetMethod(".ctor", 1);// 获取C#Entity类的构造函数
        m_OnCreateMethod = scriptClass->GetMethod("OnCreate", 0);// 获取并存储Sandbox.Player类的函数
        m_OnUpdateMethod = scriptClass->GetMethod("OnUpdate", 1);
        // 调用C#Entity类的构造函数
        {
          
          
        UUID entityID = entity.GetUUID();
        void* param = &entityID;
        m_ScriptClass->InvokeMethod(m_Instance, m_Constructor, &param);// 第一个参数传入的是Entity子类(Player)构成的mono对象
        }
    }
    
  • 在运行场景的update函数,循环遍历当前所有具有脚本组件的实体

    void Scene::OnUpdateRuntime(Timestep ts)
    {
          
          
        // 脚本
        {
          
          
            ScriptEngine::OnRuntimeStart(this);
            // 实例化实体中的C#脚本
            auto view = m_Registry.view<ScriptComponent>();
            for (auto e : view) {
          
          
                Entity entity = {
          
           e, this };
                ScriptEngine::OnUpdateEntity(entity, ts);
            }
        }
    

    根据UUID在运行脚本map找到这个封装的mono类对象,并调用C#类的OnUpdate函数

    void Hazel::ScriptEngine::OnUpdateEntity(Entity entity, Timestep ts)
    {
          
          
        UUID entityUUID = entity.GetUUID();							// 得到这个实体的UUID
        HZ_CORE_ASSERT(s_Data->EntityInstances.find(entityUUID) != s_Data->EntityInstances.end());
    
        // 根据UUID获取到ScriptInstance的指针
        Ref<ScriptInstance> instance = s_Data->EntityInstances[entityUUID];
        instance->InvokeOnUpdate((float)ts);							// 调用C#的OnUpdate函数
    }
    

其它要写的代码(省略)

  • 定义C#脚本组件
  • 面板显示脚本组件
  • 序列化和解析Yaml文件加上脚本组件

效果

请添加图片描述

Cherno遇到的BUG

  • C++把实体的UUID作为实参传给C#脚本类的构造函数,以便C#脚本能与实体联系起来。

    C#中Sandbox.Player脚本继承Entity类,Entity类有带参的构造函数,Player类没有带参的构造函数

    public class Entity
    {
          
          
        public readonly ulong ID;   // 实体的UUID
        protected Entity() {
          
           Console.WriteLine("Entity()"); ID = 0; }
        internal Entity(ulong id)
        {
          
          ID = id; }// C++此构造函数传入实体的UUID
    

    若在C++中使用Player类的构造函数,把UUID传给Player

    // 获取Sandbox Player类构成的MonoObject对象,相当于new Sandbox.Player()
    m_Instance = scriptClass->Instantiate();	
    
    m_Constructor = scriptClass.GetMethod(".ctor", 1);// 获取C#Player类的构造函数
    m_OnCreateMethod = scriptClass->GetMethod("OnCreate", 0);// 获取并存储Sandbox.Player类的函数
    m_OnUpdateMethod = scriptClass->GetMethod("OnUpdate", 1);
    // 调用C#Player类的构造函数
    {
          
          
        UUID entityID = entity.GetUUID();
        void* param = &entityID;
        m_ScriptClass->InvokeMethod(m_Instance, m_Constructor, &param);
    }
    

    是不行的,因为Player并没有带参的构造函数,毕竟在C#本地执行new Player带参的构造函数也不行的

    请添加图片描述

    所以C++的代码,需调用Entity类的构造函数,参数用Player的实例mono对象

    // 获取Sandbox Player类构成的MonoObject对象,相当于new Sandbox.Player()
    m_Instance = scriptClass->Instantiate();	
    
    // 这里不一样,是获取父类Entity的构造函数
    m_Constructor = s_Data->EntityClass.GetMethod(".ctor", 1);// 获取C#Entity类的构造函数
    m_OnCreateMethod = scriptClass->GetMethod("OnCreate", 0);// 获取并存储Sandbox.Player类的函数
    m_OnUpdateMethod = scriptClass->GetMethod("OnUpdate", 1);
    // 调用C#Entity类的构造函数
    {
          
          
        UUID entityID = entity.GetUUID();
        void* param = &entityID;
        m_ScriptClass->InvokeMethod(m_Instance, m_Constructor, &param);// 第一个参数传入的是Entity子类(Player)构成的mono对象
    }
    

    关于s_Data->EntityClass是父类Mono对象,是一开始加载C#dll时特别加载的

    // S_Data结构体
    struct ScriptEngineData {
          
          
    	......
    
        ScriptClass EntityClass;// 存储C#父类Entity的Mono类
    
    	......
    };
    void ScriptEngine::Init()
    {
          
          
        s_Data = new ScriptEngineData();
        // 初始化mono
        InitMono();
        // 加载c#程序集
        LoadAssembly("Resources/Scripts/GameEngine-ScriptCore.dll");				// 核心库
        LoadAppAssembly("SandboxProject/Assets/Scripts/Binaries/Sandbox.dll");// 游戏脚本库
    
        // 加载父类是entity的脚本类
        LoadAssemblyClasses();
    
        // 创建加载Entity父类-为了在调用OnCreate函数之前把UUID传给C#Entity的构造函数
        s_Data->EntityClass = ScriptClass("Hazel", "Entity", true);
    

猜你喜欢

转载自blog.csdn.net/qq_34060370/article/details/132266481