当前位置: 代码网 > it编程>编程语言>Asp.net > Unity热更方案HybridCLR+YooAsset,纯c#开发热更,保姆级教程,从零开始

Unity热更方案HybridCLR+YooAsset,纯c#开发热更,保姆级教程,从零开始

2024年08月02日 Asp.net 我要评论
Unity热更,HybirdCLR热更,YooAsset热更,保姆级教程,从零开始

一、前言

unity热更有很多方案,各种lua热更,ilruntime等,这里介绍的是yooasset+hybridclr的热更方案,hybridclr负责更新c#代码,yooasset负责更新资源。

简单来说,流程就是将c#代码打成dll,然后把dll当做一个资源,用yooasset热更dll资源之后,动态加载dll程序集,然后执行新逻辑

hybridclr相比其他代码热更方案而言,纯c#方便开发,更加符合开发者习惯,更新的代码执行效率也更好。

yooasset热更资源,主要是省去了自己亲自管理ab包,ab包的管理挺繁琐,assetbundle坑也很多,而且yooasset有下载器,不用自己手写网络下载,也不用自己记录资源,比对资源列表来判定需要热更什么资源。

这里将从零开始,用一个空工程,展示yooasset+hybridclr热更的使用过程。

二、创建空工程

这里使用的版本是unity2022.3.17.f1c1
在这里插入图片描述

三、接入hybridclr

unity必须添加了windows build support(il2cpp)或mac build support(il2cpp)模块
在这里插入图片描述
在这里插入图片描述

安装ide及相关编译环境
win
需要安装visual studio 2019或更高版本。安装时至少要包含使用unity的游戏开发和使用c++的游戏开发组件。
安装git

mac
要求macos版本 >= 12,xcode版本 >= 13,例如xcode 13.4.1, macos 12.4。
安装 git

添加consoletoscreen.cs脚本
和热更无关,主要在屏幕上显示log,代码如下:

using system.collections.generic;
using unityengine;

public class consoletoscreen : monobehaviour
{
    const int maxlines = 50;
    const int maxlinelength = 120;
    private string _logstr = "";

    private readonly list<string> _lines = new();

    public int fontsize = 15;

    void onenable() { application.logmessagereceived += log; }
    void ondisable() { application.logmessagereceived -= log; }

    public void log(string logstring, string stacktrace, logtype type)
    {
        foreach (var line in logstring.split('\n'))
        {
            if (line.length <= maxlinelength)
            {
                _lines.add(line);
                continue;
            }
            var linecount = line.length / maxlinelength + 1;
            for (int i = 0; i < linecount; i++)
            {
                if ((i + 1) * maxlinelength <= line.length)
                {
                    _lines.add(line.substring(i * maxlinelength, maxlinelength));
                }
                else
                {
                    _lines.add(line.substring(i * maxlinelength, line.length - i * maxlinelength));
                }
            }
        }
        if (_lines.count > maxlines)
        {
            _lines.removerange(0, _lines.count - maxlines);
        }
        // _lines.add(stacktrace);
        _logstr = string.join("\n", _lines);
    }

    void ongui()
    {
        gui.matrix = matrix4x4.trs(vector3.zero, quaternion.identity,
            new vector3(screen.width / 1200.0f, screen.height / 800.0f, 1.0f));
        gui.label(new rect(10, 10, 800, 370), _logstr, new guistyle { fontsize = 10 });
    }
}

工程初始设置
创建main场景
将consoletoscreen挂载一个创建的空物体上
菜单栏 file/buildsettings添加main场景
创建assets/hotupdate目录
在这里插入图片描述

创建热更程序集
在hotupdate目录下 右键 create/assembly definition,创建一个名为hotupdate的程序集模块
在这里插入图片描述

安装hybridclr
主菜单中点击windows/package manager
add package from git url…
填入:https://gitee.com/focus-creative-games/hybridclr_unity.git
如下:
在这里插入图片描述
在这里插入图片描述

初始化hybridclr
打开菜单hybridclr/installer…, 点击install按钮进行安装。 耐心等待30s左右,安装完成后会在最后打印 安装成功日志。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

配置hybridclr
打开菜单 hybridclr/settings, 在热更新assembly definitions配置项中添加hotupdate程序集。
在这里插入图片描述
在这里插入图片描述

