前言

本文尽量详细地展示构建一个游戏更新的流程,从资源热更新,代码热更新,到利用小米球(Ngrok)内网穿透搭建自己的简易的更新服务器,从而了解游戏更新的流程,简单地实现游戏更新的操作。


背景

一年前,我制作了一款Unity游戏并分享到同学群里。根据反馈,我发布了几个更新版本。有同学问能否部署到服务器上,我解释了Unity WebGL不支持移动设备的问题,并提到尽管尝试过在Netlify和Unity官网部署,但存在网络与WebGL的支持限制。因此,我打算采用手游的更新方式,避免整包发布,并学习服务器部署知识,制定了用于个人开发的游戏更新全流程方案。


1.代码热更 HybridCLR

探索更新方案时,首先想到的是热更新。我在B站上搜索相关视频,发现了Lua热更新和HybridCLR的热更新方案。观看“Unity huatuo系列 热更新革命性解决方案”视频后,我对HybridCLR(huatuo)有了更多了解,并决定尝试这个与Unity C#开发紧密相关的热更新方案。以下是支持我做出选择的信息:

Unity几种主要的热更新技术

  • Lua系列:Lua需要在C#环境中提供运行环境,对Lua和C#进行相互转换和调用。是目前最主流的方案
  • ILRuntime:基于Hotfix技术(一种基于反射和代码注入的热更新技术,即为通过反射使用dll,从而能够直接更新dll文件实现热更新),通过解释执行中间语言(IL)代码来运行程序,而非直接编译为机器码,从而绕过iOS系统对机器码执行的限制。
  • HybridCLR解释执行效率高,支持全C#代码热更,兼容IL2CPP平台,接近原生开发体验。它通过热更dll文件,解释执行IL代码运行程序,避免了iOS对机器码执行的限制。
  • Puerts:内置一个JavaScript/TypeScript解释器,解释执行TypeScript代码,类似于lua,但是功能更强大

HybridCLR支持原生开发流程,允许热更所有C#代码,无需像ILRuntime进行额外泛型绑定工作,这是其优势,也是我选择它进行热更的原因。
了解热更新方案的优势有助于选择适合的热更方案。关于Unity热更新技术的详细信息,建议自行研究。

HybridCLR使用要点

1.快速上手

决定采用HybridCLR热更新方案后,我尝试运行代码热更新流程以实际应用该方案。
我也在B站和CSDN学习HybridCLR使用,但发现教程需要下载替换文件,实际上新版本仅需遵循官方文档步骤
在这里插入图片描述
安装教程见HybridCLR官网
按官网快速入门流程操作,即可初步上手。

2.HybridCLR中AOT泛型问题的处理

在官方文档中这一点没有全放在快速上手中,新手容易遗漏

  1. HybridCLR解决泛型问题(这里直接使用官方的讲解)
    在这里插入图片描述
    详细见HybridCLR官方文档

  2. 项目中的使用流程
    在项目中跑通HybridCLR我们主要关注补充元数据技术和完全泛型共享技术的以下方面:

  • 支持完全泛型共享技术的就不需要主动在项目中补充元数据
  • 目前HybridCLR只在商业版,即专业版、旗舰版、热重载版中提供完全泛型共享等先进技术
  • 个人免费使用下载的是社区版,商业版需要邮件咨询商务,详细可见官方文档商业化版本的内容

因此在使用社区版的个人学习项目中我们必须补充元数据来解决泛型问题从而跑通HybridCLR热更方案。

补充元数据的步骤(这里以Windows平台的打包为例,其他平台是在这个基础上再参照官方文档修改即可,本文不涉及):

