Unity3D常用游戏功能复刻:基于Addressable资源管理实现游戏资源预下载,热更新,显示下载进度和资源大小,选择只下载指定资源

前言

大家好,我是开罗小8,今天给大家带来一篇如何使用Addressable复刻常见游戏资源预下载的功能。

资源预下载是指第一次进入游戏时,在游戏开始界面提前下载游戏需要的所有资源,或者在游戏选项中选择性下载一些资源,例如游戏的多语言配音等。

4048aa85e8ca4d0aa56fe2e3dc0fcee9.png

资源预下载的好处是减少游戏安装包的体积,游戏资源在需要时才会被下载,减少玩家的等待时间。有的应用商店会限制游戏安装包的大小,超过大小的应用不能上架。例如有的游戏安装包只有200M左右,但是进入游戏需要下载5G左右的资源。

72eb2220bd694475a7f08dea65b360b9.png

Addressable是Unity官方推出的一个基于AssetBundle的资源管理系统,在PackageManager中,能够很方便的实现资源下载,更新,加载等功能,但需要较高的学习成本。

本文章以一个案例的形式介绍如何使用Addressable实现资源预下载以及更新的功能。Unity版本为2022.3.13。如果有错误的地方欢迎在评论区指正。

一,Addressable配置

在PackageManager安装好Addressable后,找到AddressableAssetsData/AddressableAssetSettings文件

50b1cffc33944aaba91d914300d87f4e.png

主要修改有4处:

  1. Player Version Override:重写生成的Catalog版本的名字,此处设置为当前项目的游戏名。如果此处不填,每次New Build资源包时都会增加一个以打包时间来命名的Catalog文件。
  2. Build Remote Catalog:构建远程Catalog,必须勾选这个,才能让资源支持热更新。
  3. Build & Load Paths:Catalog构建和加载的路径,此处设置为Remote路径。
  4. Only update catalogs manually/Disable Catalog Update on Start:只通过手动的方式更新Catalog,不勾选的话在Addressable初始化后会自动更新Catalog。

Catalog是什么?

Catalog翻译过来是目录的意思,勾选Build Remote Catalog后才会生成,Catalog中记录着每个可寻址资源的信息,例如资源的Address和哈希值。在检查更新时,Addressble会比较本地Catalog中资源的哈希值和远程Catalog中资源的哈希值,如果不一样,说明资源需要更新。因此在更新资源前需要先更新Catalog。

二,Group设置

d50f845bd952424a852961d0fde00fcb.png

资源大小:

Video1:23MB

Video2:24MB

Video3:43MB

Video5:863MB

我在项目中设置了一个LocalGroup和三个RemoteGroup,每个资源都分配了一个Label,这个Label会用于之后选择性下载指定的资源使用。为了方便测试,此处使用了体积较大的视频文件。

点击Group,在Inspector会显示对应Group的属性,主要的配置内容如下: 

cb97bd97c3db406192faec9b45f7a5ce.png

  1. Build & Load Paths :这个Group的构建位置和加载位置,可以根据Path Preview 来预览路径,此处设置为Remote远程加载路径,构建的位置在项目根目录,此路径下的资源包不会包括在游戏本体中,可以在Window->Asset Management->Addressables->Profiles窗口修改路径。如果设置为Local,那么在打包后,资源包会被包含在游戏本体中StreamingAsset对应的位置。
  2. Include in Build:构建时是否包括当前Group,如果不勾选,不会构建当前Group
  3. Use Asset Bundle Cache:是否缓存AssetBundle,如果勾选,在第一次访问该远程资源时将会自动缓存到本地,下次加载时将读取缓存而不是重新下载。
  4. Internal Asset Naming Mode:资源在内部的命名格式,默认为Full Path,即Assets/XXX/xxx形式,设置为FileName后,命名为xxx,可以减小Catalog的大小。
  5. Bundle Mode:打包模式,此处设置为Pack Together,当前Group的所有资源都被打包进同一个Bundle。可以设置为Pack Separately,这样每一个资源都会单独被打成一个Bundle。
  6. Bundle Naming Mode:资源包的命名格式,默认会把新资源包的哈希值追加到文件名的末尾,此处设置为文件名,更新资源包时直接替换旧的资源包,避免出现许多无用的资源包。

三,修改默认缓存位置

如果勾选了缓存,在第一次下载资源时会保存到本地,可以对保存的位置进行修改,在Project窗口右键新建缓存配置文件

3c8acb6ad4cf453b9ea528ba61c0fb90.png

 Cache Directory Override 写缓存的路径

cc58158e91ad4a9a94787213d61e1496.png

选中AddressableAssetSettings文件,往下滑,找到Initialization Objects,点击加号,添加刚才创建的缓存文件

1b227e84aafe4b1095a4d4974d3acc9f.png

 四,配置资源服务器

如果服务器使用http,需要在ProjectSettings中勾选Allow downloads over HTTP,否则在更新资源的时候会报错

3d412ce4e046421d8d3b648609d0101f.png

为了便于测试,可以直接使用Addressable提供的本地服务器功能,通过Windows->Asset Management->Addressables->Hosting打开,需要确保使用默认的路径配置(Profile:Default),点击Enable后可以开启服务器,服务器的资源路径会自动定位到Remote资源的Build Path,如果使用自己的服务器,可以忽略这一步 

e5df6a23207f4bd892420cbf14733cb0.png

五,资源更新代码

完整测试代码如下:

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();
    }

}

57e50ff9b1804b6f9fcd2cbd49fb3624.png

代码解释:

资源更新的过程多为异步操作,因此使用协程更方便。

有些操作不会自动释放,可能导致内存泄露,因此在读取完对应的数据后,应该通过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并不包含在游戏中

31c1bea4eb1442fabbe10efb76c6e2a1.png

进入游戏,下载Group2,下载完毕后视频能正常播放。

c9288cc62d314f8d91cd15b27e29e7f5.gif

注:此处播放的是Video2,大小为24MB,下载完毕后点击播放,需要很长的加载时间,原因为Video2和Video5在同一个Bundle内,加载Video2需要将整个Bundle加载进内存,导致不需要的Video5也被加载了导致速度变慢,在这种情况下Group2的Bundle Mode选择Pack Separately会更合适。 

 再次查看游戏文件夹,体积从87.9MB变为968MB,增加了约880M,也就是Group2的大小

355627901f2648109061116ca4a9545c.png

找到在之前设置的缓存文件夹,可以看到资源被缓存到了对应的文件夹 (缓存目录为{UnityEngine.Application.dataPath}/AABundles)

0eadadeb0c204f519058f76234d6182e.png

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值