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

Unity C#脚本的热更新

以下内容是根据Unity 2020.1.0f1版本进行编写的

目前游戏开发厂商主流还是使用lua框架来进行热更,如xlua,tolua等,也有的小游戏是直接整包更新,这种小游戏的包体很小,代码是用C#写的;还有的游戏就是通过热更C#代码来实现热更新的。本篇就来学习一下。

1、热更C#代码的方法

AI时代,遇事不决,先问AI,下面就是百度问AI的答案:
在这里插入图片描述
在这里插入图片描述
可以看到,AI给出的答案大部分都是将C#代码编译成dll,然后在需要时动态加载对应的dll来实现代码的热更新的。下面就来尝试一下。

2、使用ILRuntime框架

ILRuntime官方文档:https://ourpalm.github.io/ILRuntime/public/v1/guide/tutorial.html
在这里插入图片描述
“scopedRegistries”: [
{
“name”: “ILRuntime”,
“url”: “https://registry.npmjs.org”,
“scopes”: [
“com.ourpalm”
]
}
],
在这里插入图片描述
如上图一,新建一个unity项目,然后在工程目录Packages下的manifest文件中增加图一框住的代码,保存。然后关闭Unity项目再重新打开。点击菜单栏Window->Package Manager打开PackageManager窗口,切换Packages切换到My Registries,可以看到刚刚加上去的ILRuntime包(如图二)。

选中后点击右下角箭头所指的install按钮就可以导入包体了(这里因为我已经导入过了所以显示的按钮是Remove)。这个包体还有示例Demo,有需要也可以一并导入到工程中。
在这里插入图片描述
导入的Demo用的是unsafe代码,导入后可能会有很多报错,点击菜单栏Edit->Project Settings打开ProjectSettings窗口,设置player页签中的OtherSettings,使工程允许使用unsafe代码。
在这里插入图片描述
接着尝试运行Demo,直接运行会报错,需要生成dll。
先用VisualStudio打开一次项目工程的sln文件,再打开下载的Demo包内工程的sln文件(如上图)。
在这里插入图片描述
在这里插入图片描述
在打开的HotFix_Project中,点击菜单栏生成->生成解决方案按钮,等待VS左下角出现生成成功提示。
在这里插入图片描述
此时回到Unity随便运行一个Examples场景,都不会有报错了。

在这里插入图片描述
接下来简单尝试一下,先做一个简单的界面(如上图),功能是点击下方左右两个按钮,点击哪边的按钮,就在中间的文本显示点击了哪边的按钮。

using UnityEngine;
using System.Collections;
using System.IO;
using System;

public class AppCommon : MonoBehaviour
{
    static AppCommon instance;
    System.IO.MemoryStream fs;
    System.IO.MemoryStream p;
    public bool isLoaded = false;
    public static AppCommon Instance
    {
        get { return instance; }
    }
    //AppDomain是ILRuntime的入口,最好是在一个单例类中保存,整个游戏全局就一个,这里为了示例方便,每个例子里面都单独做了一个
    //大家在正式项目中请全局只创建一个AppDomain
    public ILRuntime.Runtime.Enviorment.AppDomain appdomain;

    //在awake方法中先加载好appdomain
    void Awake()
    {
        instance = this;
        StartCoroutine(LoadHotFixAssembly());
    }

