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

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

文章目录

  • 前言
  • 实现此节目的思路
    • C#调用C++的函数
    • C++调用C#的函数
      • 给出名词解释
      • 具体步骤
    • 总结复习C++调用C#函数Mono的步骤(118节)
  • 代码思路+相关代码
    • 代码思路
      • C#调用C++的函数
      • C++调用C#的函数(C++项目的代码)
    • 其它要写的代码(省略)
  • 效果
  • Cherno遇到的BUG

前言

  • 此节目的

    为实现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;   // 实体的UUIDprotected Entity() { Console.WriteLine("Entity()"); ID = 0; }internal Entity(ulong id){Console.WriteLine("Entity(ulong id)"); ID = id; }// C++通过构造函数传入实体的UUIDpublic 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得到EntityHZ_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得到EntityHZ_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的构造函数,传入实体的UUIDs_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();							// 得到这个实体的UUIDHZ_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;   // 实体的UUIDprotected 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();// 初始化monoInitMono();// 加载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);
    


本文来自互联网用户投稿,文章观点仅代表作者本人,不代表本站立场,不承担相关法律责任。如若转载,请注明出处。 如若内容造成侵权/违法违规/事实不符,请点击【内容举报】进行投诉反馈!

相关文章

立即
投稿

微信公众账号

微信扫一扫加关注

返回
顶部