前言
大家好,我是开罗小8,今天给大家带来一篇如何使用Addressable复刻常见游戏资源预下载的功能。
资源预下载是指第一次进入游戏时,在游戏开始界面提前下载游戏需要的所有资源,或者在游戏选项中选择性下载一些资源,例如游戏的多语言配音等。
资源预下载的好处是减少游戏安装包的体积,游戏资源在需要时才会被下载,减少玩家的等待时间。有的应用商店会限制游戏安装包的大小,超过大小的应用不能上架。例如有的游戏安装包只有200M左右,但是进入游戏需要下载5G左右的资源。
Addressable是Unity官方推出的一个基于AssetBundle的资源管理系统,在PackageManager中,能够很方便的实现资源下载,更新,加载等功能,但需要较高的学习成本。
本文章以一个案例的形式介绍如何使用Addressable实现资源预下载以及更新的功能。Unity版本为2022.3.13。如果有错误的地方欢迎在评论区指正。
一,Addressable配置
在PackageManager安装好Addressable后,找到AddressableAssetsData/AddressableAssetSettings文件
主要修改有4处:
- Player Version Override:重写生成的Catalog版本的名字,此处设置为当前项目的游戏名。如果此处不填,每次New Build资源包时都会增加一个以打包时间来命名的Catalog文件。
- Build Remote Catalog:构建远程Catalog,必须勾选这个,才能让资源支持热更新。
- Build & Load Paths:Catalog构建和加载的路径,此处设置为Remote路径。
- Only update catalogs manually/Disable Catalog Update on Start:只通过手动的方式更新Catalog,不勾选的话在Addressable初始化后会自动更新Catalog。
Catalog是什么?
Catalog翻译过来是目录的意思,勾选Build Remote Catalog后才会生成,Catalog中记录着每个可寻址资源的信息,例如资源的Address和哈希值。在检查更新时,Addressble会比较本地Catalog中资源的哈希值和远程Catalog中资源的哈希值,如果不一样,说明资源需要更新。因此在更新资源前需要先更新Catalog。
二,Group设置
资源大小:
Video1:23MB
Video2:24MB
Video3:43MB
Video5:863MB
我在项目中设置了一个LocalGroup和三个RemoteGroup,每个资源都分配了一个Label,这个Label会用于之后选择性下载指定的资源使用。为了方便测试,此处使用了体积较大的视频文件。
点击Group,在Inspector会显示对应Group的属性,主要的配置内容如下:
- Build & Load Paths :这个Group的构建位置和加载位置,可以根据Path Preview 来预览路径,此处设置为Remote远程加载路径,构建的位置在项目根目录,此路径下的资源包不会包括在游戏本体中,可以在Window->Asset Management->Addressables->Profiles窗口修改路径。如果设置为Local,那么在打包后,资源包会被包含在游戏本体中StreamingAsset对应的位置。
- Include in Build:构建时是否包括当前Group,如果不勾选,不会构建当前Group
- Use Asset Bundle Cache:是否缓存AssetBundle,如果勾选,在第一次访问该远程资源时将会自动缓存到本地,下次加载时将读取缓存而不是重新下载。
- Internal Asset Naming Mode:资源在内部的命名格式,默认为Full Path,即Assets/XXX/xxx形式,设置为FileName后,命名为xxx,可以减小Catalog的大小。
- Bundle Mode:打包模式,此处设置为Pack Together,当前Group的所有资源都被打包进同一个Bundle。可以设置为Pack Separately,这样每一个资源都会单独被打成一个Bundle。
- Bundle Naming Mode:资源包的命名格式,默认会把新资源包的哈希值追加到文件名的末尾,此处设置为文件名,更新资源包时直接替换旧的资源包,避免出现许多无用的资源包。
三,修改默认缓存位置
如果勾选了缓存,在第一次下载资源时会保存到本地,可以对保存的位置进行修改,在Project窗口右键新建缓存配置文件
Cache Directory Override 写缓存的路径
选中AddressableAssetSettings文件,往下滑,找到Initialization Objects,点击加号,添加刚才创建的缓存文件
四,配置资源服务器
如果服务器使用http,需要在ProjectSettings中勾选Allow downloads over HTTP,否则在更新资源的时候会报错
为了便于测试,可以直接使用Addressable提供的本地服务器功能,通过Windows->Asset Management->Addressables->Hosting打开,需要确保使用默认的路径配置(Profile:Default),点击Enable后可以开启服务器,服务器的资源路径会自动定位到Remote资源的Build Path,如果使用自己的服务器,可以忽略这一步
五,资源更新代码
完整测试代码如下:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.Video;
using TMPro;
using UnityEngine.ResourceManagement.AsyncOperations;
using UnityEngine.UI;
public class Test : MonoBehaviour
{
public VideoPlayer VideoPlayer;
public GameObject Prefab;
public Transform Layout;
public string[] Groups;
public string[] VideoNames;
// Start is called before the first frame update
IEnumerator Start()
{
var checkUpdate = Addressables.CheckForCatalogUpdates(false);
yield return checkUpdate;
if (checkUpdate.Result.Count > 0)
{
Debug.Log("开始更新目录");
yield return Addressables.UpdateCatalogs();
}
Debug.Log("目录更新完毕");
Addressables.Release(checkUpdate);
for (int i = 0; i < Groups.Length; i++)
{
string label = Groups[i];
GameObject obj = Instantiate(Prefab, Layout);
obj.SetActive(true);
Button downloadButtn = obj.transform.Find("DownloadButton").GetComponent<Button>();
Button playButton = obj.transform.Find("PlayButton").GetComponent<Button>();
StartCoroutine(CheckSize(label, obj));
downloadButtn.onClick.AddListener(() => { StartCoroutine(StartDownload(label, obj)); });
if (i < VideoNames.Length)
{
var i1 = i;
playButton.onClick.AddListener(() => { StartCoroutine(PlayVideo(VideoNames[i1])); });
}
}
}
IEnumerator StartDownload(string label, GameObject prefab)
{
Button downloadButtn = prefab.transform.Find("DownloadButton").GetComponent<Button>();
Button playButton = prefab.transform.Find("PlayButton").GetComponent<Button>();
TMP_Text text = prefab.transform.Find("NameText").GetComponent<TMP_Text>();
long totalSize = 0;
var getSize = Addressables.GetDownloadSizeAsync(label);
yield return getSize;
totalSize = getSize.Result;
var downloadAsset = Addressables.DownloadDependenciesAsync(label);
double downloadSize = 0;
string speedStr = "";
float timer=0;
while (downloadAsset.Status == AsyncOperationStatus.None)
{
timer += Time.deltaTime;
if (timer > 0.3f)
{
double diff = totalSize * downloadAsset.PercentComplete - downloadSize;
diff /= timer;
speedStr = $"下载速度:{(diff / 1024.0 / 1024.0):F2}M/S";
downloadSize = totalSize * downloadAsset.PercentComplete;
timer = 0;
}
text.text = $"正在下载{label},进度:{downloadAsset.PercentComplete:P2}\n{speedStr}";
yield return null;
}
text.text = $"下载{label}完毕";
downloadButtn.gameObject.SetActive(false);
playButton.gameObject.SetActive(true);
Addressables.Release(downloadAsset);
}
IEnumerator CheckSize(string label, GameObject prefab)
{
Button downloadButtn = prefab.transform.Find("DownloadButton").GetComponent<Button>();
Button playButton = prefab.transform.Find("PlayButton").GetComponent<Button>();
TMP_Text text = prefab.transform.Find("NameText").GetComponent<TMP_Text>();
var getDownloadSize = Addressables.GetDownloadSizeAsync(label);
yield return getDownloadSize;
if (getDownloadSize.Result > 0)
{
text.text = $"{label}需要更新,大小:{(getDownloadSize.Result / 1024.0 / 1024.0):F2}MB";
downloadButtn.gameObject.SetActive(true);
playButton.gameObject.SetActive(false);
}
else
{
downloadButtn.gameObject.SetActive(false);
playButton.gameObject.SetActive(true);
text.text = $"{label}已是最新";
}
Addressables.Release(getDownloadSize);
}
private IEnumerator PlayVideo(string videoName)
{
var getVideo = Addressables.LoadAssetAsync<VideoClip>(videoName);
yield return getVideo;
VideoPlayer.clip = getVideo.Result;
VideoPlayer.Play();
}
}
代码解释:
资源更新的过程多为异步操作,因此使用协程更方便。
有些操作不会自动释放,可能导致内存泄露,因此在读取完对应的数据后,应该通过Addressables.Release(handler)的方式主动释放
通过handler.Status字段可以判断当前下载的状态,如果handler.Status == AsyncOperationStatus.None,表示正在下载中;handler.Status == AsyncOperationStatus.Succeed,表示下载成功;handler.Status == AsyncOperationStatus.Failed,表示下载失败。
1.更新Catalog
命名空间:
方法:
举例:var checkCatalog = Addressables.CheckForCatalogUpdates(false);检查目录是否有更新
checkCatalog.Result的类型List<string>,包括需要更新的Catalog的名称,如果列表个数为0,说明目录不需要更新
检查完更新后,使用var updateCatalog = Addressables.UpdateCatalogs();更新目录。
参数可以传递需要更新的目录列表来更新指定目录,类型为List<string>,也可以什么都不传,将更新所有目录。
2.检查资源是否有更新
使用var getDownloadSize = Addressables.GetDownloadSizeAsync(label);判断资源是否需要更新以及获取需要下载的资源的大小。如果getDownloadSize.Result = 0,说明资源不需要更新,否则就是需要下载的资源的大小,单位为字节。
括号中可以传入string或者List<string>,string如果是资源的Address,那么就只检测这个资源需要更新的大小,如果传入的是Label,那么就检查所有带有该Label的资源的大小。如果多个资源被打在同一个包,一个资源变更需要重新下载整个资源包,因此对于需要频繁更新的资源包,可以设置Group的Bundle Mode 为 Pack Separately,这将对每个资源都单独打一个包
3.更新资源
使用var downloadAsset = Addressables.DownloadDependenciesAsync(label);更新资源。
通过downloadAsset.PercentComplete可以获取当前进度的百分比,可以通过上面求的总资源大小乘以百分比来获取当前已下载的大小,并计算下载速度。括号中的参数与上面的一致。
更新完资源后一定要调用Addressables.Release(downloadAsset),如果不释放,下载完后再加载对应的资源会报无法加载资源包的错误。
六,最终结果展示
将项目打包,打包后大小为87.9MB,在本项目中,Video5的大小为800M左右,可见Video5并不包含在游戏中
进入游戏,下载Group2,下载完毕后视频能正常播放。
注:此处播放的是Video2,大小为24MB,下载完毕后点击播放,需要很长的加载时间,原因为Video2和Video5在同一个Bundle内,加载Video2需要将整个Bundle加载进内存,导致不需要的Video5也被加载了导致速度变慢,在这种情况下Group2的Bundle Mode选择Pack Separately会更合适。
再次查看游戏文件夹,体积从87.9MB变为968MB,增加了约880M,也就是Group2的大小
找到在之前设置的缓存文件夹,可以看到资源被缓存到了对应的文件夹 (缓存目录为{UnityEngine.Application.dataPath}/AABundles)