Hazel game engine (123) C# script properties are visible in the editor panel, Mono hot update dll

If there are errors in the code, terminology, etc. in the text, please correct me

foreword

  • of this program

    In order to display the properties of the C# script class on the editor panel, and modify the value of the property, the C# script will be hot updated (supported by mono4.5api)

  • How to achieve

    Use mono's API to get and set the value of the property according to the class name

    Refer to the mono official api website: http://docs.go-mono.com/?link=xhtml%3adeploy%2fmono-api-class.html

  • implementation details

    • Since the name of the C# class type is a bit unclear at a glance, it is necessary to customize the class type and use map to convert the name of the C# type name.

      For example: C#'s float space name + class name is -> System.Single , we need to convert our custom class name **"Float". **

      The custom class name should be close to the C++ class name, which is easy to identify.

    • Consider the design:

      Each script has multiple attributes and values. It is reasonable to declare the map<attribute name, value> as a ScriptComponent component, but it is a bit troublesome to consider serialization and deserialization under the component, so it is discarded. this road.

      The solution of the video is to design a map<attribute name, struct{}>, where there are attribute values ​​in the struct, etc., and put this map in the ScriptClass class abstracted by the loaded C# class, so that there is no need to consider the issue of serialized data storage, but Every time the program starts, you need to get the properties of the C# script class , which are stored in the map

Text Narration: Implementation Ideas

  • A C# script corresponds to a ScriptClass class, and the ScriptClass class has **map (fieldmap)** to save the attributes and value types of this C# script.

    • After loading the dll, read the class name of the game script library, load the C# class to get the encapsulated MonoClass object

    • Then get all the properties of the C# class according to MonoClass (reflection)

    • Loop attributes to get a single MonoClassField object, and get the name, access permission, and type of C# attributes according to MonoClassField (reflection)

    • Determine whether the map stores this attribute according to the permission

  • When running the game, because the running script map in Section 120 stores the running ScriptInstance pointer, it is used to execute specific scripts that need to be updated in OnUpdate.

    • current design

      • ScriptEngine class

        Encapsulate loading and build Mono environment class

      • ScriptClass class

        Encapsulate and load C# classes into Mono classes

      • ScriptInstance class

        Encapsulates the Mono class object instantiated by the Mono class

    • ScriptInstance and ScriptClass classes

      1. ScriptInstance class (instance object Class cl = new Class()) has a reference to ScriptClass class (abstract class Class), and their relationship is equivalent to one-to-many .
      2. An instance object of ScriptInstance must have a ScriptClass object
      3. ScriptInstance is the encapsulated Mono class object created at runtime , and ScriptClass is the encapsulated Mono class created by loading the detection dll .

    Therefore, you can run the script map[UUID of the current entity] ->ScriptInstance->ScriptClass-> fieldmap

    This fieldmap stores all the properties of the current C# script

  • After getting the name of the corresponding attribute, you can use Mono's API to get and set the value of this attribute

Code description: idea + key code

key code

Mono API

// 获取所有属性
mono_class_get_fields()
// 得到单个属性
MonoClassField* field = mono_class_get_fields(monoClass, &iterator)
// 获取单个属性的名称
mono_field_get_name(field)
// 获取单个属性的权限
mono_field_get_flags(field)
// 获取单个属性的类类型
mono_field_get_type(field)    
// 设置和获取属性值
mono_field_get_value(m_Instance, field.ClassField, buffer);
mono_field_set_value(m_Instance, field.ClassField, (void*)value);