设置playersettings
打开菜单edit/project setting,在player选项中,设置如下几个配置:
scripting backend 切换为 il2cpp。
api compatability level 切换为 .net 4.x(unity 2019-2020) 或.net framework(unity 2021+)
在这里插入图片描述
在这里插入图片描述

hybridclr方面的操作告一段落,接下来接入yooasset

四、接入yooasset

安装yooasset
打开菜单edit/project settings/package manager
在package manager选项找那个,输入如下内容,点击save:

(国际版)
name: package.openupm.com
url: https://package.openupm.com
scope(s): com.tuyoogame.yooasset

(中国版)
name: package.openupm.cn
url: https://package.openupm.cn
scope(s): com.tuyoogame.yooasset
(这个配置好像报错了,我用的上面国际版的地址)
在这里插入图片描述
在这里插入图片描述

打开菜单windows/package manager
packages选择my registries,出现了yooasset,点击install安装。
在这里插入图片描述
在这里插入图片描述

五、搭建本地资源服务器nginx

为了模拟热更流程,需要一个服务器作为热更资源的下载,我们可以在本机搭建一个资源服务器。我这里用的是nginx。
下载地址:https://nginx.org/en/download.html
在这里插入图片描述

随便下一个,下载之后是个zip包,解压之后如下:
在这里插入图片描述

注意:端口冲突时,更改端口:打开文件:conf-nginx.conf,修改第36行的listen,我改的是8084,自己随意
然后运行nginx.exe即可
在这里插入图片描述

现在打开网址http://127.0.0.1:8084如下:
在这里插入图片描述

html文件夹下,就可以放需要热更的资源,比如我准备打包测试的是pc平台,我就在html文件夹下建了个testproject文件夹,在里面再建个pc文件夹,testproject是项目名,pc是平台名,用以放接下来要热更的资源
在这里插入图片描述

六、实战

创建热更目录
之前的hotupdate文件夹是为了放c#代码,用以生成程序集的,接下来创建的目录,是为了热更资源的(包括c#代码的dll资源,dll也是一种资源),创建如下目录:
在这里插入图片描述

准备工作
hotupdate文件夹下创建instantiatebyasset.cs,这是会被热更的代码:

using unityengine;

public class instantiatebyasset : monobehaviour
{
    void start()
    {
        debug.log("原始代码");
    }
}

在这里插入图片描述

创建一个cube预制体,挂载instantiatebyasset组件,放入myasset/prefabs中
在这里插入图片描述

收集热更资源

创建resources文件夹,在resources文件夹内通过右键创建配置文件(project窗体内右键 -> create -> yooasset -> create setting),将配置文件放在resources文件夹下
在这里插入图片描述

打开菜单yooasset/assetbundle collector,点击showpackages,再点击+号,创建packages,再点击showpackages,只显示groups就行,方便操作
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

打开enable addressable
添加code和prefab分组,具体设置如下:
在这里插入图片描述
在这里插入图片描述

选项含义如下,其他含义,请看官网
在这里插入图片描述

编写loaddll.cs
编写代码初始化yooasset,从资源服加载热更资源,加载热更程序集,将loaddll.cs挂在main场景的main camera上。

整个代码流程就是先初始化,然后下载热更资源,然后补充元数据,然后开始执行热更代码。
关于补充元数据,通俗的理解,举个例子:

由于unity il2cpp打包的代码裁剪,假设在打包时的代码,从来没有用过list泛型,打包时,list的元数据会被裁掉了。到玩家手机的app上,根本不存在list的程序定义等关键信息。当玩家运行app时,在运行前预编译这个app的代码时,是不存在list的相关定义的,编译完成后,在运行时,热更逻辑中,使用了list,虽然这个热更的dll中有list的程序定义,但是已经过了编译阶段,现在是执行阶段,执行阶段是直接找预编译时的程序定义去进行实例化。所以在动态执行包含有list的代码时,由于编译阶段缺失了list的程序定义相关的元数据,无法对其进行实例化。所以在运行包含了list的热更代码时,先补齐编译阶段缺失的list元数据,之后才能正常运行和实例化热更中的包含了list的代码。

常见的需要补充的元数据如:mscorlib.dll, system.dll, system.core.dll

详细信息原理请浏览官网

代码如下:

using hybridclr;
using system;
using system.collections;
using system.collections.generic;
using system.linq;
using system.reflection;
using unityengine;
using yooasset;