1.设置好热更程序集
在这里插入图片描述
2.点击HybridCLR/Generate/All 这一步它会根据HotUpdate生成需要补充的元数据dll到AOTGenericReference.cs中
在这里插入图片描述
3.根据AOTGenericReferences.cs填充HybridCLR Settings的补充元数据AOT dlls部分(尽管这一步骤不是必须的,只要启动AOT代码时加载了这些dlls即可。这样做是为了便于后续使用迁移工具直接移动dlls)
在这里插入图片描述
在这里插入图片描述
4.生成的元数据dlls将自动存放在裁剪后的AOT dll输出目录,之后需将其转移到个人项目的热更新资源文件夹(使用提供的工具代码移动到Addressables的热更新文件夹并覆盖,以同步到Addressables中。覆盖后必须刷新dll文件夹以避免Addressables没有更新文件);HotUpdate dll的输出处理方式相同。

在这里插入图片描述
在这里插入图片描述
工具代码(避免今后手动移动),其中的assembliesDstDirForAddressable修改为自己项目需要移动到的地址,其中的SettingsUtil即用到了前文的设置:

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using HybridCLR.Editor;
using UnityEditor;
using System.IO;

// 用于将热更程序集拷贝到Addressables目录下
public class CopyHotUpdateAssemblyToAddressables : MonoBehaviour
{

    static string assembliesDstDirForAddressable =  "Assets/HotUpdateDllBytes"; // Addressable的bytes资源目录直接覆盖即可Addressables能够定向同名资源

    [MenuItem("Build/Copy HotUpdate Assemblies to Addressable")]
    public static void CopyHotUpdateAssembliesToAddressable()
    {
        var target = EditorUserBuildSettings.activeBuildTarget;
        string hotfixDllSrcDir = SettingsUtil.GetHotUpdateDllsOutputDirByTarget(target); // 热更程序集输出目录
        // 项目中的HotUpdateDllBytes文件夹位于Assets/HotUpdateDllBytes
        foreach (var dll in SettingsUtil.HotUpdateAssemblyFilesExcludePreserved) // 遍历所有需要热更新的程序集文件
        {
            string dllPath = $"{hotfixDllSrcDir}/{dll}";

            string dllBytesPathForAddressable = $"{assembliesDstDirForAddressable}/{dll}.bytes";
            File.Copy(dllPath, dllBytesPathForAddressable, true); // 拷贝热更程序集到Addressable的bytes资源目录
            Debug.Log($"成功复制热更程序集:{dllPath} 到 Addressable 的 bytes 资源目录:{dllBytesPathForAddressable}");
        }
    }

    [MenuItem("Build/CopyStripedAotdllsToAddressable")]
    public static void CopyStripedAotdllsToStreamingAssets()
    {
        var target = EditorUserBuildSettings.activeBuildTarget;
        string aotDllSrcDir = SettingsUtil.GetPostIl2CppStripAssembliesDirectory(target); //aot程序集输出目录
        //本项目中的HotUpdateDllBytes文件夹地址即Assets/HotUpdateDllBytes
        foreach (var dll in SettingsUtil.AOTAssemblyNames) // 遍历所有热更新程序集
        {
            string dllFullPath = $"{aotDllSrcDir}/{dll}"; // 构造DLL文件的完整路径
            // string dllBytesPath = $"{hotfixAssembliesDstDir}/{dll}.bytes";
            string dllBytesPathForAddressable = $"{assembliesDstDirForAddressable}/{dll}.bytes";
            File.Copy(dllFullPath, dllBytesPathForAddressable, overwrite: true); // 将DLL文件复制到Addressable的bytes资源目录,并覆盖已有文件 
            Debug.Log($"[copy hotfix dll {dllPath} -> {dllBytesPathForAddressable} success!");
        }
    }

}

工具效果:
在这里插入图片描述
完成迁移后,即实现了元数据DLL和热更新DLL的更新,具体的DLL更新方式可根据个人需求灵活选择。

5.元数据DLL和热更新DLL的加载过程,在启动AOT代码中进行,以下是一个示例代码:

