当前位置: 首页 > article >正文

ILRuntime热更新通过Addressables加载DLL

需求

通过热更的方式改变游戏物体的运动状态

Addressables资源加载可以看下之前写的“Unity资源打包Addressable AA包”

1.安装插件

首先需要在项目的Packages/manifest.json中,添加ILRuntime的源信息,在这个文件的dependencies节点前和列表中增加以下内容

{
  "scopedRegistries": [
    {
      "name": "ILRuntime",
      "url": "https://registry.npmjs.org",
      "scopes": [
        "com.ourpalm"
      ]
    }
  ],
  "dependencies": {
    "com.ourpalm.ilruntime": "1.6.0",

2.允许非安全代码

Edit > Project Settings > Player中勾选Allow 'unsafe' code

3.创建程序集

ILRuntime热更新简单来说

之前写的代码都打入一个包内了,比如打包的Windows程序下面路径内MyILRuntime_Data\Managed\Assembly-CSharp.dll

现在分成不同的包,并且是在程序运行后再加载需要热更新的程序包。

1.创建两个文件夹

在Assets中创建一个Script文件夹,

Script文件夹中再创建

        一个Hotfix文件夹,这里存放热更新的代码

        一个DLL文件夹,这里存放上面代码打包的DLL文件

2.创建一个程序集

在Hotfix文件夹中右键 Create > Assembly Definition 创建一个程序集,这里名为Unity.Hotfix。

此时此文件夹中的代码都会被打包到一起。

在Hotfix文件夹中增加一个Hot.cs文件。

\Library\ScriptAssemblies 文件夹中会多出一个Unity.Hotfix.dll文件,此文件夹中的脚本都会被打包到此文件夹中。

3.复制程序集DLL文件到指定文件夹

创建BuildHotfixEditor.cs文件到Assets > Editor 文件夹下,这个路径Editor是必须的。

using UnityEngine;
using UnityEditor;
using System.IO;

//Unity 自带特性 每次编译完脚本后都会自动编译(编译器模式下)
[InitializeOnLoad]
public class BuildHotfixEditor 
{
    //程序集路径
    const string scriptAssemblies = "Library/ScriptAssemblies";
    //目标路径
    const string codeDir = "Assets/Script/DLL";
    //hotfixdll
    const string hotfixDll = "Unity.Hotfix.dll";
    //Hotfixpdb
    const string hotfixPdb = "Unity.Hotfix.pdb";
    static BuildHotfixEditor() 
    {
        string fixDll = Path.Combine(codeDir,$"{hotfixDll}.txt");
        string fixPdb = Path.Combine(codeDir,$"{hotfixPdb}.txt");
        //开始复制
        File.Copy(Path.Combine(scriptAssemblies,hotfixDll),fixDll,true);
        File.Copy(Path.Combine(scriptAssemblies,hotfixPdb),fixPdb,true);
        //刷新
        AssetDatabase.Refresh();
        Debug.Log("程序集拷贝完成");
    }
}

 Unity编辑器会把程序集自动保存在\Library\ScriptAssemblies路径下

复制程序集的两个文件"Unity.Hotfix.dll"、"Unity.Hotfix.pdb"到Assets/Script/DLL指定目录下(方便Addressables加载)

复制过程中增加了.txt后缀,这是因为Addressables不支持.dll .pdb作为后缀的资源

整个过程是每次编辑代码后自动完成的

4.配置程序集配置文件

Assembly Definition References

配置此程序集会引用到的其他程序集

允许使用非安全代码

Allow 'unsafe' Code

4.编写测试代码

1.调用静态方法

Hot.cs 位于Assets\Script\Hotfix 热更新代码

可以传入两个参数的静态方法

using UnityEngine;
namespace Unity.Hotfix
{
    public class Hot
    {
        public static void Log(string str,string str1)
        {
            Debug.Log("Log:" + str+":"+str1);
        }
    }
}

Hello.cs 位于Assets\Script 正常Unity中的代码

决定了什么时候加载和使用热更新代码

using System.IO;
using ILRuntime.Mono.Cecil.Pdb;
using UnityEngine;
using UnityEngine.AddressableAssets;

public class Hello : MonoBehaviour
{
    MemoryStream hotfixdllstream;
    MemoryStream hotfixpdbstream;

    private UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<TextAsset> handle_dll;
    private UnityEngine.ResourceManagement.AsyncOperations.AsyncOperationHandle<TextAsset> handle_pdb;
    byte[] dll;
    bool dllOK = false;
    byte[] pdb;
    bool pdbOK = false;
    void Start()
    {
        handle_dll = Addressables.LoadAssetAsync<TextAsset>("Assets/Script/DLL/Unity.Hotfix.dll.txt");
        handle_dll.Completed += (obj) =>
        {
            dllOK = true;
            dll = obj.Result.bytes;
            if (pdbOK)
                LoadEnd();
        };

        handle_pdb = Addressables.LoadAssetAsync<TextAsset>("Assets/Script/DLL/Unity.Hotfix.pdb.txt");
        handle_pdb.Completed += (obj) =>
        {
            pdbOK = true;
            pdb = obj.Result.bytes;
            if (dllOK)
                LoadEnd();
        };

    }

    public void LoadEnd()
    {
        hotfixdllstream = new MemoryStream(dll);
        hotfixpdbstream = new MemoryStream(pdb);
        var appDomain = ILRuntimeAppDomainManager.Instance.AppDomain;
        appDomain.LoadAssembly(hotfixdllstream, hotfixpdbstream, new PdbReaderProvider());
        appDomain.Invoke("Unity.Hotfix.Hot", "Log", null, new string[] { "Hello ILRuntime", "热更新" });
    }
}

其中void Start()中都是之前写的“Unity资源打包Addressable AA包”中的内容

通过Addressable加载Hotfix程序集

加载到程序中转换成MemoryStream

通过appDomain.LoadAssembly(hotfixdllstream, hotfixpdbstream, new PdbReaderProvider());方法加载

最后appDomain.Invoke("Unity.Hotfix.Hot", "Log", null, new string[] { "Hello ILRuntime", "热更新" });调用热更中的Log方法

其中需要实例化一个AppDomain类,可以把它作为一个单例。

ILRuntimeAppDomainManager.cs 位于Assets\Script

using System.Collections.Generic;
using ILRuntime.CLR.Method;
using ILRuntime.CLR.TypeSystem;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;
using ILRuntime.Runtime.Stack;
using UnityEngine;

public class ILRuntimeAppDomainManager
{
    private static ILRuntimeAppDomainManager _instance;
    private AppDomain _appDomain;

    // 私有构造函数,确保单例模式
    private ILRuntimeAppDomainManager()
    {
        _appDomain = new AppDomain();
        _appDomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());
        SetupCLRRedirection();
    }

    unsafe void SetupCLRRedirection()
    {
        //这里面的通常应该写在InitializeILRuntime,这里为了演示写这里
        var arr = typeof(GameObject).GetMethods();
        foreach (var i in arr)
        {
            if (i.Name == "AddComponent" && i.GetGenericArguments().Length == 1)
            {
                _appDomain.RegisterCLRMethodRedirection(i, AddComponent);
            }
        }
    }

    unsafe static StackObject* AddComponent(ILIntepreter __intp, StackObject* __esp, IList<object> __mStack, CLRMethod __method, bool isNewObj)
    {
        //CLR重定向的说明请看相关文档和教程,这里不多做解释
        ILRuntime.Runtime.Enviorment.AppDomain __domain = __intp.AppDomain;

        var ptr = __esp - 1;
        //成员方法的第一个参数为this
        GameObject instance = StackObject.ToObject(ptr, __domain, __mStack) as GameObject;
        if (instance == null)
            throw new System.NullReferenceException();
        __intp.Free(ptr);

        var genericArgument = __method.GenericArguments;
        //AddComponent应该有且只有1个泛型参数
        if (genericArgument != null && genericArgument.Length == 1)
        {
            var type = genericArgument[0];
            object res;
            if(type is CLRType)
            {
                //Unity主工程的类不需要任何特殊处理,直接调用Unity接口
                res = instance.AddComponent(type.TypeForCLR);
            }
            else
            {
                //热更DLL内的类型比较麻烦。首先我们得自己手动创建实例
                var ilInstance = new ILTypeInstance(type as ILType, false);//手动创建实例是因为默认方式会new MonoBehaviour,这在Unity里不允许
                //接下来创建Adapter实例
                var clrInstance = instance.AddComponent<MonoBehaviourAdapter.Adaptor>();
                //unity创建的实例并没有热更DLL里面的实例,所以需要手动赋值
                clrInstance.ILInstance = ilInstance;
                clrInstance.AppDomain = __domain;
                //这个实例默认创建的CLRInstance不是通过AddComponent出来的有效实例,所以得手动替换
                ilInstance.CLRInstance = clrInstance;

                res = clrInstance.ILInstance;//交给ILRuntime的实例应该为ILInstance

                clrInstance.Awake();//因为Unity调用这个方法时还没准备好所以这里补调一次
            }

            return ILIntepreter.PushObject(ptr, __mStack, res);
        }

        return __esp;
    }

    // 获取单例实例的属性
    public static ILRuntimeAppDomainManager Instance
    {
        get
        {
            if (_instance == null)
            {
                _instance = new ILRuntimeAppDomainManager();
            }
            return _instance;
        }
    }

    // 获取AppDomain实例的属性
    public AppDomain AppDomain
    {
        get { return _appDomain; }
    }
}

 这里本来只需要实现一个单例即可

下面些代码是为热更中实现MonoBehaviour添加的代码

_appDomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());