/// <summary>
/// 脚本工作流程:
/// 1.下载资源,用yooasset资源框架进行下载
///    1.资源文件,ab包
///    2.热更新dll
/// 2.给aot dll补充元素据,通过runtimeapi.loadmetadataforaotassembly
/// 3.通过实例化prefab,运行热更代码
/// </summary>
public class loaddll : monobehaviour
{
    /// <summary>
    /// 资源系统运行模式
    /// </summary>
    public eplaymode playmode = eplaymode.hostplaymode;

    private resourcepackage _defaultpackage;

    void start()
    {
        startcoroutine(inityooassets(startgame));
    }

    #region yooasset初始化

    ienumerator inityooassets(action ondownloadcomplete)
    {
        // 1.初始化资源系统
        yooassets.initialize();

        string packagename = "defaultpackage";
        var package = yooassets.trygetpackage(packagename) ?? yooassets.createpackage(packagename);
        yooassets.setdefaultpackage(package);
        if (playmode == eplaymode.editorsimulatemode)
        {
            //编辑器模拟模式
            var initparameters = new editorsimulatemodeparameters { simulatemanifestfilepath = editorsimulatemodehelper.simulatebuild(edefaultbuildpipeline.builtinbuildpipeline, "defaultpackage") };
            yield return package.initializeasync(initparameters);
        }
        else if (playmode == eplaymode.hostplaymode)
        {
            //联机运行模式
            string defaulthostserver = gethostserverurl();
            string fallbackhostserver = gethostserverurl();
            debug.log(defaulthostserver);
            var initparameters = new hostplaymodeparameters();
            initparameters.buildinqueryservices = new gamequeryservices();
            // initparameters.decryptionservices = new gamedecryptionservices();
            // initparameters.deliveryqueryservices = new defaultdeliveryqueryservices();
            initparameters.remoteservices = new remoteservices(defaulthostserver, fallbackhostserver);
            var initoperation = package.initializeasync(initparameters);
            yield return initoperation;

            if (initoperation.status == eoperationstatus.succeed)
            {
                debug.log("资源包初始化成功!");
            }
            else
            {
                debug.logerror($"资源包初始化失败:{initoperation.error}");
            }
        }


        //2.获取资源版本
        var operation = package.updatepackageversionasync();
        yield return operation;

        if (operation.status != eoperationstatus.succeed)
        {
            //更新失败
            debug.logerror(operation.error);
            yield break;
        }

        string packageversion = operation.packageversion;
        debug.log($"updated package version : {packageversion}");

        //3.更新补丁清单
        // 更新成功后自动保存版本号,作为下次初始化的版本。
        // 也可以通过operation.savepackageversion()方法保存。
        var operation2 = package.updatepackagemanifestasync(packageversion);
        yield return operation2;

        if (operation2.status != eoperationstatus.succeed)
        {
            //更新失败
            debug.logerror(operation2.error);
            yield break;
        }

        //4.下载补丁包
        yield return download();

        //判断是否下载成功
        var assets = new list<string> { "hotupdate.dll" }.concat(aotmetaassemblyfiles);
        foreach (var asset in assets)
        {
            var handle = package.loadassetasync<textasset>(asset);
            yield return handle;
            var assetobj = handle.assetobject as textasset;
            s_assetdatas[asset] = assetobj;
            debug.log($"dll:{asset}   {assetobj == null}");
        }

        _defaultpackage = package;
        ondownloadcomplete();
    }
    
    private string gethostserverurl()
    {
        //模拟下载地址,8084为nginx里面设置的端口号,项目名,平台名
        return "http://127.0.0.1:8084/testproject/pc";
    }

    /// <summary>
    /// 远端资源地址查询服务类
    /// </summary>
    private class remoteservices : iremoteservices
    {
        private readonly string _defaulthostserver;
        private readonly string _fallbackhostserver;

        public remoteservices(string defaulthostserver, string fallbackhostserver)
        {
            _defaulthostserver = defaulthostserver;
            _fallbackhostserver = fallbackhostserver;
        }

        string iremoteservices.getremotemainurl(string filename)
        {
            return $"{_defaulthostserver}/{filename}";
        }

        string iremoteservices.getremotefallbackurl(string filename)
        {
            return $"{_fallbackhostserver}/{filename}";
        }
    }