private IEnumerator LoadAotDll()
    {
        //这一步实际上是为了解决AOT 泛型类的问题 
        HomologousImageMode mode = HomologousImageMode.SuperSet;
        if (aotMetadataDllLabelRef == null)
        {
            Debug.Log("AOT元数据DLL标签为空");
            yield break;
        }
        yield return waitHandle = Addressables.LoadAssetsAsync<TextAsset>(aotMetadataDllLabelRef, null);

        List<TextAsset> aots = waitHandle.Result as List<TextAsset> //获取AOT元数据DLL
        if (aots != null) // 确保AOT元数据DLL列表不为空
        {
            foreach (var asset in aots)
            {
                LoadImageErrorCode errorCode = RuntimeApi.LoadMetadataForAOTAssembly(asset.bytes, mode); // 为AOT程序集加载元数据
                if (errorCode == LoadImageErrorCode.OK)
                {
                    Debug.Log($"加载AOT元数据DLL:{asset.name}成功");
                    continue;
                }

                Debug.Log($"加载AOT元数据DLL:{asset.name}失败,错误码:{errorCode}");
            }

        }
        else
        {
            Debug.Log("AOT元数据加载错误");
        }

    }

    private IEnumerator LoadHotFixDll()
    {
#if UNITY_EDITOR
        Assembly hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "HotUpdate");
        if (hotUpdateAss != null)
        {
            Debug.Log("已经加载热更DLL");
            yield break;
        }
#endif
        // 加载热更DLL
        // 这里使用标签来加载资源 Addressables会自动根据标签来加载所有资源
        yield return waitHandle = Addressables.LoadAssetsAsync<TextAsset>(hotUpdateDllLabelRef, null);
        List<TextAsset> dlls = waitHandle.Result as List<TextAsset>
        if (dlls == null)
        {
            Debug.Log("无热更DLL");
            yield break;
        }
        foreach (var asset in dlls)
        {
            Debug.Log("加载热更DLL:" + asset.name);
            Assembly.Load(asset.bytes);
            Debug.Log("加载热更DLL:" + asset.name + "完成");

        }
    }

此处采用了Addressables系统根据标签加载资源的方法,使得后续对元数据dlls和热更dll的修改变得更加灵活便捷。

3.HybridCLR代码热更工作流

按照快速入门的更新流程即可,该工具只是替代了手动迁移元数据与热更dll的操作
在这里插入图片描述

至此便完成了整个的HybridCLR热更流程。

在商业版中完全泛型共享技术就不需要上述流程中加载元数据dll,而只需要加载热更dll就行。
关于代码裁剪、裁剪后的dll内存大小以及dll优化等问题,建议参考官方文档进行深入研究和了解。


2.资源热更 Addressables

成功测试HybridCLR方案后,我开始研究代码和资源的热更技术。资源热更主要依赖于AssetBundle(AB包),通过编写AB包管理器来实现热更资源的加载。同时,利用Addressables(AA包)提供的管理器,可以方便地进行资源变更。使用Addressables插件过程中,遵循官方教程即可顺利实现。


3.资源服务器的搭建

在这里插入图片描述
Addressables的Host工具用于运行资源服务器,但实际操作中需频繁开关服务器更新资源。我曾多次遇到Addressables Hosting工具关闭不彻底,端口服务占用,需手动关闭进程的问题。因此,我尝试了其他工具来做资源服务器。
使用IIS作为资源服务器,因为它预装在Windows系统中且操作简便。

1.IIS的基础使用

详情使用请参阅这篇文章,在本文中就不赘述。

2.IIS与Addressables适配

在上文的基础上还要进行以下设置来避免访问资源被IIS屏蔽:

  1. 设置上传的资源文件夹的MIME类型(Windows打包的Addressables默认为StandaloneWindow64)在这里插入图片描述

  2. 点击添加,添加如下类型从而避免Addressables更新检查时网络请求被阻挡
    在这里插入图片描述
    在这里插入图片描述
    在这里插入图片描述
    出现问题时可以查看日志对网络请求进行查看并分析基本可以解决。
    在这里插入图片描述

  3. 如图设置网站和文件夹的身份验证让Addressables能够访问在这里插入图片描述

  4. 绑定网站的地址,之后用于内网穿透在这里插入图片描述
    在这里插入图片描述

