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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/901884.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【mysql进阶】4-5. InnoDB 内存结构

InnoDB 内存结构 1 InnoDB存储引擎中内存结构的主要组成部分有哪些&#xff1f; &#x1f50d; 分析过程 从官⽹给出的InnoDB架构图中可以找到答案 InnoDB存储引擎架构链接&#xff1a;https://dev.mysql.com/doc/refman/8.0/en/innodb-architecture.html ✅ 解答问题 InnoD…

ECharts饼图-富文本标签,附视频讲解与代码下载

引言&#xff1a; 在数据可视化的世界里&#xff0c;ECharts凭借其丰富的图表类型和强大的配置能力&#xff0c;成为了众多开发者的首选。今天&#xff0c;我将带大家一起实现一个饼图图表&#xff0c;通过该图表我们可以直观地展示和分析数据。此外&#xff0c;我还将提供详…

虚拟光驱软件 PowerISO v8.7.0 中文激活版

PowerISO 是一款虚拟光驱工具及强大的光盘映像文件制作工具。支持创建、编辑、提取、压缩、加密和转换ISO/BIN图像文件。同时自带DISM工具&#xff0c;支持ESD/ISO/WIM/ESD格式转换&#xff0c;制作镜像文件制作U盘启动&#xff0c;支持ISO/BIN/IMG/DAA/WIM等各种常见文件类型。…

【Nas】X-Doc:jellyfin“该客户端与媒体不兼容,服务器未发送兼容的媒体格式”问题解决方案

【Nas】X-Doc&#xff1a;jellyfin“该客户端与媒体不兼容&#xff0c;服务器未发送兼容的媒体格式”问题解决方案 当使用Jellyfin播放视频时出现“该客户端与媒体不兼容&#xff0c;服务器未发送兼容的媒体格式”&#xff0c;这是与硬件解码和ffmpeg设置有关系&#xff0c;具体…

机器学习新领域:联邦学习方法——分布式机器学习

联邦学习是一种分布式机器学习方法&#xff0c;旨在保护数据隐私并提高模型的训练效率。以下是对联邦学习的详细介绍&#xff0c;包括其基本概念、应用场景以及面临的挑战。 一、介绍 1. 基本概念 联邦学习的核心思想是将模型训练过程分散到多个数据源上&#xff0c;而不需要…

生产级AI智能体开发实践【旅行规划】

在我最近的博客文章《使用 LangChain 代理创建多模式聊天机器人的开发人员指南》中&#xff0c;讨论了 AI 代理的作用&#xff0c;并演示了使用 LangChain 框架的实现。虽然它适用于概念验证 (POC)&#xff0c;但它不适合生产环境。 在这篇文章中&#xff0c;我将提供一种更适…

移情别恋c++ ദ്ദി˶ー̀֊ー́ ) ——15.C++11(1)

1. 自动类型推导 (auto) C11 引入了 auto 关键字&#xff0c;可以根据初始值的类型自动推导变量的类型&#xff0c;从而减少了手动声明类型的繁琐。例如&#xff1a; std::vector<int> vec {1, 2, 3, 4}; auto it vec.begin(); // 自动推导类型为 std::vector<in…

C语言程序设计:现代设计方法习题笔记《chapter5》下篇

第七题 题目分析&#xff1a;求最大最小值转换为条件判断问题&#xff0c;最大值有四种可能&#xff0c;最小值相应有三种情况&#xff0c;给出下列代码。 示例代码&#xff1a; #include <stdio.h>int main() {int num1, num2, num3, num4; // 定义四个变量来存储输入…

【项目实战】HuggingFace教程,初步实战,使用HF做一些小型任务

Huggingface教程 一、前期准备工作二、学习pipline2.1.试运行代码&#xff0c;使用HuggingFace下载模型2.2. 例子1&#xff0c;情感检测分析(只有积极和消极两个状态)2.3. 例子2&#xff0c;文本生成 三、学会使用Tokenizer & Model3.1.tokenizer&#xff08;分词器&#x…

Lampiao靶机入侵实战

