Unity热更新基础

前言

热更新指的是将在不让用户重新下载游戏的前提下对游戏中的资源(比如贴图,预制体,代码等)进行更新,本文中主要使用到Addressable

原理

Addressable在打包时主要分成两种,Local与Remote,Local是基础本地包,不可被更新的基础包,而Remote包则是放在服务器上供玩家下载的远程包,在这里存放需要热更新的资源或者附加资源,Addressable的热更本质是资源替换,不是增量更新。每次Build会生成新的Catalog和bundle文件,客户端下载完整的替换旧文件,不是像Git那样只下载差异部分。

准备工作

用PackManager下载Addressable包,在Windows中找到如下面板

其中Groups是关键,打开后大概是这个面板

可以点击左上角的New创建新的分组,每一个分组可以在检查器中选择是Local组还是Remote组。还有设置Remote组的存放地址等。将资源放入对应组最简单的方式是将其拖入对应分组,还有一种方式是利用利用AssetDatabase类将指定文件夹下的指定文件打包到同一组。一般分组情况如下:

  1. Local_Base:基础资源,永不改变
  2. Remote_Level:关卡资源,DLC地图等
  3. Remote_Audio:音频资源和图片等需要大量修改的
  4. Remote_Others:不常更新的资源

注意观察该面板中的一些属性,比如第一栏AddressableName,即对应文件的字符串键,在使用该资源可以通过该字符串加载对应资源,还有最后一栏Labels,类似Unity中的Tag,将统一类的资源分到一组,用于统一下载,比如将第一关的敌人都加上同一个Label,在进入关卡前就下载对应资源即可

注意资源需要打包后才能使用,右上角Build即是打包按钮,可以选择Update(小规模改动)和New Build(整个重新打包),打包后一般可以查看每一个包的大小等属性

使用包中资源

一般使用资源,有两种常见用法,使用拖引用的方式,使用字符串加载

加载资源需要用到异步加载的方式,当使用Addressables.LoadAssetAsync时,该方法会返回一个句柄,当不需要用到该资源,除了卸载该资源,还应该调用Addressables.Release方法释放该句柄,否则会导致引用计数异常。而使用InstantiateAsync的方法创建预制体时,则它会帮助创建预制体,至于拖引用的方式,则是用AssetReference类的LoadAssetAsync方式加载,本质上还是需要释放句柄。

复制代码
 public class AddressableLoader : MonoBehaviour
 {
   
     public async Task LoadAsset<T>(string key) where T:Object
     {
         AsyncOperationHandle<T> handle = Addressables.LoadAssetAsync<T>(key);
         await handle.Task;
         if(handle.Status==AsyncOperationStatus.Succeeded)
         {
             T asset= handle.Result;
             //用Load就要Release,不需要用到该资源时就Release,这里只是演示
             Addressables.Release(handle);
         }
     }
    
     public async Task LoadScene(string sceneKey)
     {
         var handle = Addressables.LoadSceneAsync(sceneKey,
             UnityEngine.SceneManagement.LoadSceneMode.Single);

         // 进度监控
         while (!handle.IsDone)
         {
             float progress = handle.PercentComplete;
             Debug.Log($"Loading: {progress:P}");
             await Task.Yield();
         }

         if (handle.Status == AsyncOperationStatus.Succeeded)
         {
             Debug.Log("Scene loaded!");
         }
     }
     public async Task InstantiatePrefab(string key, Vector3 position)
     {
         // 自动处理: 加载 + 实例化 + 依赖管理
         var handle = Addressables.InstantiateAsync(key, position, Quaternion.identity);
         GameObject instance = await handle.Task;

         // InstantiateAsync内部会引用计数+1,创建的GameObject销毁时引用计数-1,但handle本身需要手动Release。
     }
	//提前下载对应Label的资源,同样需要释放句柄
     public async Task PreloadDependencies(string label)
     {
         // 获取所有依赖,即标签
         var dependencies = Addressables.GetDownloadSizeAsync(label);
         long size = await dependencies.Task;

         if (size > 0)
         {
             Debug.Log($"Need download: {size / 1024 / 1024}MB");

             // 下载并加载到内存
             var downloadHandle = Addressables.DownloadDependenciesAsync(label);
             await downloadHandle.Task;

             Addressables.Release(downloadHandle);
         }
     }
 }