    /// <summary>
    /// 资源文件查询服务类
    /// </summary>
    internal class gamequeryservices : ibuildinqueryservices
    {
        public bool query(string packagename, string filename, string filecrc)
        {
#if unity_iphone
            throw new exception("ios平台需要内置资源");
            return false;
#else
            return false;
#endif
        }
    }

    #endregion

    #region 下载热更资源

    ienumerator download()
    {
        int downloadingmaxnum = 10;
        int failedtryagain = 3;
        var package = yooassets.getpackage("defaultpackage");
        var downloader = package.createresourcedownloader(downloadingmaxnum, failedtryagain);

        //没有需要下载的资源
        if (downloader.totaldownloadcount == 0)
        {
            yield break;
        }

        //需要下载的文件总数和总大小
        int totaldownloadcount = downloader.totaldownloadcount;
        long totaldownloadbytes = downloader.totaldownloadbytes;

        //注册回调方法
        downloader.ondownloaderrorcallback = ondownloaderrorfunction;
        downloader.ondownloadprogresscallback = ondownloadprogressupdatefunction;
        downloader.ondownloadovercallback = ondownloadoverfunction;
        downloader.onstartdownloadfilecallback = onstartdownloadfilefunction;

        //开启下载
        downloader.begindownload();
        yield return downloader;

        //检测下载结果
        if (downloader.status == eoperationstatus.succeed)
        {
            //下载成功
            debug.log("更新完成");
        }
        else
        {
            //下载失败
            debug.log("更新失败");
        }
    }

    /// <summary>
    /// 开始下载
    /// </summary>
    /// <param name="filename"></param>
    /// <param name="sizebytes"></param>
    private void onstartdownloadfilefunction(string filename, long sizebytes)
    {
        debug.log(string.format("开始下载:文件名:{0},文件大小:{1}", filename, sizebytes));
    }

    /// <summary>
    /// 下载完成
    /// </summary>
    /// <param name="issucceed"></param>
    private void ondownloadoverfunction(bool issucceed)
    {
        debug.log("下载" + (issucceed ? "成功" : "失败"));
    }

    /// <summary>
    /// 更新中
    /// </summary>
    /// <param name="totaldownloadcount"></param>
    /// <param name="currentdownloadcount"></param>
    /// <param name="totaldownloadbytes"></param>
    /// <param name="currentdownloadbytes"></param>
    private void ondownloadprogressupdatefunction(int totaldownloadcount, int currentdownloadcount, long totaldownloadbytes, long currentdownloadbytes)
    {
        debug.log(string.format("文件总数:{0},已下载文件数:{1},下载总大小:{2},已下载大小{3}", totaldownloadcount, currentdownloadcount, totaldownloadbytes, currentdownloadbytes));
    }

    /// <summary>
    /// 下载出错
    /// </summary>
    /// <param name="filename"></param>
    /// <param name="error"></param>
    private void ondownloaderrorfunction(string filename, string error)
    {
        debug.log(string.format("下载出错:文件名:{0},错误信息:{1}", filename, error));
    }

    #endregion

    #region 补充元数据

    //补充元数据dll的列表
    //通过runtimeapi.loadmetadataforaotassembly()函数来补充aot泛型的原始元数据
    private static list<string> aotmetaassemblyfiles { get; } = new() { "mscorlib.dll", "system.dll", "system.core.dll", };
    private static dictionary<string, textasset> s_assetdatas = new dictionary<string, textasset>();
    private static assembly _hotupdateass;
    
    public static byte[] readbytesfromstreamingassets(string dllname)
    {
        if (s_assetdatas.containskey(dllname))
        {
            return s_assetdatas[dllname].bytes;
        }

        return array.empty<byte>();
    }

    

    /// <summary>
    /// 为aot assembly加载原始metadata, 这个代码放aot或者热更新都行。
    /// 一旦加载后,如果aot泛型函数对应native实现不存在,则自动替换为解释模式执行
    /// </summary>
    private static void loadmetadataforaotassemblies()
    {
        /// 注意,补充元数据是给aot dll补充元数据,而不是给热更新dll补充元数据。
        /// 热更新dll不缺元数据,不需要补充,如果调用loadmetadataforaotassembly会返回错误
        homologousimagemode mode = homologousimagemode.superset;
        foreach (var aotdllname in aotmetaassemblyfiles)
        {
            byte[] dllbytes = readbytesfromstreamingassets(aotdllname);
            // 加载assembly对应的dll,会自动为它hook。一旦aot泛型函数的native函数不存在,用解释器版本代码
            loadimageerrorcode err = runtimeapi.loadmetadataforaotassembly(dllbytes, mode);
            debug.log($"loadmetadataforaotassembly:{aotdllname}. mode:{mode} ret:{err}");
        }
    }