07-Lampiao靶机入侵实战 一、扫描采集信息 1、获取IP地址 nmap -sn 192.168.81.0/24获得IP地址为&#xff1a;192.168.81.1282、获取端口信息 由于nmap默认情况下只扫描常用的1000个端口&#xff0c;覆盖面并不全&#xff0c;所以建议全端口扫描 nmap -p 1-65535 192.168.…

JSON格式及jackson.jar包的安装与配置

目录 为什么会出现JSON? JSON格式 jackson的jar文件下载与配置 jackson的简单使用 读取json格式 将现有对象转换成json序列 为什么会出现JSON? 在JSON出现前&#xff0c;由于多种编程语言的语法细节都不是完全相同&#xff0c;在网络传输信息时无法使用同一的格式&…

高效集成:YS采购订单与帆软MongoDB的对接实践

高效集成&#xff1a;YS采购订单与帆软MongoDB的对接实践 YS采购订单对接帆软MongoDB&#xff1a;用友BIP数据集成案例分享 在企业信息化系统中&#xff0c;数据的高效流动和处理是实现业务智能化的关键。本文将聚焦于一个具体的系统对接集成案例——YS采购订单对接帆软MongoD…

sqli-labs靶场安装以及刷题记录-docker

sqli-labs靶场安装以及刷题记录-docker sqli-labs靶场安装-dockersqli-labs靶场刷题less-1 单引号less-2 数字型less-3 单引号括号less-4 双引号括号less-5 单引号布尔盲注less-6 双引号布尔盲注less-7 单引号加括号、输出到文件less-8 单引号布尔盲注less-9 单引号时间盲注les…

Ollama+Open WebUI,windows部署一个本地AI

在Ollama官网下载&#xff0c;点击DownLoad 下载完之后进行安装&#xff0c;配置环境变量&#xff0c;完成后打开CMD命令行工具测试 运行并下载模型 之后选择Open WebUI作为图形化界面 &#x1f680; Getting Started | Open WebUI 运行Docker命令 docker run -d -p 3000:80…

ArcGIS002:软件自定义设置

摘要&#xff1a;本文详细介绍安装arcgis10.2后软件自定义设置内容&#xff0c;包括工具条的启用、扩展模块的启用、如何加载项管理器、快捷键设置、样式管理器的使用以及软件常规设置。 一、工具条的启用 依次点击菜单栏【自定义】->【工具条】&#xff0c;根据工作需求勾…

07 设计模式-结构型模式-桥接模式

桥接&#xff08;Bridge&#xff09;是用于把抽象化与实现化解耦&#xff0c;使得二者可以独立变化。这种类型的设计模式属于结构型模式&#xff0c;它通过提供抽象化和实现化之间的桥接结构&#xff0c;来实现二者的解耦。 这种模式涉及到一个作为桥接的接口&#xff0c;使得…

背包九讲——二维费用背包问题

目录 二维费用背包问题 问题描述&#xff1a; 解决方法&#xff1a; 方法一&#xff1a; 代码实现&#xff1a; 方法二&#xff1a; 代码实现&#xff1a; 背包问题第五讲——二维费用背包问题 背包问题是一类经典的组合优化问题&#xff0c;通常涉及在限定容量的背包中…

gateway 整合 spring security oauth2

微服务分布式认证授权方案 在分布式授权系统中&#xff0c;授权服务要独立成一个模块做统一授权&#xff0c;无论客户端是浏览器&#xff0c;app或者第三方&#xff0c;都会在授权服务中获取权限&#xff0c;并通过网关访问资源 OAuth2的四种授权模式 授权码模式 授权服务器将授…

CentOS 7镜像下载

新版本系统镜像下载&#xff08;当前最新是CentOS 7.4版本&#xff09; CentOS官网 官网地址 http://isoredirect.centos.org/centos/7.4.1708/isos/x86_64/ http://mirror.centos.org/centos/7/isos/ 国内的华为云&#xff0c;超级快&#xff1a;https://mirrors.huaweiclou…

Linux TCP CC状态机

万字详文&#xff1a;TCP 拥塞控制详解 - 知乎bcc/tools/tcpcong.py at master iovisor/bccbcc/tools/tcpcong_example.txt at master iovisor/bcc 1.状态机 2.tcp map