SetupCLRRedirection();

unsafe void SetupCLRRedirection()

unsafe static StackObject* AddComponent

MonoBehaviourAdapter.cs 位于Assets\Script

using UnityEngine;
using System;
using ILRuntime.Runtime.Enviorment;
using ILRuntime.Runtime.Intepreter;
using ILRuntime.CLR.Method;


public class MonoBehaviourAdapter : CrossBindingAdaptor
{
    public override Type BaseCLRType
    {
        get
        {
            return typeof(MonoBehaviour);
        }
    }

    public override Type AdaptorType
    {
        get
        {
            return typeof(Adaptor);
        }
    }

    public override object CreateCLRInstance(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
    {
        return new Adaptor(appdomain, instance);
    }
    //为了完整实现MonoBehaviour的所有特性,这个Adapter还得扩展,这里只抛砖引玉,只实现了最常用的Awake, Start和Update
    public class Adaptor : MonoBehaviour, CrossBindingAdaptorType
    {
        ILTypeInstance instance;
        ILRuntime.Runtime.Enviorment.AppDomain appdomain;

        public Adaptor()
        {

        }

        public Adaptor(ILRuntime.Runtime.Enviorment.AppDomain appdomain, ILTypeInstance instance)
        {
            this.appdomain = appdomain;
            this.instance = instance;
        }

        public ILTypeInstance ILInstance { get { return instance; } set { instance = value; } }

        public ILRuntime.Runtime.Enviorment.AppDomain AppDomain { get { return appdomain; } set { appdomain = value; } }

        IMethod mAwakeMethod;
        bool mAwakeMethodGot;
        public void Awake()
        {
            //Unity会在ILRuntime准备好这个实例前调用Awake,所以这里暂时先不掉用
            if (instance != null)
            {
                if (!mAwakeMethodGot)
                {
                    mAwakeMethod = instance.Type.GetMethod("Awake", 0);
                    mAwakeMethodGot = true;
                }

                if (mAwakeMethod != null)
                {
                    appdomain.Invoke(mAwakeMethod, instance, null);
                }
            }
        }

        IMethod mStartMethod;
        bool mStartMethodGot;
        void Start()
        {
            if (!mStartMethodGot)
            {
                mStartMethod = instance.Type.GetMethod("Start", 0);
                mStartMethodGot = true;
            }

            if (mStartMethod != null)
            {
                appdomain.Invoke(mStartMethod, instance, null);
            }
        }

        IMethod mUpdateMethod;
        bool mUpdateMethodGot;
        void Update()
        {
            if (!mUpdateMethodGot)
            {
                mUpdateMethod = instance.Type.GetMethod("Update", 0);
                mUpdateMethodGot = true;
            }

            if (mUpdateMethod != null)
            {
                appdomain.Invoke(mUpdateMethod, instance, null);
            }
        }

        public override string ToString()
        {
            IMethod m = appdomain.ObjectType.GetMethod("ToString", 0);
            m = instance.Type.GetVirtualMethod(m);
            if (m == null || m is ILMethod)
            {
                return instance.ToString();
            }
            else
                return instance.Type.FullName;
        }
    }
}

打包调试

编写好上面的代码后,Hotfix.dll.txt和Hotfix.pdb.txt会自动复制到Assets>Script>DLL文件夹中

Addressables Groups中新创建一个Script分组,记得修改成远程加载

关于远程打包看另外两篇文章 "Unity资源打包Addressable AA包" "Addressables资源打包(AA包)代码中改变远程地址"

运行程序,这里自行查看log。

可以用Log Viewer.unitypackage或SRDebugger1.12.1.unitypackage工具

热更

修改位于Assets\Script\Hotfix 热更新代码Hot.cs

代码的搬运是自动完成的只要在Addressables中Build即可

再将资源复制到服务器路径中即可

2.控制游戏物体移动

首先根据另外两篇文章 "Unity资源打包Addressable AA包" "Addressables资源打包(AA包)代码中改变远程地址"实现游戏物体的远程加载

接着上面的脚本增加两个脚本,上面的脚本中已经增加MonoBehaviourAdapter这样的适配器

ControlMove.cs 放在Assets\Script中并挂载在游戏物体上

using UnityEngine;
using ILRuntime.Runtime.Enviorment;
public class ControlMove : MonoBehaviour
{
    AppDomain appDomain;
    private void Start()
    { 
        appDomain = ILRuntimeAppDomainManager.Instance.AppDomain;  
        CallHotfixMethod(); 
    }
    public void CallHotfixMethod()
    {
        appDomain.Invoke("Unity.Hotfix.AddComponent", "ADD", null, gameObject);
    }
}

HotMove.cs 放在Assets\Script\Hotfix文件夹中 通过上面的代码动态添加到游戏物体上

using System;
using System.Collections.Generic;
using UnityEngine;

namespace Unity.Hotfix
{
    class SomeMonoBehaviour : MonoBehaviour
    {
        float time;
        void Awake()
        {
            Debug.Log("!! SomeMonoBehaviour.Awake");
        }

