前言
热更新指的是将在不让用户重新下载游戏的前提下对游戏中的资源(比如贴图,预制体,代码等)进行更新,本文中主要使用到Addressable
原理
Addressable在打包时主要分成两种,Local与Remote,Local是基础本地包,不可被更新的基础包,而Remote包则是放在服务器上供玩家下载的远程包,在这里存放需要热更新的资源或者附加资源,Addressable的热更本质是资源替换,不是增量更新。每次Build会生成新的Catalog和bundle文件,客户端下载完整的替换旧文件,不是像Git那样只下载差异部分。
准备工作
用PackManager下载Addressable包,在Windows中找到如下面板

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

可以点击左上角的New创建新的分组,每一个分组可以在检查器中选择是Local组还是Remote组。还有设置Remote组的存放地址等。将资源放入对应组最简单的方式是将其拖入对应分组,还有一种方式是利用利用AssetDatabase类将指定文件夹下的指定文件打包到同一组。一般分组情况如下:
- Local_Base:基础资源,永不改变
- Remote_Level:关卡资源,DLC地图等
- Remote_Audio:音频资源和图片等需要大量修改的
- 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即可完全理解流程,这里不再赘述