    IEnumerator LoadHotFixAssembly()
    {
        //首先实例化ILRuntime的AppDomain,AppDomain是一个应用程序域,每个AppDomain都是一个独立的沙盒
        appdomain = new ILRuntime.Runtime.Enviorment.AppDomain();
        //正常项目中应该是自行从其他地方下载dll,或者打包在AssetBundle中读取,平时开发以及为了演示方便直接从StreammingAssets中读取,
        //正式发布的时候需要大家自行从其他地方读取dll

        //!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
        //这个DLL文件是直接编译HotFix_Project.sln生成的,已经在项目中设置好输出目录为StreamingAssets,在VS里直接编译即可生成到对应目录,无需手动拷贝
#if UNITY_ANDROID
        WWW www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.dll");
#else
        WWW www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.dll");
#endif
        while (!www.isDone)
            yield return null;
        if (!string.IsNullOrEmpty(www.error))
            UnityEngine.Debug.LogError(www.error);
        byte[] dll = www.bytes;
        www.Dispose();

        //PDB文件是调试数据库,如需要在日志中显示报错的行号,则必须提供PDB文件,不过由于会额外耗用内存,正式发布时请将PDB去掉,下面LoadAssembly的时候pdb传null即可
#if UNITY_ANDROID
        www = new WWW(Application.streamingAssetsPath + "/HotFix_Project.pdb");
#else
        www = new WWW("file:///" + Application.streamingAssetsPath + "/HotFix_Project.pdb");
#endif
        while (!www.isDone)
            yield return null;
        if (!string.IsNullOrEmpty(www.error))
            UnityEngine.Debug.LogError(www.error);
        byte[] pdb = www.bytes;
        fs = new MemoryStream(dll);
        p = new MemoryStream(pdb);
        try
        {
            appdomain.LoadAssembly(fs, p, new ILRuntime.Mono.Cecil.Pdb.PdbReaderProvider());
        }
        catch
        {
            Debug.LogError("加载热更DLL失败,请确保已经通过VS打开Assets/Samples/ILRuntime/1.6/Demo/HotFix_Project/HotFix_Project.sln编译过热更DLL");
        }

        InitializeILRuntime();
        OnHotFixLoaded();
    }

    private void OnDestroy()
    {
        fs.Close();
        p.Close();
    }

    unsafe void InitializeILRuntime()
    {
#if DEBUG && (UNITY_EDITOR || UNITY_ANDROID || UNITY_IPHONE)
        //由于Unity的Profiler接口只允许在主线程使用,为了避免出异常,需要告诉ILRuntime主线程的线程ID才能正确将函数运行耗时报告给Profiler
        appdomain.UnityMainThreadID = System.Threading.Thread.CurrentThread.ManagedThreadId;
#endif
        //这里做一些ILRuntime的注册
        appdomain.RegisterCrossBindingAdaptor(new MonoBehaviourAdapter());
        appdomain.RegisterValueTypeBinder(typeof(Vector3), new Vector3Binder());
        appdomain.DelegateManager.RegisterDelegateConvertor<UnityEngine.Events.UnityAction>((act) =>
        {
            return new UnityEngine.Events.UnityAction(() =>
            {
                ((Action)act)();
            });
        });
        ILRuntime.Runtime.Generated.CLRBindings.Initialize(appdomain);
    }

    unsafe void OnHotFixLoaded()
    {
        isLoaded = true;
        Debug.Log("AppDomain Loaded");
    }
}

首先在Unity中新建一个名叫AppCommon的脚本,用于定义一些项目内通用的单例类。这里主要是定义一个叫appdomain的类,在Demo中是建议全局只创建一个的。
每个Demo都会有加载这个appdpmain的方法,将代码复制到AppCommon类中,在其InitializeILRuntime方法中注册好全部需要用到的事件等。

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

public class MyView1 : MonoBehaviour
{
    static MyView1 instance;

    public static MyView1 Instance
    {
        get { return instance; }
    }

    void Start()
    {
        instance = this;
        StartCoroutine(LoadHotFixAssembly());
    }

    IEnumerator LoadHotFixAssembly()
    {
        while(!AppCommon.Instance.isLoaded)
        {
            yield return 0;
        }
        OnHotFixLoaded();
    }

    void OnHotFixLoaded()
    {
        SetupCLRRedirection();
        SetupCLRRedirection2();
        AppCommon.Instance.appdomain.Invoke("HotFix_Project.TestMyView1", "ShowView", null, gameObject);
    }

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

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

    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;
    }

    unsafe static StackObject* GetComponent(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 = null;
            if (type is CLRType)
            {
                //Unity主工程的类不需要任何特殊处理,直接调用Unity接口
                res = instance.GetComponent(type.TypeForCLR);
            }
            else
            {
                //因为所有DLL里面的MonoBehaviour实际都是这个Component,所以我们只能全取出来遍历查找
                var clrInstances = instance.GetComponents<MonoBehaviourAdapter.Adaptor>();
                for (int i = 0; i < clrInstances.Length; i++)
                {
                    var clrInstance = clrInstances[i];
                    if (clrInstance.ILInstance != null)//ILInstance为null, 表示是无效的MonoBehaviour,要略过
                    {
                        if (clrInstance.ILInstance.Type == type)
                        {
                            res = clrInstance.ILInstance;//交给ILRuntime的实例应该为ILInstance
                            break;
                        }
                    }
                }
            }

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

        return __esp;
    }
}