code ideas

  • A C# script corresponds to a ScriptClass class, and the ScriptClass class has **map (fieldmap)** to save the attributes and value types of this C# script.

    • After loading the dll, read the class name of the game script library, load the C# class to get the encapsulated MonoClass object

    • Then get all the properties of the C# class according to MonoClass (reflection)

    • Loop attributes to get a single MonoClassField object, and get the name, access permission, and type of C# attributes according to MonoClassField (reflection)

    • Determine whether the map stores this attribute according to the permission

    // 属性名称对应的结构体
    struct ScriptField {
          
          
        ScriptFieldType Type;
        std::string Name;
    
        MonoClassField* ClassField;
    };
    class ScriptClass {
          
          
        public:
        ScriptClass() = default;
        ScriptClass(const std::string& classNamespace, const std::string& className, bool isCore = false);		// 119.3. 创建一个MonoClass类
        MonoObject* Instantiate();// 119.4.创建一个由MonoClass类构成的mono对象并且初始化
        MonoMethod* GetMethod(const std::string& name, int parameterCount);	// 119.5.1 获取类的函数
        // 119.5.2 调用类的函数
        MonoObject* InvokeMethod(MonoObject* instance, MonoMethod* method, void** params = nullptr);
        // 123.属性
        const std::map<std::string, ScriptField>& GetFields() const {
          
           return m_Fields; }
        private:
        std::string m_ClassNamespace;
        std::string m_ClassName;
        MonoClass* m_MonoClass = nullptr;
    
        std::map<std::string, ScriptField> m_Fields; // map
    
        friend class ScriptEngine;
    };
    void ScriptEngine::LoadAssemblyClasses()
    {
          
          
        .....
        for (int32_t i = 0; i < numTypes; i++)
        {
          
          
            .....
            // 2.加载Dll中所有C#类
            MonoClass* monoClass = mono_class_from_name(s_Data->AppAssemblyImage, nameSpace, className);
    		.....
            // 123:读取脚本类的属性
            int fieldCount = mono_class_num_fields(monoClass);		
            HZ_CORE_WARN("{} has {} fields:", className, fieldCount);
            void* iterator = nullptr;
            // 获取所有属性,并得到单个属性
            while (MonoClassField* field = mono_class_get_fields(monoClass, &iterator))
            {
          
          
                const char* filedName = mono_field_get_name(field);// 获取单个属性的名称
                uint32_t flags = mono_field_get_flags(field);		// 获取单个属性的权限
                if (flags & FIELD_ATTRIBUTE_PUBLIC) {
          
           				// &按位与 1 1=1,1 0 = 0,0 1 = 0
                    MonoType* type = mono_field_get_type(field);	// 获取单个属性的类类型
                    ScriptFieldType fieldType = Utils::MonoTypeToScriptFieldType(type);
                    HZ_CORE_TRACE("	{}({})", filedName,Utils::ScriptFieldTypeToStirng(fieldType));
                    // 用Map存储这个属性
                    scriptClass->m_Fields[filedName] = {
          
           fieldType, filedName, field };
                }
            }
        }
    
  • When running the game, you can run the script map[the UUID of the current entity] -> ScriptInstance->ScriptClass-> fieldmap in the editing panel

    This fieldmap stores all the properties of the current C# script

    // 实体的脚本组件 mutable去除常量属性
    DrawComponent<ScriptComponent>("Script", entity, [entity](auto& component)mutable
    {
          
          
    	.....
    	if (ImGui::InputText("Class", buffer, sizeof(buffer))) {
          
          
    		component.ClassName = buffer;
    	}
    	// 123:c#脚本属性
    	// UUID->ScriptInstance->ScriptClass->fieldmap
    	Ref<ScriptInstance> scriptInstance = ScriptEngine::GetEntityScriptInstance(entity.GetUUID());
    	if (scriptInstance) {
          
          
            /
            /
            // 读取map中的属性
    		const auto& fields = scriptInstance->GetScriptClass()->GetFields();
    		for (const auto& [name, field] : fields)// 获取保存的属性名称
    		{
          
          
    			if (field.Type == ScriptFieldType::Float) {
          
          
    				float data = scriptInstance->GetFieldValue<float>(name);// 下一步有函数定义:获取属性值
    				if (ImGui::DragFloat(name.c_str(), &data)) {
          
          
    						scriptInstance->SetFieldValue(name, data);// 下一步有函数定义:设置属性值
    				}
    			}
    		}
    	}
    
  • After getting the name of the corresponding attribute, you can use Mono's API to get and set the value of this attribute

    // 获取属性值的api
    template<typename T>
    T GetFieldValue(const std::string& name) // 
    {
          
          
        bool success = GetFieldValueInternal(name, s_FieldValueBuffer);
        if (!success)
            return T();
        return *(T*)s_FieldValueBuffer;
    }
    bool ScriptInstance::GetFieldValueInternal(const std::string& name, void* buffer)
    {
          
          
        const auto& fields = m_ScriptClass->GetFields();
        auto it = fields.find(name);
        if (it == fields.end()) {
          
          
            return nullptr;
        }
        const ScriptField& field = it->second;
        //
        //
        // mono的API
        mono_field_get_value(m_Instance, field.ClassField, buffer); 
        return true;
    }
    
    // 设置属性值的api
    template<typename T>
    void SetFieldValue(const std::string& name,  T& value)
    {
          
          
        SetFieldValueInternal(name, &value);// 引用变量的地址 等于 被引用变量的地址
    }
    bool ScriptInstance::SetFieldValueInternal(const std::string& name, void* value)
    {
          
          
        const auto& fields = m_ScriptClass->GetFields();
        auto it = fields.find(name);
        if (it == fields.end()) {
          
          
            return false;
        }
        const ScriptField& field = it->second;
        //
        //
        // mono的API
        mono_field_set_value(m_Instance, field.ClassField, (void*)value);
        return true;
    }
    

Effect

  1. get attribute

Please add a picture description

  1. Set the speed attribute value when the game is running

    Please add a picture description

  2. Set the speed attribute value when the game is running

    Please add a picture description

  3. gift effect

    When the speed is adjusted to 0.1, wasd is obviously blocked, and when it is adjusted to 0.5, the speed is significantly improved (indicating that the hot update is successful)

    1. xiaoguo1232

Follow-up to optimize

  • This section is complete

    This section completes the properties of the C# script visible in the panel at runtime, and after modifying the value of the property, the value of the C# script property can be updated immediately

  • shortcoming

    However, the properties of the C# script are not displayed before running (during editing) , let alone modified.

  • should be optimized

    It should be realized that the properties of the C# script can be detected during editing , and the value can be set, and the value set during editing can be read at runtime.

  • Implementation ideas

    When editing, you can use map to store script attribute values, and wait until runtime to read the attribute values ​​set during editing according to the map.

Bugs found

  • SceneHierarchyPanel.cpp

    Write and display properties of C# scripts on the interface

    • Error screenshot

      Please add a picture description

    • Solution

      Declare lambda as mutable and remove const from the captured variable

    • Cause Analysis

      Since lambda does not modify the capture variable by default, the capture variable entity is assigned const , and the entity does not set the const GetUUID() function, so an error is reported

  • ScriptEngine.h

    Writing a template function program in the ScriptInstance class will report an error and red, as follows

    • Error screenshot

    • Solution

      After testing, you need to declare the order of ScriptClass, ScriptInstance, and ScriptEngine .

    • Cause Analysis

      The order in which errors were reported before was ScriptEngine, ScriptInstance, and ScriptClass.

      Because ScriptEngine uses ScriptInstance, and ScriptEngine is before ScriptInstance, so the class declaration cannot be found, and the order needs to be adjusted

  • MonoApi gets the value of the property

    In the video, Cherno directly uses the character pointer to obtain the value, saying that void* is not used because of buffering, so I tested using void* to obtain the value, after all, because monoapi is only a void* pointer

    // monoapi mono_field_get_value函数是void*指针
    MONO_API MONO_RT_EXTERNAL_ONLY void
    mono_field_get_value (MonoObject *obj, MonoClassField *field, void *value);
    
    • Error screenshot

      Please add a picture description

      As shown in the figure: use void* result to pass to the mono_field_get_value function

    • Cause Analysis

      void* result;A pointer was declared, but no memory was allocated for it . When trying mono_field_get_valueto store a value in resultthe memory pointed to by the function, since no memory was allocated, an access violation will result.

    • normal solution

      inline static void* result = malloc(8);// void数组
      template<typename T>
      T GetFieldValue(const std::string& name)
      {
              
              
          bool success = GetFieldValueInternal(name, result);// void数组
          if (!success)
              return T();
          return *(T*)result;
      }
      bool ScriptInstance::GetFieldValueInternal(const std::string& name, void* buffer)
      {
              
              
          const auto& fields = m_ScriptClass->GetFields();
          auto it = fields.find(name);
          if (it == fields.end()) {
              
              
              return nullptr;
          }
          const ScriptField& field = it->second;
          mono_field_get_value(m_Instance, field.ClassField, buffer);// void指针传入
          return true;
      }
      
    • Back to basics solution 1

      Just follow the video and change it to a character array

      inline static char s_FieldValueBuffer[8];// 字符数组
      template<typename T>
      T GetFieldValue(const std::string& name)
      {
              
              
          bool success = GetFieldValueInternal(name, s_FieldValueBuffer); // 注意区分,传入的实参不同
          if (!success)
              return T();
          return *(T*)s_FieldValueBuffer;
      }
      bool ScriptInstance::GetFieldValueInternal(const std::string& name, void* buffer)
      {
              
              
          const auto& fields = m_ScriptClass->GetFields();
          auto it = fields.find(name);
          if (it == fields.end()) {
              
              
              return nullptr;
          }
          const ScriptField& field = it->second;
          mono_field_get_value(m_Instance, field.ClassField, buffer);// 字符数组指针传入
          return true;
      }
      

Guess you like

Origin blog.csdn.net/qq_34060370/article/details/132266578