赞
踩
最近遇到的一系列问题跟AssetsBundle有很大关系,然后刨根问底,揪出了许多跟Unity资源管理有关的问题。本片技术日志将讲解我于今天关于以下几点内容的思考:
(以上内容跟目录并不完全一致)
Resources 目录的出现是为了解决游戏资源动态加载的问题。比如说,和平精英在游戏大厅界面,战斗地图资源是不需要加载的,而需要等到玩家进入场景的时候再加载出来。游戏开发者将一些需要动态加载的资源放进Resources目录里面,然后通过Resources.Load()
进行资源动态加载。
Resources目录是无关乎路径的,它只要在Assets目录底下,无论是作为它的直接子目录,还是孙目录,孙孙目录,Unity都能认出它来,并把分散在各地的Resources目录在最终打包发布的时候,整合到一个文件中,其中包含了关于各个资源的序列化数据,索引目录,元数据等等。
关于Resources 目录的使用,官方有这么一段话:
2.1. Best Practices for the Resources System
Don't use it.
简而言之,这个目录连官方都不想开发者用下去了。关于Resources 目录的缺点,在网上有很多说法,我并没有实践验证过,但是现在项目里基本也没有在用Resources 目录了。
AssetBundles 可以看作是Resources 目录的替代品,尽管我不知道它为什么而出现。开发者可以将任何资源打成AssetBundles资源包(简称AB包),然后完成动态加载以及热更的功能。AssetBundles 的文件结构跟 Resources 目录整合后的文件结构很像。官方有这么一张AssetBundles的结构图:
之前,Resources 目录下的资源Untiy都会自动帮你打包好。现在 AssetBundles需要你自己主动指定 Bundles名称,同时编写代码生成AB包资源。
以安卓为例(我只有安卓手机用来测试)。以下三个重要的资源管理路径在是定义在类 Application 定义:
这三个路径Windows(Unity Editor Mode)平台和安卓平台的路径如下(我的Unity项目根目录在D:/unityProjects/TestAssetManager/):
Windows平台
Android平台
dataPath 路径单纯地指向你的游戏运行的“根目录”,在Windows平台的Untiy Editor Mode上,它指向游戏项目的Assets目录。在安卓平台上,它指向你游戏运行的apk文件。
dataPath 是只读的,一般来说,玩家是不能直接接触到这个文件的(手机存储的根目录从/storage/emulated/0/)开始。
在编写AssetBundles打包代码时,需要提供打包的输出路径,一般来说,都是指定streamingAssetsPath。Unity为这几个资源管理路径专门做了平台区分,官方文档里也建议使用这个路径来存储资源。
如果指定的是这个路径,在打包之后,Assets目录底下会生成一个StreamingAssets目录。当然,这个路径本身指向的这个目录如果不存在,需要你手动创建。然后打包的资源都会放在这里。而在Android平台上,streamingAssetsPath指向一个奇怪的目录(为什么会有一个感叹号)。官方文档里有这样一段话:
Note that on some platforms it is not possible to directly access the StreamingAssets folder because there is no file system access in the web platforms, and because it is compressed into the .apk file on Android. On those platforms, a url will be returned, which can be used using the WWW class.
意思是,你不可能直接访问到这个文件夹,在安卓平台上,它被存放到了apk压缩文件里面。你需要在代码里使用UnityWebRequest类进行里面的资源访问,访问的钥匙是一个file://
协议的URL。
在Unity上打好AB包后,再打包到安卓上,StreamAssets目录下的所有文件都会原封不动地拷贝到安卓对应的streamingAssetsPath路径下。使用Unity提供的类可以访问这些资源。(文章后面提供打包和访问的示例代码)。
这个目录也是只读的。
顾名思义,这个目录底下存着持久化数据,这些数据不随着更新而删除。一般来说,游戏的本地存档都会放在这里(看看图中Windows平台的路径,是不是想起破解单机游戏修改本地存档时搜索的路径?)(如果没修改过本地存档这种龌龊事可以不用理我)。而在安卓平台上,这个路径指向程序的沙盒目录下的files目录。
这个目录是可读可写的。
一般来说,游戏运行时需要数据和资源,应当优先访问 persistentDataPath ,如果没有查找到资源,再去streamingAssetsPath下找。这样做的目的是游戏更新时,可以将下载好的新的AssetsBundle覆盖原来的persistentDataPath底下的数据(因为streamingAssetsPath是不可写的)。
由于persistentDataPath下的文件可以被玩家直接接触,一些敏感的文件就不适合放在此处,比如某些配置文件,等等。
提这个话题是因为项目里面使用到AssetBundles和persistentDataPath跟热更有很大关系。在Unity开发中,热更需要两种技术的支持:Lua脚本开发 和 AssetBundles。
由于C#不能热更(因为C#是静态编译,而且会编译成中间语言用于跨平台,导致游戏一旦编译安装后,就几乎不能修改C#代码的逻辑。而Lua是脚本语言,是动态解释的,所以为了支持热更,需要先将一些几乎不会更新的基础功能用C#写好,然后暴露接口给上层Lua,再在Lua做主要逻辑扩展。关于C#和Lua的交互技术已经有很多了。比如ToLua,XLua。。。)
同时AssetBundles支持资源层面的动态更新。
在Unity编辑器里,你可以为Project视图下的任何资源进行打包。点击任意资源,然后再Inspector视图的下方有这么一个子视图(我选了一个xml文件):
可以看到下方有一个AssetBundle属性。两个下拉菜单分别选择Bundle名和后缀。后缀是无所谓的,你可以直接在名字里接上后缀。在此之前,你需要 new 一个新的name。比如我的就是 appbuildconfig.unity3d
。new 好的名字就会存在在那儿,之后如果有资源也使用这个名字,那么这些使用相同Bundle名的资源都会被打进同一个AB包。
为需要打包的资源指定好了Bundle名之后(这一步其实也可以用脚本做,此处,略),然后就编写一个脚本提供Unity编辑器接口开启打包。如下:
这是一个Editor脚本,需要放在一个名为“Editor”的目录下才会起作用。注意第 13 行的路径选择,你也可以不选择 Application.streamingAssetsPath
,但是在打包成apk时,Unity只会把这个路径下的文件拷贝到Android平台上对应的streamingAssetsPath。而且Unity已经为这个路径做了很好的平台区分,所以没有必要自己再搞一套。
另外,重复一遍,这个路径所指示的目录不一定存在,需要手动判断,手动创建。
写完脚本回到Unity等待Unity自动编译完,就会发现菜单上多了一个选项,点击它,开启打包。
打包好之后,在steamingAssetsPath路径下多了这么几个文件:
其实这个工程我还给一个游戏物体打了个playercapsule.unity3d
包,如果你只打包了一个xml,那么多出来的应该是上图中选中的几项。(实际上,我总共打包了六个xml文件进 appbuildconfig.unity3d 包里)。其中,appbuildconfig.unity3d 就是 AssetBundle 文件,appbuildconfig.unity3d.manifest 是清单文件。打开清单文件瞧瞧:
注意 Assets 一栏,罗列了打包进 appbuildconfig.unity3d 的所有六个文件。现在要考虑工程打包成apk,然后在手机上安装(可以安装 mumu安卓模拟器)。但是打包进安卓之后,streamingAssetsPath 路径是不可访问的,那如何获取?代码如下:
using System.Collections.Generic; using System.IO; using UnityEngine; using UnityEngine.Networking; public class CopyFromStreamingAsset : MonoBehaviour { void Start() { StartCoroutine("GetAssetFromWWW"); } IEnumerator GetAssetFromWWW() { string streamPath = Application.streamingAssetsPath; UnityWebRequest uwr = UnityWebRequestAssetBundle.GetAssetBundle(streamPath + "/appbuildconfig.unity3d"); yield return uwr.SendWebRequest(); if (uwr.isNetworkError || uwr.isHttpError) { Debug.LogError("Can't read"); } else { AssetBundle bundle = DownloadHandlerAssetBundle.GetContent(uwr); TextAsset ta = bundle.LoadAsset<TextAsset>("appbuildconfig"); // ta.text is what you want } } }
首先,提供一个资源所在的 URL 给UnityWebRequestAssetBundle,让他从路径加载一个UntiyWebRequest资源。这个URL的前缀可以直接使用streamingAssetsPath。因为在安卓平台上,它就是一个 file://
协议的URL。然后用协程异步等待资源架子啊完成。然后用 DownloadHandlerAssetBundle.GetContent 从资源中拿到 AssetBundle 包。最后用 AssetBundle.LoadAsset<T> 从AB包里拿到对应名称的资源(这些资源的名称在上面的manifest清单文件已经罗列出来了,加不加后缀都一样)。当然了,LoadAsset只能拿到Unity已经识别的资源,xml可以被当作是Untiy的文本资源 TextAsset 来获取。如果你打包的是一个预置体,那就变成了 GameObject go = AssetBundle.LoadAsset<GameObject>(objectName);
另外,一般lua脚本都是需要热更的,这种时候如果直接使用lua的源码文件的AB包覆盖到persistent路径下,会比较容易破解。按照工作室某大佬提示,一般这种情况都会先把lua源码文件转成字节码文件,然后再打成AB包,最后进行热更。
到这儿,文章基本就结束了。花了整整一天来学习,测试,验证,写博文,累死俺了。欢迎大佬批评指正。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。