下载远程资源

每次打完远程包并添加到服务器上时,都会导致Catalog发生变化,而Addressable会检查配置是否发生变化,如果是,就可以尝试下载新的Remote包

复制代码
async Task<bool> CheckAndUpdate()
{
    try
    {
        // 1. 初始化
        var init = await Addressables.InitializeAsync().Task;

        // 2. 检查目录更新
        var checkHandle = Addressables.CheckForCatalogUpdates(false);
        var catalogs = await checkHandle.Task;

        if (catalogs != null && catalogs.Count > 0)
        {
            // 3. 更新目录
          
            await Addressables.UpdateCatalogs(catalogs).Task;

            // 4. 计算下载大小
            var sizeHandle = Addressables.GetDownloadSizeAsync(catalogs);
            long size = await sizeHandle.Task;
            Addressables.Release(sizeHandle);

            if (size > 0)
            {
                float mb = size / 1024f / 1024f;
                statusText.text = $"需要下载 {mb:F1}MB";

                // 5. 下载资源(带进度)
                var downloadHandle = Addressables.DownloadDependenciesAsync(catalogs);

                while (!downloadHandle.IsDone)
                {
                    progressBar.value = downloadHandle.PercentComplete;
                    statusText.text = $"下载中... {downloadHandle.PercentComplete:P0}";
                    await Task.Yield();
                }

                await downloadHandle.Task;
                Addressables.Release(downloadHandle);
            }
        }
        return true;
    }
    catch (Exception e)
    {
        Debug.LogError($"更新失败: {e}");
        return false;
    }
}

HybridCLR

Addressables本身并不支持将c#脚本打包,我们可以用到HybridCLR让C#代码能被当作资源下载执行。核心是将热更代码编译成DLL,运行时通过Assembly.Load加载

复制代码
// 下载DLL(Addressable)
var handle = Addressables.LoadAssetAsync<TextAsset>("HotUpdate.dll");
TextAsset dllAsset = await handle.Task;

// 加载执行(HybridCLR)
Assembly hotAssembly = Assembly.Load(dllAsset.bytes);
Type entryType = hotAssembly.GetType("HotUpdate.Main");
entryType.GetMethod("Start").Invoke(null, null);

详细内容跟随HybridCLR的Demo即可完全理解流程,这里不再赘述

相关推荐
云上空17 小时前
腾讯云使用对象存储托管并分享WebGL小游戏(unity3d)(需要域名)
unity·腾讯云·webgl·游戏开发·对象存储·网页托管
小贺儿开发19 小时前
Unity3D VR党史主题展馆
unity·人机交互·vr·urp·展馆·党史
TopGames19 小时前
Unity实现10万人同屏动态避障和导航寻路系统 支持3D地形
unity·性能优化·游戏引擎
在路上看风景1 天前
01. GUIContent
unity
托洛夫斯基扎沙耶1 天前
Unity中状态机与行为树的简单实现
unity·游戏引擎
TrudgeCarrot1 天前
unity打包使用SPB管线出现 DontSava错误解决
unity·游戏引擎·dontsave
3D霸霸1 天前
unity 创建URP新场景
unity·游戏引擎
玉梅小洋2 天前
Unity 2D游戏开发 Ruby‘s Adventure 1:课程介绍和资源导入
游戏·unity·游戏引擎·游戏程序·ruby
托洛夫斯基扎沙耶2 天前
Unity可视化工具链基础
unity·编辑器·游戏引擎
浅陌sss2 天前
检查Unity对象要始终使用 != null而不是?.
unity