        void Start()
        {
            Debug.Log("!! SomeMonoBehaviour.Start");
        }

        void Update()
        {
            this.transform.Translate(new Vector3(0,1*Time.deltaTime,0));
        }

        public void Test()
        {
            Debug.Log("SomeMonoBehaviour");
        }
    }

    public class AddComponent
    {
        public static void ADD(GameObject go)
        {
            go.AddComponent<SomeMonoBehaviour>();
        }
    }
}

修改热更的代码,进行替换可以改变游戏物体的运动方式


http://www.kler.cn/a/392823.html

相关文章:

  • vue2+ element ui 集成pdfjs-dist
  • macOS解决U盘装完系统容量变小的问题
  • 设计模式-七个基本原则之一-迪米特法则 + 案例
  • 【Spring】@Autowired与@Resource的区别
  • 翼鸥教育:从OceanBase V3.1.4 到 V4.2.1,8套核心集群升级实践
  • NCC前端调用查询弹框
  • DAY113代码审计-PHPTP框架微P系统漏审项目等
  • 初识机器学习
  • vue el-date-picker 日期选择 回显后成功后无法改变的解决办法
  • 2024年9月青少年软件编程(C语言/C++)等级考试试卷(九级)
  • Kafka基础知识学习
  • Spring Boot编程训练系统:数据管理与存储
  • Leetcode刷题笔记14
  • 时序预测:多头注意力+宽度学习
  • 2 C++ 基本内置类型
  • Vulnhub靶场案例渗透[8]- HackableII
  • 更换电脑 重新安装软件
  • 前端基础的讲解-JS(11)
  • 磁盘的物理组成(Linux网络服务器 15)
  • Kafka--关于broker的夺命连环问
  • 半导体企业如何利用 Jira 应对复杂商业变局?
  • C++进阶-->封装map和set
  • deeponet作者相关三篇论文链接(理论基础、实用拓展、外推)
  • lmod安装和使用
  • 12 go语言(golang) - 数据类型:接口
  • C++ 优先算法 —— 四数之和(双指针)