什么是xasset 4.0
众所周知,Unity资产管理方面的知识十分细碎,很多细节稍不注意就会导致资源冗余或者内存泄漏,很多前辈也在为解决这个问题不懈的努力。
今天为大家介绍的是之前有直播过的一个开源的Unity项目资源管里利器,因为它发布了新的4.0版本,支持了很多新的特性所以需要重新给大家再介绍下。
我本人的风格一向是从运行Demo开始,逐步分析理解它的架构,所以这个指南也不会一开始就从宏观上带大家去理解(其实是功课没做足,确实不知道是什么个情况 XD),不过有一说一,个人觉得以这种行文方式非常适合做入门指南
下载
https://github.com/xasset/xasset
git大家肯定都会用,如果速度慢,可以从我的xasset码云镜像拉取:https://gitee.com/NKG_admin/xasset_Gitsync.git
环境
游戏引擎: Unity 2019.4.0 LTF
.Net框架:.Net Framework 4.7.2
IDE:Rider 2019.3
xasset版本:截至此Commmit https://github.com/xasset/xasset/commit/3d4983cd24ff92a63156c8078caf34b20d2d4c02
运行
来到Init场景,直接点击运行(我们可以看到UI界面相当有内味,作者下了血本在Asset Store购买的,泪目)
这个VFS,全名Virtual File System,用于提高IO性能(android)和安全性,建议开启,后面会细谈。
资源热更新
作者已经配置好了远程资源服务器路径
如果有内容更新,就会出现这个界面(不过速度很慢就是了,因为现在我们还整不明白怎么打出AB包,所以就先用Demo的这个云端文件服务器,后面推荐给大家一个本地的虚拟文件服务器,用于学习和研究框架)
我们来看控制台
第一个Log主要来自Versions.cs和Updater.cs
第二个和第三个Log主要来自Download.cs和Downloader.cs,我们一个一个看
Versions.cs
通过翻看其相关联的部分源码,可以看到他主要是负责资源版本信息的构建与加载 的,并且在构建和加载版本信息时就已经用到了VFS。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 public static void BuildVersions (string outputPath, string [] bundles, int version ) { var path = outputPath + "/" + Filename; if (File.Exists(path)) { File.Delete(path); } var dataPath = outputPath + "/" + Dataname; if (File.Exists(dataPath)) { File.Delete(dataPath); } var disk = new VDisk(); foreach (var file in bundles) { using (var fs = File.OpenRead(outputPath + "/" + file)) { disk.AddFile(file, fs.Length, Utility.GetCRC32Hash(fs)); } } disk.name = dataPath; disk.Save(); using (var stream = File.OpenWrite(path)) { var writer = new BinaryWriter(stream); writer.Write(version); writer.Write(disk.files.Count + 1 ); using (var fs = File.OpenRead(dataPath)) { var file = new VFile {name = Dataname, len = fs.Length, hash = Utility.GetCRC32Hash(fs)}; file.Serialize(writer); } foreach (var file in disk.files) { file.Serialize(writer); } } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public static List<VFile> LoadVersions (string filename, bool update = false ) { var data = update ? _updateData : _baseData; data.Clear(); using (var stream = File.OpenRead(filename)) { var reader = new BinaryReader(stream); var list = new List<VFile>(); var ver = reader.ReadInt32(); Debug.Log("LoadVersions:" + ver); var count = reader.ReadInt32(); for (var i = 0 ; i < count; i++) { var version = new VFile(); version.Deserialize(reader); list.Add(version); data[version.name] = version; } return list; } }
此外,Versions因为底层实现依赖了VFS,所以支持任意格式的资源文件的版本管理,可以非常方便的对Wwise、Fmod等自定义格式的文件进行版本控制。
Updater.cs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 private IEnumerator RequestVersions ( ) { OnMessage("正在获取版本信息..." ); var request = UnityWebRequest.Get(GetDownloadURL(Versions.Filename)); request.downloadHandler = new DownloadHandlerFile(_savePath + Versions.Filename); yield return request.SendWebRequest(); if (!string .IsNullOrEmpty(request.error)) { var mb = MessageBox.Show("提示" , string .Format("获取服务器版本失败:{0}" , request.error), "重试" , "退出" ); yield return mb; if (mb.isOk) { StartUpdate(); } else { Quit(); MessageBox.Dispose(); } yield break ; } request.Dispose(); _versions = Versions.LoadVersions(_savePath + Versions.Filename, true ); }
既然这里提到了Updater,就拔丝抽茧把这个Update流程看一下吧,先来看下它的初始化部分
1 2 3 4 5 6 7 8 9 10 11 12 private void Start ( ) { _downloader = gameObject.AddComponent<Downloader>(); _downloader.onUpdate = OnUpdate; _downloader.onFinished = OnComplete; _savePath = Application.persistentDataPath + '/' ; Assets.updatePath = _savePath; _platform = GetPlatformForAssetBundles(Application.platform); }
当我们点击 TOUCH TO START 按钮时,会执行
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public void StartUpdate ( ) { OnStart(); if (checking != null ) { StopCoroutine(checking); } checking = Checking(); StartCoroutine(checking); }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 private IEnumerator Checking ( ) { if (!Directory.Exists(_savePath)) { Directory.CreateDirectory(_savePath); } yield return RequestVFS ( ) ; yield return RequestCopy ( ) ; yield return RequestVersions ( ) ; if (_versions.Count > 0 ) { OnMessage("正在检查版本信息..." ); PrepareDownloads(); var totalSize = _downloader.size; if (totalSize > 0 ) { var tips = string .Format("发现内容更新,总计需要下载 {0} 内容" , Downloader.GetDisplaySize(totalSize)); var mb = MessageBox.Show("提示" , tips, "下载" , "跳过" ); yield return mb; if (mb.isOk) { _downloader.StartDownload(); yield break ; } } } OnComplete(); }
总结一下,热更新流程
Download.cs和Downloader.cs
Download.cs继承DownloadHandlerScript实现了一套自己的下载处理逻辑,而Downloader.cs就是用来管理所有的Download对象的,并且做了一些附加功能,比如记录当前下载进度,用于做断点续传,不过Demo作者并没有演示,只是预留了接口,大家可以自行查看。
加载场景
好了,这个时候我们已经把所有资源更新完毕了,开始进入场景。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 private IEnumerator LoadGameScene ( ) { OnMessage("正在初始化" ); Assets.runtimeMode = true ; var init = Assets.Initialize(); yield return init ; if (string .IsNullOrEmpty(init .error)) { init .Release(); OnProgress(0 ); OnMessage("加载游戏场景" ); var scene = Assets.LoadSceneAsync(gameScene, false ); while (!scene.isDone) { OnProgress(scene.progress); yield return null ; } } else { init .Release(); var mb = MessageBox.Show("提示" , "初始化异常错误:" + init .error + "请联系技术支持" ); yield return mb; Quit(); } }
其中最主要的,是Assets.Initialize();初始化工作,以及加载场景的那两句代码
其实在xasset中,加载AB资源非常方便
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 public static SceneAssetRequest LoadSceneAsync (string path, bool additive ) { if (string .IsNullOrEmpty(path)) { Debug.LogError("invalid path" ); return null ; } path = GetExistPath(path); var asset = new SceneAssetAsyncRequest(path, additive); if (! additive) { if (_runningScene != null ) { _runningScene.Release();; _runningScene = null ; } _runningScene = asset; } asset.Load(); asset.Retain(); _scenes.Add(asset); Log(string .Format("LoadScene:{0}" , path)); return asset; } public static AssetRequest LoadAssetAsync (string path, Type type ) { return LoadAsset(path, type, true ); } public static AssetRequest LoadAsset (string path, Type type ) { return LoadAsset(path, type, false ); }
加载资源
我们接着看Demo,点击顶部下拉列表,随便选择一个资源,点击加载即可看到效果
我们来看看他代码怎么实现的
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 private IEnumerator LoadAsset ( ) { if (_assets == null || _assets.Length == 0 ) { yield break ; } var path = _assets [_optionIndex]; var ext = Path.GetExtension (path); if (ext.Equals (".png!webp" , StringComparison.OrdinalIgnoreCase)) { var request = LoadSprite (path); yield return request; if (!string .IsNullOrEmpty (request.error)) { request.Release (); yield break ; } var go = Instantiate (temp.gameObject, temp.transform.parent); go.SetActive (true ); go.name = request.asset.name; var image = go.GetComponent<Image> (); image.sprite = request.asset as Sprite; _gos.Add (go); } }
那么这个AB路径是怎么回事呢,我们断点看一下,发现都是全路径 ,所幸xasset提供了Assets.GetAllAssetPaths();来获取所有AB路径名,我们可以自己封装一个API,做一个Dictionary<string,string> AllAssetPathShortName,Key为单纯的资产名,例如Btn_Buy1_h,Value就是全路径名,这样使用起来也比较方便。
我们来看看资源加载这一块的底层源码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 private static AssetRequest LoadAsset (string path, Type type, bool async ) { if (string .IsNullOrEmpty(path)) { Debug.LogError("invalid path" ); return null ; } path = GetExistPath(path); AssetRequest request; if (_assets.TryGetValue(path, out request)) { request.Retain(); _loadingAssets.Add(request); return request; } string assetBundleName; if (GetAssetBundleName(path, out assetBundleName)) { request = async ? new BundleAssetAsyncRequest(assetBundleName) : new BundleAssetRequest(assetBundleName); } else { if (path.StartsWith("http://" , StringComparison.Ordinal) || path.StartsWith("https://" , StringComparison.Ordinal) || path.StartsWith("file://" , StringComparison.Ordinal) || path.StartsWith("ftp://" , StringComparison.Ordinal) || path.StartsWith("jar:file://" , StringComparison.Ordinal)) request = new WebAssetRequest(); else request = new AssetRequest(); } request.url = path; request.assetType = type; AddAssetRequest(request); request.Retain(); Log(string .Format("LoadAsset:{0}" , path)); return request; }
总结,对与资源加载,我们只需要提供AB全路径名以及目标类型,即可加载AB,并且通过**.asset**取得目标对象。
卸载资源
只加载资源,不卸载资源可不行,我们来看看xasset是怎么处理资源卸载这一块逻辑的。
同样看Demo的卸载资源选项
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 private IEnumerator UnloadAssets ( ) { foreach (var image in _gos) { DestroyImmediate (image); } _gos.Clear (); foreach (var request in _requests) { request.Release (); } _requests.Clear (); yield return null ; Assets.RemoveUnusedAssets (); }
来看看底层源码实现
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 public static void RemoveUnusedAssets ( ) { foreach (var item in _assets) { if (item.Value.IsUnused()) { _unusedAssets.Add(item.Value); } } foreach (var request in _unusedAssets) { _assets.Remove(request.url); } foreach (var item in _bundles) { if (item.Value.IsUnused()) { _unusedBundles.Add(item.Value); } } foreach (var request in _unusedBundles) { _bundles.Remove(request.url); } }
打包
对于打包这一块的支持,xasset做的也非常到位,自动分析依赖,冗余
xasset的打包方法是对文件夹进行Applay Rule
具体分类
Text:此文件夹下的每个文本文件都会各自打成一个AB包
Prefab:此文件夹下的每个Prefab都会各自打成一个AB包
Png:此文件夹下的每个图片都会各自打成一个AB包
Material:此文件夹下的每个Material都会各自打成一个AB包
Controller:此文件夹下的每个Controller都会各自打成一个AB包
Asset:此文件夹下的每个Asset都会各自打成一个AB包
Scene:此文件夹下的每个场景都会各自打成一个AB包
Directory:此文件夹下的每个文件夹都会各自打成一个AB包
以Demo为例,我们每个Scene一个AB,每个UI下的子文件夹一个AB
然后为了打包,我们需要填写起始Scene,注意不包含需要热更的Scene
最后我们Build Bundles
会生成在项目目录/DLC/目标平台下
上传文件服务器
上传AB文件到服务器时要注意,需要把整个DLC文件夹都上传到服务器
xasset自带了HFS工具,这是一个本地的资源服务器,我们可以用它做实验(不过俺打不开,重新下了一个)
把这个网址复制到Unity,每个人可能都不一样
特性
VFS
Virtual File System(虚拟文件系统),通过Virtual File和Virtual Disk来实现一套I/O方案,用自定义的格式对所有资源文件进行打包防止资源被ABE或AS之类的工具轻易提取,除了安全性得到提升外,它在测试的Android设备上的IO性能也有客观的提升,参考
惰性GC
之所以叫惰性GC,是因为和上一个版本相比,上一个版本是每帧都会检查和清理未使用的资源,这个版本底层只会在切换场景或者主动调用Assets.RemoveUnusedAssets();的时候才会清理未使用的资源,这样用户可以按需调整资源回收的频率,在没有内存压力的时候,不回收可以获得更好的性能。
架构流程图