知识点来源:人间自有韬哥在,hybridclr,豆包
目录
- 一、代码热更新
- 1.代码热更新概述
- 2.HybridCLR
- 二、资源热更新
- 1.资源热更新概述
- 2.AB包
- 2.1.AB包的加载
- 2.2.卸载AB包
- 2.3.加载AB包依赖包
- 2.4.获取MD5
- 2.5.生成对比文件
- 2.6.更新AB包
- 3.Addressable
- 3.1.AssetReference资源标识类
- 3.2.加载资源
- 3.3. 异步加载场景
- 3.4.释放资源
- 3.5.通过名字加载资源
- 3.6.通过名字释放资源
- 3.7.通过名字动态加载场景
- 3.8.根据资源名或标签名加载多个对象
- 3.9.根据多种信息加载对象
- 3.10.根据资源定位信息加载资源
- 3.11.AsyncOperationHandle
一、代码热更新
1.代码热更新概述
代码热更新是指在应用程序运行过程中,无需重新启动整个程序,就能对代码进行更新和修改,使新的代码逻辑生效。在游戏开发等领域,这一技术极为关键。例如,当游戏上线后发现严重的代码漏洞或需要快速添加新功能时,代码热更新能让开发者迅速修复问题或推送新内容,避免玩家因重新下载和安装完整游戏而流失。
2.HybridCLR
(1)安装 2019.4.40、2020.3.26+、 2021.3.0+、2022.3.0+ 中任一版本
(2)填入https://gitee.com/focus-creative-games/hybridclr_unity.git或https://github.com/focus-creative-games/hybridclr_unity.git
(3)打开菜单HybridCLR/Installer…, 点击安装按钮进行安装。 耐心等待30s左右,安装完成后会在最后打印 安装成功日志。
(4)创建 Assets/HotUpdate 目录,在目录下 右键 Create/Assembly Definition,创建一个名为HotUpdate的程序集模块
(5)打开菜单 HybridCLR/Settings, 在Hot Update Assemblies配置项中添加HotUpdate程序集
(5)配置PlayerSettings
- 如果你用的hybridclr包低于v4.0.0版本,需要关闭增量式GC(Use Incremental GC) 选项
- Scripting Backend 切换为 IL2CPP
- Api Compatability Level 切换为 .Net 4.x(Unity 2019-2020) 或 .Net Framework(Unity 2021+)
(6)创建热更新脚本, Assets/HotUpdate/Hello.cs
using System.Collections;
using UnityEngine;
public class Hello
{
public static void Run()
{
Debug.Log("Hello, HybridCLR");
}
}
(7)创建Assets/LoadDll.cs脚本
using HybridCLR;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.Networking;
public class LoadDll : MonoBehaviour
{
void Start()
{
// Editor环境下,HotUpdate.dll.bytes已经被自动加载,不需要加载,重复加载反而会出问题。
#if !UNITY_EDITOR
Assembly hotUpdateAss = Assembly.Load(File.ReadAllBytes($"{Application.streamingAssetsPath}/HotUpdate.dll.bytes"));
#else
// Editor下无需加载,直接查找获得HotUpdate程序集
Assembly hotUpdateAss = System.AppDomain.CurrentDomain.GetAssemblies().First(a => a.GetName().Name == "HotUpdate");
#endif
Type type = hotUpdateAss.GetType("Hello");
type.GetMethod("Run").Invoke(null, null);
}
}
(8)打包运行
- 运行菜单
HybridCLR/Generate/All
进行必要的生成操作。这一步不可遗漏!!! - 将{proj}/HybridCLRData/HotUpdateDlls/StandaloneWindows64(MacOS下为StandaloneMacXxx)目录下的HotUpdate.dll复制到Assets/StreamingAssets/HotUpdate.dll.bytes,注意,要加.bytes后缀!!!
- 打开Build Settings对话框,点击Build And Run,打包并且运行热更新示例工程。
(9) 测试更新
- 修改Assets/HotUpdate/Hello.cs的Run函数中Debug.Log(“Hello, HybridCLR”);代码,改成Debug.Log(“Hello, World”);。
- 运行菜单命令
HybridCLR/CompileDll/ActiveBulidTarget
重新编译热更新代码。 - 将{proj}/HybridCLRData/HotUpdateDlls/StandaloneWindows64(MacOS下为StandaloneMacXxx)目录下的HotUpdate.dll复制为刚才的打包- 输出目录的 XXX_Data/StreamingAssets/HotUpdate.dll.bytes。
- 重新运行程序,会发现屏幕中显示Hello, World,表示热更新代码生效了!
二、资源热更新
1.资源热更新概述
资源热更新是指在应用程序运行时,对游戏或应用的资源(如图片、音频、视频、模型等)进行更新,而无需重新安装整个应用。这在游戏开发中尤为重要,因为游戏资源通常较大,且随着游戏的运营,需要不断更新资源以提供新的内容和优化体验。例如,一款手机游戏需要定期更新角色皮肤、地图场景、剧情动画等资源,通过资源热更新,玩家无需重新下载整个游戏包,就能获取并使用这些新资源。
2.AB包
2.1.AB包的加载
AB包的同步加载
- 加载AB包:使用
AssetBundle.LoadFromFile
方法可同步地从指定路径加载AB包,并返回加载后的AB包对象。由于通常在StreamingAssets
中会多拷贝一份,所以可以直接从该路径加载。
示例代码:
AssetBundle modelAssetBundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "model");
注意:同一个AB包不能重复加载,否则会报错,如以下代码会引发错误:
//AssetBundle modelAssetBundle2 = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "model");//报错
- 加载AB包中的资源:通过
AssetBundle.LoadAsset
方法,可从一个已加载的AB包中同步加载指定资源。
示例代码:
// 不建议只传名字,因为可能会得到同名不同类型的资源
GameObject cube = modelAssetBundle.LoadAsset("Cube", typeof(GameObject)) as GameObject;
Instantiate(cube);
// 同一个AB包可用于加载多个不同资源
GameObject sphere = modelAssetBundle.LoadAsset<GameObject>("Sphere");
Instantiate(sphere);
AB包的异步加载
- 加载AB包:
AssetBundle.LoadFromFileAsync
方法用于异步地从指定路径加载AB包,返回一个异步加载AB包的类AssetBundleCreateRequest
。 - 加载AB包中的资源:
AssetBundle.LoadAssetAsync
方法可异步地从一个已加载的AB包中加载指定资源。异步加载也有多个重载方法,使用时需注意避免仅传名字的方式(可能得到同名不同类型资源),泛型加载在Lua中不支持,建议传入资源的Type。
示例协程代码:
IEnumerator LoadAssetBundleAsync<T>(string AssetBundleName, string resourceName, Type resourceType)
{
// 异步加载AB包
AssetBundleCreateRequest assetBundleCreateRequest = AssetBundle.LoadFromFileAsync(Application.streamingAssetsPath + "/" + AssetBundleName);
yield return assetBundleCreateRequest;
// 异步加载AB包中的资源
// 不建议只传名字
// AssetBundleRequest assetBundleRequest = assetBundleCreateRequest.assetBundle.LoadAssetAsync(resourceName);
AssetBundleRequest assetBundleRequest = assetBundleCreateRequest.assetBundle.LoadAssetAsync(resourceName, resourceType);
// 泛型加载在Lua中不支持
// AssetBundleRequest assetBundleRequest = assetBundleCreateRequest.assetBundle.LoadAssetAsync<T>(resourceName);
yield return assetBundleRequest;
// 获取实际加载出的资源并as成实际类型
image.sprite = assetBundleRequest.asset as Sprite;
}
- 使用协程进行异步加载:通过
StartCoroutine
开启协程进行异步加载,要确保传入的资源类型正确。
示例代码:
StartCoroutine(LoadAssetBundleAsync<Sprite>("icon", "quanlity_0", typeof(Sprite)));
2.2.卸载AB包
- 卸载指定AB包:
AssetBundle.Unload
方法用于卸载指定的AB包,传入的参数bool
值表示是否卸载场景上加载出来的AB包资源。
示例代码:
modelAssetBundle.Unload(false);
- 卸载所有已加载的AB包:
AssetBundle.UnloadAllAssetBundles
方法可卸载所有已加载的AB包,同样传入的参数bool
值表示是否卸载场景上加载出来的AB包资源。
示例代码:
AssetBundle.UnloadAllAssetBundles(false);
2.3.加载AB包依赖包
//加载主包 主包的名字和AB包生成路径一样
AssetBundle mainAssetBundle = AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + "PC");
//加载AB包依赖关系清单文件
//AssetBundleManifest是Unity中用于管理AB包依赖关系的类。
AssetBundleManifest assetBundleManifest = mainAssetBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest");
//GetAllDependencies方法 传入AB包名得到AB包依赖的所有AB包
string[] modelDependencieAssetBundleNameArray = assetBundleManifest.GetAllDependencies("model");
//遍历依赖的所有AB包
for (int i = 0;i< modelDependencieAssetBundleNameArray.Length;i++)
{
Debug.Log(modelDependencieAssetBundleNameArray[i]);//icon
AssetBundle.LoadFromFile(Application.streamingAssetsPath + "/" + modelDependencieAssetBundleNameArray[i]);//遍历加载所有依赖的AB包
}
GameObject cube2 = modelAssetBundle.LoadAsset("Cube", typeof(GameObject)) as GameObject;
Instantiate(cube2);//生成的cube因为加载了所需要的所有依赖包 不会丢失材质
2.4.获取MD5
通过资源名或者资源大小无法判断资源是否更新,因此需要利用MD5的唯一性来判断资源的更新
private string GetMD5(string filePath)
{
// 将文件以流的形式打开
using (FileStream fileStream = new FileStream(filePath, FileMode.Open))
{
// 声明一个MD5对象用于生成MD5码
MD5 md5 = new MD5CryptoServiceProvider();
// 利用ComputeHash方法得到数据的MD5码,返回一个字节数组
byte[] md5Info = md5.ComputeHash(fileStream);
// 关闭文件流
fileStream.Close();
// 将字节数组形式的MD5码转换为16进制字符串
StringBuilder stringBuilder = new StringBuilder();
for (int i = 0; i < md5Info.Length; i++)
{
// 将字节数组中的每个字节转换为两位的16进制字符串,并追加到stringBuilder中 x2代表转成16进制
stringBuilder.Append(md5Info[i].ToString("x2"));
}
// 返回生成的MD5字符串
return stringBuilder.ToString();
}
}
2.5.生成对比文件
[MenuItem("AB包工具/创建对比文件")]
public static void CreateABCompareFile()
{
//获取文件夹信息
DirectoryInfo directory = Directory.CreateDirectory(Application.dataPath + "/ArtRes/AB/PC/");
//获取该目录下的所有文件信息
FileInfo[] fileInfos = directory.GetFiles();
//用于存储信息的 字符串
string abCompareInfo = "";
foreach (FileInfo info in fileInfos)
{
//没有后缀的 才是AB包 我们只想要AB包的信息
if(info.Extension == "")
{
//拼接一个AB包的信息
abCompareInfo += info.Name + " " + info.Length + " " + GetMD5(info.FullName);
//用一个分隔符分开不同文件之间的信息
abCompareInfo += '|';
}
}
//因为循环完毕后 会在最后由一个 | 符号 所以 把它去掉
abCompareInfo = abCompareInfo.Substring(0, abCompareInfo.Length - 1);
//存储拼接好的 AB包资源信息
File.WriteAllText(Application.dataPath + "/ArtRes/AB/PC/ABCompareInfo.txt", abCompareInfo);
//刷新编辑器
AssetDatabase.Refresh();
Debug.Log("AB包对比文件生成成功");
}
2.6.更新AB包
遍历远端 AB 包字典
发现本地 AB 包字典没有对应 AB 包就直接下载
发现本地 AB 包字典有对应 AB 包但是 MD5 码不同就更新
遍历远端 AB 包字典时边遍历边移除本地 AB 包字典可能存在的 AB 包,这样本地 AB 包字典剩下的就是要删除的 AB 包
3.Addressable
3.1.AssetReference资源标识类
AssetReference
:通用资源标识类,可用于加载任意类型资源AssetReferenceAtlasedSprite
:图集资源标识类AssetReferenceGameObject
:游戏对象资源标识类AssetReferenceSprite
:精灵图片资源标识类AssetReferenceTexture
:贴图资源标识类AssetReferenceTexture2D
AssetReferenceTexture3D
AssetReferenceT<T>
:指定类型标识类
通过声明不同类型标识类对象,可在Inspector窗口中筛选关联的资源对象,示例代码:
public AssetReference assetReference;
public AssetReferenceAtlasedSprite assetReferenceAtlasedSprite;
public AssetReferenceGameObject assetReferenceGameObject;
public AssetReferenceSprite assetReferenceSprite;
public AssetReferenceTexture assetReferenceTexture;
public AssetReferenceT<AudioClip> assetReferenceTAudioClip;
public AssetReferenceT<RuntimeAnimatorController> assetReferenceTRuntimeAnimatorController;
public AssetReferenceT<TextAsset> assetReferenceTTextAsset;
public AssetReferenceT<Material> assetReferenceTMaterial;
public AssetReference AssetReferenceScene;
3.2.加载资源
- 注意事项:
- 所有Addressables加载相关都使用异步加载。
- 加载资源时建议选择
Simulate Groups(advanced)
模拟模式,而非Use Existing Build(requires built groups)
(从AB包模式中加载)。
- 相关方法:
AssetReference.LoadAssetAsync
:异步加载可寻址资源,返回异步操作处理者AsyncOperationHandle
对象。AsyncOperationHandle.Completed
:完成回调,添加监听函数进行对完成事件的监听。AsyncOperationHandle.Status
:异步加载状态,一般用于判断是否成功。AsyncOperationHandle.Result
:异步加载出来的资源。AssetReference.IsDone
判断资源是否加载AssetReference.Asset
加载出来的资源
示例代码:
// 异步加载可寻址资源
AsyncOperationHandle<GameObject> asyncOperationHandle = assetReference.LoadAssetAsync<GameObject>();
// 添加完成回调监听
asyncOperationHandle.Completed += OnAsyncOperationHandleCompleted;
// 加载成功后的回调函数
private void OnAsyncOperationHandleCompleted(AsyncOperationHandle<GameObject> asyncOperationHandle)
{
if (asyncOperationHandle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(asyncOperationHandle.Result);
}
// 使用标识类创建
// if(assetReference.IsDone)
// {
// Instantiate(assetReference.Asset);
// }
}
3.3. 异步加载场景
使用AssetReference.LoadSceneAsync
异步加载场景,并添加Completed
回调。
AssetReferenceScene.LoadSceneAsync().Completed += (asyncOperationHandle) =>
{
print("场景加载结束");
};
3.4.释放资源
- 使用
AssetReference.ReleaseAsset
释放资源,建议在加载成功且用完资源后,在加载成功的回调中释放,避免没加载成功就释放。 - 注意事项:
- 执行释放资源方法后,资源标识类中的资源会置空,但
AsyncOperationHandle
类中的对象不为空。 - 释放资源不会影响场景中已实例化出来的对象,但会影响使用的资源(如AB包模式下赋值材质后释放会丢失材质)。
- 执行释放资源方法后,资源标识类中的资源会置空,但
示例代码:
assetReference.LoadAssetAsync<GameObject>().Completed += (asyncOperationHandle) =>
{
if (asyncOperationHandle.Status == AsyncOperationStatus.Succeeded)
{
GameObject cube = Instantiate(asyncOperationHandle.Result);
assetReference.ReleaseAsset();
assetReferenceTMaterial.LoadAssetAsync().Completed += (materialAsyncOperationHandle) =>
{
cube.GetComponent<MeshRenderer>().material = materialAsyncOperationHandle.Result;
assetReferenceTMaterial.ReleaseAsset();
print(materialAsyncOperationHandle.Result);
print(assetReferenceTMaterial.Asset);
};
}
};
接实例化对象
使用AssetReference.InstantiateAsync
异步实例化对象,该方法有一些重载可调整位置角度等,相当于自动完成LoadAssetAsync
并在Completed
回调中实例化对象的操作,一般适用于GameObject预设体。
assetReferenceGameObject.InstantiateAsync();
通过标签相关特性约束标识类对象
意味着assetReference只能关联标签为”SD”, “HD”, “FHD”的资源
[AssetReferenceUILabelRestriction("SD", "HD", "FHD")]
public AssetReference assetReference;
3.5.通过名字加载资源
动态加载单个资源
在Unity中,可使用Addressables
来动态加载单个资源,需引用UnityEngine.AddressableAssets
和UnityEngine.ResourceManagement.AsyncOperations
命名空间。使用Addressables.LoadAssetAsync
方法,传入资源名或标签名来动态加载单个资源。
示例代码
Addressables.LoadAssetAsync<GameObject>("Red").Completed += (asyncOperationHandle) =>
{
if (asyncOperationHandle.Status == AsyncOperationStatus.Succeeded)
Instantiate(asyncOperationHandle.Result);
};
** 注意事项**
- 重复加载相同资源不会报错,但没有对应的Key会报错。
- 若存在同名或同标签的同类型资源,会自动加载找到的第一个满足条件的对象。
- 若存在同名或同标签的不同类型资源,可根据泛型类型来决定加载哪一个。
3.6.通过名字释放资源
使用Addressables.Release
方法释放资源,要在资源加载完成且使用完毕后进行释放。
asyncOperationHandle = Addressables.LoadAssetAsync<GameObject>("Cube");
asyncOperationHandle.Completed += (handle) =>
{
if (handle.Status == AsyncOperationStatus.Succeeded)
Instantiate(handle.Result);
Addressables.Release(handle);
};
// 在对象被删除后释放是较为合理的
private void OnDestroy()
{
Addressables.Release(asyncOperationHandle);
}
3.7.通过名字动态加载场景
使用Addressables.LoadSceneAsync
动态加载场景,可手动激活异步加载的场景。
Addressables.LoadSceneAsync("SampleScene", UnityEngine.SceneManagement.LoadSceneMode.Single, false)
.Completed += (handle) =>
{
Debug.Log("异步场景加载完成");
handle.Result.ActivateAsync().completed += (a) =>
{
Debug.Log("异步激活场景完成");
Addressables.Release(handle);
};
};
3.8.根据资源名或标签名加载多个对象
使用Addressables.LoadAssetsAsync
传入一个Key异步加载多个资源。
// 回调函数处理资源obj
Addressables.LoadAssetsAsync<Object>("Cube", (obj) =>
{
print("回调函数处理资源" + obj.name);
});
// asyncOperationHandle完成事件中处理资源handle.Result
AsyncOperationHandle<IList<Object>> asyncOperationHandle = Addressables.LoadAssetsAsync<Object>("Red", (obj) =>
{
// print(obj.name);
});
asyncOperationHandle.Completed += (handle) =>
{
foreach (var item in handle.Result)
{
print("asyncOperationHandle完成时间中处理资源" + item.name);
}
Addressables.Release(handle);
};
3.9.根据多种信息加载对象
使用Addressables.LoadAssetsAsync
传入Key列表异步加载多个资源,可指定合并模式。
List<string> keyList = new List<string>() { "Cube", "Red" };
Addressables.LoadAssetsAsync<Object>(keyList, (obj) => { print("根据多种信息加载对象" + obj.name); },
Addressables.MergeMode.Intersection);
3.10.根据资源定位信息加载资源
根据名字或者标签获取资源定位信息加载资源
可使用Addressables.LoadResourceLocationsAsync
异步加载单Key资源定位信息,再通过Addressables.LoadAssetAsync
传入ResourceLocation
资源定位信息进行资源加载。
示例代码如下:
// 异步加载资源定位信息 返回列表型handle
AsyncOperationHandle<IList<IResourceLocation>> asyncOperationHandle = Addressables.LoadResourceLocationsAsync("Cube", typeof(GameObject));
asyncOperationHandle.Completed += (handle) =>
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
// handle.Result是所有满足条件的资源的资源定位信息列表
foreach (var resourceLocation in handle.Result)
{
// 打印出资源名
print(resourceLocation.PrimaryKey);
// 根据资源定位信息异步加载资源
Addressables.LoadAssetAsync<GameObject>(resourceLocation).Completed += (obj) => { Instantiate(obj.Result); };
}
}
else
{
Addressables.Release(asyncOperationHandle);
}
};
根据名字标签组合信息获取资源定位信息加载资源
使用Addressables.LoadResourceLocationsAsync
异步加载多Key资源定位信息,结合资源名和标签名的组合、合并模式以及资源类型进行加载。
示例代码如下:
// 异步加载组合型资源定位信息 返回列表型handle
AsyncOperationHandle<IList<IResourceLocation>> asyncOperationHandle2 = Addressables.LoadResourceLocationsAsync(new List<string>() { "Cube", "Sphere", "SD" }, Addressables.MergeMode.Union, typeof(Object));
asyncOperationHandle2.Completed += (handle) =>
{
if (handle.Status == AsyncOperationStatus.Succeeded)
{
// 资源定位信息加载成功
foreach (var resourceLocation in handle.Result)
{
// 使用定位信息来加载资源
print("******");
print(resourceLocation.PrimaryKey); // 资源名
print(resourceLocation.InternalId); // 资源路径
print(resourceLocation.ResourceType.Name); // 资源类型
Addressables.LoadAssetAsync<Object>(resourceLocation).Completed += (obj) =>
{
// Instantiate(obj.Result);
};
}
}
else
{
Addressables.Release(asyncOperationHandle2);
}
};
3.11.AsyncOperationHandle
获取加载进度
通过 AsyncOperationHandle.GetDownloadStatus
可获取下载状态,包含以下属性:
DownloadStatus.Percent
:下载进度DownloadStatus.DownloadedBytes
:当前已下载字节数DownloadStatus.TotalBytes
:总共需下载字节数
示例代码:
IEnumerator LoadAssetCoroutine()
{
AsyncOperationHandle<GameObject> asyncOperationHandle = Addressables.LoadAssetAsync<GameObject>("Cube");
while (!asyncOperationHandle.IsDone)
{
DownloadStatus downloadStatus = asyncOperationHandle.GetDownloadStatus();
print(downloadStatus.Percent);
print(downloadStatus.DownloadedBytes + "/" + downloadStatus.TotalBytes);
yield return 0;
}
if (asyncOperationHandle.Status == AsyncOperationStatus.Succeeded)
{
Instantiate(asyncOperationHandle.Result);
}
else
Addressables.Release(asyncOperationHandle);
}
StartCoroutine(LoadAssetCoroutine());
无类型句柄转换
- 有类型的
AsyncOperationHandle
可隐式转换为无类型的AsyncOperationHandle
。 - 使用
AsyncOperationHandle.Convert<T>()
可将无类型句柄转换为有类型的泛型对象。
示例代码:
AsyncOperationHandle<Texture2D> asyncOperationHandleTexture2D = Addressables.LoadAssetAsync<Texture2D>("Cube");
// 有类型转无类型
AsyncOperationHandle tempAsyncOperationHandle = asyncOperationHandleTexture2D;
// 无类型转有类型
asyncOperationHandleTexture2D = tempAsyncOperationHandle.Convert<Texture2D>();
这种转换便于在 Addressables
管理器字典中使用 AsyncOperationHandle
类型。
强制同步加载资源
使用 AsyncOperationHandle.WaitForCompletion
可等待异步加载完成后继续执行后续代码。
示例代码:
print("1");
asyncOperationHandleTexture2D.WaitForCompletion();
print("2");
print(asyncOperationHandleTexture2D.Result.name);
注意:
- Unity 2020.1 及之前版本,执行该代码会等待所有未完成的异步加载操作完成。
- Unity 2020.2 及之后版本,加载已下载资源时性能影响较小。
总体不建议使用该方式加载资源,因其可能阻塞主线程。