然后还是在Unity中新建一个名叫MyView1的脚本,这个脚本的功能其实只是等待上述的appdomain类加载完之后,然后调用加载的dll内部的类以及方法即可。实际逻辑是写到HotFix_Project里的,也只有HotFix_Project里的代码能热更。
这里写的比较简单,大部分代码是抄MonoBehaviourDemo的,实际上就是需要使appdomain注册自定义实现AddComponent方法和GetComponent方法。这样,在热更的代码中就可以通过AddComponent方法来把C#代码以组件的形式挂载到对应的GameObject中。
在这里插入图片描述
在这里插入图片描述
最后就是热更代码部分。
因为要用到Unity UI部分的方法,所以需要将UnityEngine.UI的dll复制过来并加入到HotFix_Project的引用中。(先把dll复制到UnityDlls目录下,然后再在VS上右键添加引用,在打开的窗口中点击右下角浏览,然后选择复制的dll文件即可)

using UnityEngine;
using UnityEngine.UI;

namespace HotFix_Project
{
    class TestMyView1 : MonoBehaviour
    {
        private Button btn1;
        private Button btn2;
        private Text text;

        void Start()
        {
            btn1 = gameObject.transform.Find("btn1").GetComponent<Button>();
            btn2 = gameObject.transform.Find("btn2").GetComponent<Button>();
            btn1.onClick.AddListener(OnClickBtn1);
            btn2.onClick.AddListener(OnClickBtn2);
            text = gameObject.transform.Find("Text").GetComponent<Text>();
        }

        void OnClickBtn1()
        {
            text.text = "点击了左边的按钮";
        }

        void OnClickBtn2()
        {
            text.text = "点击了右边的按钮";
        }

        public static void ShowView(GameObject go)
        {
            go.AddComponent<TestMyView1>();
        }
    }
}

接着在HotFix_Project新建一个名叫TestMyView1的C#脚本,实现上面描述的功能就可以了。这一部分代码就是可热更的。
写好代码后,点击HotFix_Project菜单栏的生成->重新生成解决方案按钮,即可运行Unity。
效果如下:
在这里插入图片描述
最后,如果需要实现热更,就是在AppCommon加载appdomain的协程中,修改一下加载的文件位置(如上图框住的部分,这里我没试)。
所以实际上,C#代码热更就是将代码编译成dll,然后在加载后以反射调用或者委托等方式来调用写在dll内部的类和方法。因此每次热更只需要重新编译生成dll就可以了。
在这里插入图片描述
在这里插入图片描述
此外,该框架还能生成一些CLR绑定的代码,用于减少反射调用的消耗。(实际上这里的做法有点类似于tolua框架)

代码仓库地址:https://gitee.com/chj–project/CSharpHotUpdate


http://www.kler.cn/news/367481.html

相关文章:

  • 面向对象与设计模式第二节:设计模式实战
  • 【C++】—— 模板进阶
  • C语言习题~day33
  • 《决策思维:人人必备的决策口袋书》
  • 网络原理之 TCP解释超详细!!!
  • k8s可以部署私有云吗?私有云部署全攻略
  • 单细胞 | 转录因子足迹分析
  • Docker容器间通信
  • 深入了解 MySQL 中的 INSERT ... SELECT 语句
  • iOS弹出系统相册选择弹窗
  • VS/Qt Creator +QT生成带.ico图标的.exe 并打包
  • qt QLabel详解
  • 智能合约在Web3中的作用:区块链技术的创新实践
  • JAVA基础-树和Set集合
  • uiautomatorviewer中的两个错误
  • 在虚拟化环境中,虚拟机的资源分配是否真的能够完全等效于物理服务器?是否有某些特定的工作负载在虚拟化环境中始终无法达到理想表现?
  • 【ChatGPT插件漏洞三连发之一】未授权恶意插件安装
  • Chromium HTML5 新的 Input 类型search对应c++
  • 【C++ 真题】B2099 矩阵交换行
  • 5.Linux按键驱动-fasync异步通知
  • 微信支付Java+uniapp微信小程序
  • Netty简单应用
  • C语言教程——数组(2)
  • UML之用例图详解
  • Linux 常用命令总汇
  • 二、Spring的执行流程