每次更新后要刷新一下IIS中的网站和资源文件夹避免资源没有更新


4.热更资源的分发

CCD服务的缺陷与小米球的使用

热更资源可利用Unity的Cloud Content Delivery (CCD)服务,将资源存放在CCD,并且新版Addressables支持CCD服务。使用时,请通过Addressables安装CCD插件(见下图),而非在Package Manager中安装,以避免冲突(在2021.3.10f1c2版本存在此冲突)。
在这里插入图片描述
使用时我发现CCD网站加载缓慢,资源下载也需梯子,遇到网络问题时难以解决。并且CCD功能多样实际使用需要一定的学习成本
因此,我尝试使用IIS和内网穿透工具Ngrok来搭建一个简易的服务器分发热更资源。网络速度相比CCD好很多

小米球利用Ngrok技术并提供代理服务器可以帮助我们快速地进行内网穿透,具体操作可参考这篇文章
开启小米球后,设置好对应的IIS服务绑定的地址,并将左侧的任意一个网址填入Addressables的Remote资源的远程加载地址中即可。
在这里插入图片描述
在这里插入图片描述

至此,游戏更新流程已全部完成。

连接网络打开游戏后便能直接进行游戏的更新


5.本方案的热更体验

1.热更代码的验证与划分

测试代码热更新的一个简单方法是更改打印内容,这些内容可以直接显示在游戏屏幕上,或者使用我的这篇文章中提到的Quantum Console查看。
在进行热更新时,热更程序集的划分需仔细考量,官方文档对此有如下说明:
在这里插入图片描述

项目中,我将Assembly-CSharp用作AOT程序集,并拆分出热更程序集,使用Assembly-CSharp作为热更程序集会影响性能。

2.实际热更中的补救经验

在游戏的更新过程中,我总结了这些个人经验来将AOT脚本补救为热更脚本(仅供参考):

  1. AOT脚本热更补丁方法:
    在编辑器中创建一个新的脚本,同时删除需要添加为热更的AOT脚本,对项目进行重构,以实现原本AOT脚本转化为热更脚本。
  2. 挂载热更脚本的远程物体替换原AOT脚本物体的补丁方法:
    远程场景中可直接替换原挂载AOT脚本的物体为挂载热更脚本的物体,或者在远程场景中新增脚本来替换掉所有的原AOT脚本
  3. 远程场景替换本地场景的补丁方法:
    创建一个新的远程场景直接替换本地场景,这一步可以做到任意地热更原本地场景中的非持久化AOT脚本为热更脚本

3.热更方案的拓展

HybridCLR方案能够与其他技术如Lua相结合,并且能够在第二点的热更补丁方法的基础上实现高度的热更灵活性。

总结:提前考虑好热更资源与热更脚本为最佳,可以有效减少后续对AOT脚本或本地物体的重构


结语

本文通过个人探索经历,解析了更新全流程,旨在分享思考过程和提供经验借鉴。补充了HybridCLR教程中新手容易出现的误区,包括元数据补充和完全泛型共享机制的版本问题,帮助读者避免误区。探索了一个简单的个人资源服务器的搭建方案,实现了零成本且稳定的游戏更新流程,实际迭代并总结了热更的经验,提供了相关文章链接和操作提示,以规避方案中的重要错误。
尽管网上已有优秀的HybridCLR文章,我还是决定撰写这篇博客,因为我在探索过程中遇到了许多困难和挑战。我希望通过梳理思路,帮助他人,并提醒自己不要轻视学习过程,这不仅是对知识的尊重,也是对自己探索尝试的认可。
本文如有不妥之处,恳请各位大佬不吝赐教,予以指正。

文章参考

📌Unity WebGL不支持移动设备的问题
📌HybridCLR官网
📌HybridCLR官方AOT泛型问题文档
📌HybridCLR官方文档商业化版本的内容
📌Addressables官方教程
📌Windows IIS服务教程
📌小米球Ngrok内网穿透教程
📌逆向工程调试:解决Unity打包后程序异常问题的一种较通用的方法

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