    #endregion

    #region 运行测试

    void startgame()
    {
        // 加载aot dll的元数据
        loadmetadataforaotassemblies();
        // 加载热更dll
#if !unity_editor
        _hotupdateass = assembly.load(readbytesfromstreamingassets("hotupdate.dll"));
#else
        _hotupdateass = system.appdomain.currentdomain.getassemblies().first(a => a.getname().name == "hotupdate");
#endif
        debug.log("运行热更代码");
        startcoroutine(run_instantiatecomponentbyasset());
    }

    ienumerator run_instantiatecomponentbyasset()
    {
        // 通过实例化assetbundle中的资源,还原资源上的热更新脚本
        var package = yooassets.getpackage("defaultpackage");
        var handle = package.loadassetasync<gameobject>("cube");
        yield return handle;
        handle.completed += handle_completed;
    }

    private void handle_completed(assethandle obj)
    {
        debug.log("准备实例化");
        gameobject go = obj.instantiatesync();
        debug.log($"prefab name is {go.name}");
    }

    #endregion
}
//ps:版本不同可能有一些类名发生变化,请参照现阶段版本自行修改,官网可能更新不及时。

打包阶段
(这里演示的是pc平台)
打开菜单 hybridclr/generate/all,耐心等待之后,
回到assets同级目录,将hybridclrdata/hotupdatedlls/standalonewindows64/hotupdate.dll复制到assets/myassset/codes内,并且加上后缀.bytes,这是包含热更逻辑代码的dll
在这里插入图片描述

再将hybridclrdata/assembliespostil2cppstrip/standalonewindows64目录下的mscorlib.dll, system.dll, system.core.dll这三个dll复制到assets/myassset/codes内,并且加上后缀.bytes,这是包含补充元数据的dll
在这里插入图片描述
在这里插入图片描述

打开菜单yooasset/assetbundle builder,buildmodel选forcerebuild,全量构建。一般第一次选这个,后面incrementalbuild热更选增量构建,点击clickbuild
在这里插入图片描述

构建完之后会自动打开构建后的资源目录,在asset同级目录下的bundles/standalonewindows64/defaultpackage/2024-06-27-1194(构建时的版本号)
在这里插入图片描述

把里面的所有东西,放在本地资源服务器nginx的目录下,之前代码里访问的地址:http://127.0.0.1:8084/testproject/pc就是这里了,到时候热更就是从这里下载最新的资源
在这里插入图片描述

操作完上述之后,打开菜单 file/build settings/build先把.exe打出来,双击运行,能看到先从http://127.0.0.1:8084/testproject/pc路径下下载资源包,然后补充元数据,然后实例化了cube预制体,执行了预制体上的代码,打印了“原始代码”。
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

开始热更
终于到了激动人心的热更环节,前面搞了那么多就是为了热更代码和资源。接下来我们把cube预制体尺寸x改为10,将instantiatebyasset脚本的输出从“原始代码”改为“热更后的代码”。
在这里插入图片描述
在这里插入图片描述

点击菜单 hybridclr/compiledll/activebuildtarget,把新的hotupdate.dll复制替换掉myasset里面原来的hotupdate.dll,记得加后缀.bytes,另外三个mscorlib.dll, system.dll, system.core.dll也复制替换过去记得加后缀.bytes
在这里插入图片描述
在这里插入图片描述

打开菜单yooasset/assetbundle builder,buildmodel选incrementalbuild,增量构建,点击clickbuild,然后自动打开了生成的资源所在的文件夹,把这一堆生成的东西,复制到本地资源服务器nginx的testproject/pc目录下,之前的旧资源删掉。
在这里插入图片描述
在这里插入图片描述

然后重新打开之前的exe程序,看到代码和资源都热更了,大功告成!!!
在这里插入图片描述

七、最后

至于其他的更多细节,比如加密解密,资源收集设置等待,可以多多浏览官网以及官网的示例项目。
hybridclr
yooasset

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com