• QQ
  • nahooten@sina.com
  • 常州市九洲新世界花苑15-2

游戏开发

常州手游开发-U3D AssetBundle爬坑

原创内容,转载请注明原文网址:http://homeqin.cn/a/wenzhangboke/jishutiandi/youxikaifa/2019/0227/403.html

常州手游开发-U3D AssetBundle爬坑

 

这篇文章从AssetBundle的打包,运用,管理以及内存占用各个方面停止了比拟全面的剖析,对AssetBundle运用过程中的一些坑停止填补指引以及喷!

 

AssetBundleUnity引荐的资源管理方式,手机App外包罗列了诸如热更新,紧缩,灵敏等等优点,但AssetBundle的坑是十分深的,很多躲藏细节让你运用起来需求非常慎重,一不当心就会掉入深坑,打包没规划好,20MB的资源紧缩到了30MB,或者大量的包招致打包以及加载时的各种低效,或者莫明其妙地丧失关联,或者内存爆掉,以及各种加载失败,在网上研讨了大量关于AssetBundle的文章,但每次看完之后,还是有不少疑问,所以只能经过理论来解答心中的疑问,为确保结果的精确性,下面的测试在编辑器下,WindowsIOS下都停止了测试比拟。

 

首先你为什么要选择AssetBundle,纵使他有千般益处,但普通选择AssetBundle的缘由就是,要做热更新,动态更新游戏资源,或者你Resource下的资源超越了它的极限(2GB还是4GB?),假如你没有这样的需求,那么倡议你不要运用这个坏东西,闹心~~

 

当App开发培训选择了AssetBundle之后,以及我开端喷AssetBundle之前,我们需求对AssetBundle的工作流程做一个简单的引见:

AssetBundle能够分为打包AssetBundle以及运用AssetBundle

 

打包需求在UnityEditor下编写一些简单的代码,来取出你要打包的资源,然后调用打包办法停止打包

 

 

Object obj = AssetDatabase.LoadMainAssetAtPath("Assets/Test.png");

BuildPipeline.BuildAssetBundle(obj, null,

                                  Application.streamingAssetsPath + "/Test.assetbundle",

                                 BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets

                                 | BuildAssetBundleOptions.DeterministicAssetBundle, BuildTarget.StandaloneWindows);

 

 

 

 

在运用的时分,需求用WWW来加载Bundle,然后再用加载出来的BundleLoad资源

 

WWW w = new WWW("file://" + Application.streamingAssetsPath + "/Test.assetbundle");

myTexture = w.assetBundle.Load("Test");

 

【一,打包】

 

接下来我们常州网站开发培训来看一下打包:

 

1.资源的搜集

 

    在打包前我们能够经过遍历目录的方式来自动化地停止打包,能够有选择性地将一些目录打包成一个Bundle,这块也能够用各种配置文件来管理资源,也能够用目录标准来管理

    我这边是用一个目录标准对资源停止大的分类,分为公共以及游戏内,游戏外几个大模块,然后用一套简单命名标准来指引打包,例如用OBOOneByOne)作为目录后缀来指引将目录下一切资源独立打包,默许打成一个包,用Base前缀来表示这属于公共包,同级目录下的其他目录需求依赖于它

 

    运用DirectoryGetFilesGetDirectories能够很便当地获取到目录以及目录下的文件

    Directory.GetFiles("Assets/MyDirs""*.*", SearchOption.TopDirectoryOnly);

    Directory.GetDirectories(Application.dataPath + "/Resources/Game""*.*", SearchOption.AllDirectories);

 

2.资源读取


    GetFiles搜集到的资源途径能够被加载,加载之前需求判别一下后缀能否.meta,假如是则不取出该资源,然后将途径转换至Assets开头的相对途径,然后加载资源

    string newPath = "Assets" + mypath.Replace(Application.dataPath, "");

    newPath = newPath.Replace("\\""/");

    Object obj = AssetDatabase.LoadMainAssetAtPath(newPath);

 

3.打包常州企业培训函数

 

    我们调用BuildPipeline.BuildAssetBundle来停止打包:

    BuildPipeline.BuildAssetBundle5个参数,第一个是主资源,第二个是资源数组,这两个参数必需有一个不为null,假如主资源存在于资源数组中,是没有任何关系的,假如设置了主资源,能够经过Bundle.mainAsset来直接运用它

    第三个参数是途径,普通我们设置为  Application.streamingAssetsPath + Bundle的目的途径和Bundle称号

    第四个参数有四个选项,BuildAssetBundleOptions.CollectDependencies会去查找依赖,BuildAssetBundleOptions.CompleteAssets会强迫包含整个资源,BuildAssetBundleOptions.DeterministicAssetBundle会确保生成独一ID,在打包依赖时会有用到,其他选项没什么意义

    第五个参数是平台,在安卓,IOSPC下,我们需求传入不同的平台标识,以打出不同平台适用的包,留意,Windows平台下打出来的包,不能用于IOS

 

 

 

    在打对应的包之前应该先选择对应的平台再打包

 

4.打包的决策

 

    在打包的时分,我们需求对包的大小和数量停止一个均衡,一切资源打成一个包,一个资源打一个包,都是比拟极端的做法,他们的问题也很明显,更多状况下我们需求灵敏地将他们组合起来

    打成一个包的缺陷是加载了这个包,我们不需求的东西也会被加载进来,占用额外内存,而且不利于热更新

    打成多个包的缺陷是,容易形成冗余,首先影响包的读取速度,然后包之间的内容可能会有反复,且太多的包不利于资源管理

    哪些模块打成一个包,哪些模块打成多个包,需求依据实践状况来,例如游戏中每个怪物都需求打成一个包,由于每个怪物之间是独立的,例如游戏的根底UI,能够打成一个包,由于他们在各个界面都会呈现

 

    PS.想打包进AssetBundle中的二进制文件,文件名的后缀必需为“.bytes”

 

【二,解包】

    解包的第一步是将Bundle加载进来,new一个WWW传入一个URL即可加载Bundle,我们能够传入一个Bundle的网址,从常州软件技术培训网络下载,也能够传入本地包的途径,普通我们用file://开头+Bundle途径,来指定本地的Bundle,用http://https://开头+Bundle网址来指定网络Bundle

 

string.Format("file://{0}/{1}", Application.streamingAssetsPath, bundlePath);

 

    在安卓下途径不一样,假如是安卓平台的本地Bundle,需求用jar:file://作为前缀,并且需求设置特殊的途径才干加载

 

string.Format("jar:file://{0}!/assets/{1}", Application.dataPath, bundlePath);

 

    传入指定的URL之后,我们能够用WWW来加载Bundle,加载Bundle需求耗费一些时间,所以我们普通在协同里面加载Bundle,假如加载失败,你能够在www.error中得到失败的缘由

 

 

IEnumerator LoadBundle(string url)

{

    WWW www = = new WWW(url);

    yield return www;

 

    if (www.error != null)

    {

    Debug.LogError("Load Bundle Faile " + url + " Error Is " + www.error);

    yield break;

    }

 

    //Do something ...

}

 

    除了创立一个WWW之外,还有另一个办法能够加载BundleWWW.LoadFromCacheOrDownload(url, version),运用这个函数对内存的占用会小很多,但每次重新打包都需求将该Bundle对应的版本号更新(第二个参数version),否则可能会运用常州平台运营之前的包,而不是最新的包,LoadFromCacheOrDownload会将Bundle从网络或程序资源中,解压到一个磁盘高速缓存,普通能够了解为解压到本地磁盘,假如本地磁盘曾经存在该版本的资源,就直接运用解压后的资源。关于AssetBundle一切对内存占用的状况,后面会有一小节特地引见它

 

    LoadFromCacheOrDownload会记载一切Bundle的运用状况,并在恰当的时分删除最近很少运用的资源包,它允许存在两个版本号不同但名字一样的资源包,这意味着你更新这个资源包之后,假如没有更新代码中的版本号,你可能取到的会是旧版本的资源包,从而产生其他的一些BUG。另外,当你的磁盘空间缺乏的时分(硬盘爆了),LoadFromCacheOrDownload只是一个普通的new WWW!后面关于内存引见的小节也会对这个感慨号停止引见的

 

    拿到Bundle之后,我们就需求Load里面的资源,有LoadLoadAll以及LoadAsyn可供选择

 

 

    //将一切对象加载资源

    Object[] objs = bundle.LoadAll();

 

    //加载名为obj的资源

    Object obj = bundle.Load("obj");

 

    //异步加载名为resName,类型为type的资源

    AssetBundleRequest res = bundle.LoadAsync(resName, type);

        yield return res;

    var obj = res.asset;

 

    我们经常会把各种游戏对象做成一个Prefab,那么Prefab也会是我们Bundle中常见的一种资源,运用Prefab时需求留意一点,Bundle中加载的Prefab是不能直接运用的,它需求被实例化之后,才干运用,而关于这种Prefab,实例化之后,这个Bundle就能够被释放了

 

    //需求先实例化

    GameObject obj = GameObject.Instantiate(bundle.Load("MyPrefab")) as GameObject;

 

    关于从Bundle中加载出来的Prefab,能够了解为我们直接从资源目录下拖到脚本上的一个Public变量,是未被实例化的Prefab,只是一个模板

 

    假如你用上面的代码来加载资源,当你的资源渐渐多起来的时分,你可能会发现一个很坑爹的问题,你要加载的资源加载失败了,例如你要加载一个GameObject,但是整个加载过程并没有报错,而当你要运用这个GameObject的时分,出错了,而同样的代码,我们在PC上可能没有发现这个问题,当我们打安卓或IOS包时,某个资源加载失败了。

 

    呈现这种神奇的问题,首先是疑心打包的问题,包太大了?删掉一些内容,不行!重新打一个?还是不行!然后发现来来回回,都是这一个GameObject报的错,难道是这个GameObject里面局部资源有问题?对这个GameObject各种剖析,把它大卸八块,处置成一个很简单的GameObject,还是不行!难道是名字的问题?把这个GameObject的名字改了一下,能够了!

 

    原本事情到这就该完毕了,但是,这也太莫明其妙了吧!而且,最重要的是,哥就喜欢原来的名字!!把这个资源改成新的名字,怎样看怎样变扭,怎样看都没有原来的名字美观,所以继续折腾了起来~

 

    首先单步跟踪到这个资源的Load,资源被胜利Load出来了,但是Load出来的东西有点怪怪的,明显不是一个GameObject,而是一个莫明其妙的东西,可能是Unity生成的一个中间对象,或许是一个索引对象,反正不是我要的东西,打包的GameObject怎样会变成这个玩意呢?于是在加载Bundle的中央,把Bundle LoadAll了一下,然后查看这个Bundle里面的内容

 

 

 

 

    在这里我们能够看到,有一个叫RoomHallViewRoomMainViewGameObject,并且,LoadAll之后的资源比我打包的资源要多很多,看样子一切关联到的资源都被自动打包进去了,数组的427RoomHallViewGameObject,而431才是RoomMainViewGameObject。能够看到名字叫做RoomMainViewRoomHallView的对象有好几个,GameObjectTransform,以及一个只要名字的对象,它的类型是一个ReferenceData

 

    认真查看能够发现,RoomHallViewGameObject是排在数组中一切名为RoomHallView对象的最前面,而RoomMainView则是ReferenceData排在前面,当我们Load或者LoadAsyn时,是一次数组的遍历,当遍历到名字匹配的对象时,则将对象返回,LoadAsyn会对类型停止匹配,但由于我们传入的是Object,而简直一切的对象都是Object,所以返回的结果就是第一个名字匹配的对象

 

    Load以及LoadAsyn时,除了名字,把要加载对象的类型也传入,再调试,原来的名字也能够正常被读取到了,这个细节十分的坑,由于在官网并没有提示,而且示例的sample也没有说应该留意这个中央,并且呈现问题的几率很小。所以一旦呈现,就坑死了

 

bundle.Load("MyPrefab"typeof(GameObject))

 

    另外,不要在IOS模仿器上测试AssetBundle,你会收到bad url的错误

 

【三,依赖】

 

    依赖和打包息息相关,之所以把依赖单独分开来讲,是由于这玩意太坑了.......

 

1.打包依赖】

 

    在我们打包的时分,将两个资源打包成单独的包,那么两个资源所共用的资源,就会被打包成两份,这就形成了冗余,所以我们需求将公共资源抽出来,打成一个Bundle,然后后面两个资源,依赖这个公共包,那么还有另外一种办法,就是把它们三打成一个包,但这不利于后期维护    

 

 

 

 

    我们运用BuildPipeline.PushAssetDependencies()BuildPipeline.PopAssetDependencies()来开启Bundle之间的依赖关系,当我们调用PushAssetDependencies之后,会开启依赖形式,当我们依次打包 A B C时,假如A包含了B的资源,B就不会再包含这个资源,而是直接依赖A的,假如AB包含了C的资源,那么C的这个资源旧不会被打包进去,而是依赖AB。这时分只需有同样的资源,就会向前依赖,当我们希望,BC依赖A,但BC之间不相互依赖,就需求嵌套Push Pop了,当我们调用PopAssetDependencies就会完毕依赖

 

 

  string path = Application.streamingAssetsPath;

  BuildPipeline.PushAssetDependencies();

 

  BuildTarget target = BuildTarget.StandaloneWindows;

 

  BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/UI_tck_icon_houtui.png"), null,

                                 path + "/package1.assetbundle",

                                 BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets

                                 | BuildAssetBundleOptions.DeterministicAssetBundle, target);

 

 

  BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/New Material.mat"), null,

                                 path + "/package2.assetbundle",

                                 BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets

                                 | BuildAssetBundleOptions.DeterministicAssetBundle, target);

 

 

  BuildPipeline.PushAssetDependencies();

  BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/Cube.prefab"), null,

                                 path + "/package3.assetbundle",

                                 BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets

                                 | BuildAssetBundleOptions.DeterministicAssetBundle, BuildTarget.StandaloneWindows);

  BuildPipeline.PopAssetDependencies();

 

 

 

  BuildPipeline.PushAssetDependencies();

  BuildPipeline.BuildAssetBundle(AssetDatabase.LoadMainAssetAtPath("Assets/Cubes.prefab"), null,

                                 path + "/package4.assetbundle",

                                 BuildAssetBundleOptions.CollectDependencies | BuildAssetBundleOptions.CompleteAssets

                                 | BuildAssetBundleOptions.DeterministicAssetBundle, target);

  BuildPipeline.PopAssetDependencies();

 

  BuildPipeline.PopAssetDependencies();

 

 

    上面的代码演示了如何运用依赖,这个测试运用了一个纹理,一个材质,一个正方体Prefab,还有两个正方体组成的Prefab,材质运用了纹理,而两组正方体都运用了这个材质,上面的代码用Push开启了依赖,打包纹理,然后打包材质(材质自动依赖了纹理),然后嵌套了一个Push,打包正方体(正方体依赖前面的材质和纹理),然后Pop,接下来再嵌套了一个Push,打包那组正方体(不依赖前面的正方体,依赖材质和纹理)

 

    假如我们只开启最外面的Push Pop,而不嵌套Push Pop,那么两个正方体组成的Prefab就会依赖单个正方体的Prefab,依赖是一把双刃剑,它能够去除冗余,但有时分我们又需求那么一点点冗余

 

2.依赖丧失】

 

    当我们的Bundle之间有了依赖之后,就不能像前面那样简单地直接Load对应的Bundle了,我们需求Bundle所依赖的Bundle先加载进来,这个加载只是WWW或者LoadFromCacheOrDownload,并不需求对这个Bundle停止Load,假如BundleB依赖BundleA,当我们要加载BundleB的资源时,假定BundleA没有被加载进来,或者曾经被Unload了,那么BundleB依赖BundleA的局部就会丧失,例如每个正方体上都挂着一个脚本,当我们不嵌套Push Pop时,单个正方体的Bundle没有被加载或者曾经被卸载,我们加载的那组正方体上的脚本就会丧失,脚本也是一种资源,当一个脚本曾经被打包了,依赖这个包的资源,就不会被再打进去

 

CubesCube都挂载同一个脚本,TestObjeCubes依赖Cube,将Cube所在的Bundle Unload,再Load CubesBundleCubes的脚本丧失,脚本,纹理,材质等一切资源,都是如此

 

 

3.更新依赖】

 

    在打包的时分我们需求指定BuildAssetBundleOptions.DeterministicAssetBundle选项,这个选项会为每个资源生成一个独一的ID,当这个资源被重新打包的时分,肯定这个ID不会改动,包的依赖是依据这个ID来的,运用这个选项的益处是,当资源需求更新时,依赖于该资源的其他资源,不需求重新打包

 

    A -> B -> C

 

    A依赖B依赖C时,B更新,需求重新打包CB,而A不需求动,打包C的缘由是,由于B依赖于C,假如不打包C,直接打包B,那么C的资源就会被反复打包,而且BC的依赖关系也会断掉

 

【四,内存】

 

    在运用WWW加载Bundle时,会开拓一块内存,这块内存是Bundle文件解压之后的内存,这意味着这块内存很大,经过Bundle.Unload能够释放掉这块内存,Unload trueUnload false 都会释放掉这块内存,而这个Bundle也不能再用,假如要再用,需求重新加载Bundle需求留意的是,依赖这个Bundle的其他Bundle,在Load的时分,会报错

 

    得到Bundle之后,我们用Bundle.Load来加载资源,这些资源会从Bundle的内存被复制出来,作为Asset放到内存中,这意味着,这块内存,也很大,Asset内存的释放,与Unity其他资源的释放机制一样,能够经过Resources.UnloadUnuseAsset来释放没有援用的资源,也能够经过Bundle.Unload(true)来强迫释放Asset,这会招致一切援用到这个资源的对象丧失该资源

 

 

 

 

    上面两段话能够得出一个结论,在new WWW(url)的时分,会开拓一块内存存储解压后的Bundle,而在资源被Load出来之后,又会开拓一块内存来存储Asset资源,WWW.LoadFromCacheOrDownload(url)的功用和new WWW(url)一样,但LoadFromCacheOrDownload是将Bundle解压到磁盘空间而不是内存中,所以LoadFromCacheOrDownload返回的WWW对象,自身并不会占用过多的内存(只是一些索引信息,每个资源对应的磁盘途径,在Load时从磁盘取出),针对手机上内存较小的状况,运用WWW.LoadFromCacheOrDownload替代new WWW能够有效地俭省内存。但LoadFromCacheOrDownload大法也有不灵验的时分,当它不灵验时,LoadFromCacheOrDownload返回的WWW对象将占用和new WWW一样的内存,所以不论你的Bundle是如何创立出来的,都需求在不运用的时分,及时地Unload

 

    另外运用LoadFromCacheOrDownload需求留意的问题是——第二个参数,版本号,Bundle重新打包之后,版本号没有更新,取出的会是旧版本的Bundle,并且一个Bundle缓存中可能会存在多个旧版本的Bundle,例如1,2,3 三个版本的Bundle

 

 

 

    Bundle Load完之后,不需求再运用该Bundle了,停止Unload,假如有其他Bundle依赖于该Bundle,则应该等依赖于该BundleBundle不需求再Load之后,Unload这个Bundle,普通呈现在大场景切换的时分。

 

    我们晓得在打包Bundle的时分,有一个参数是mainAsset,假如传入该参数,那么资源会被视为主资源打包,在得到Bundle之后,能够用AssetBundle.mainAsset直接运用,那么能否在WWW获取Bundle的时分,就曾经将mainAsset预先Load出来了呢?不是!在我们调用AssetBundle.mainAsset取出mainAsset时,它的get办法会阻塞地去Load mainAsset,然后返回,AssetBundle.mainAsset同等于Load("mainAssetName")  

 

    PS.反复Load同一个资源并不会开拓新的内存来存储这个资源

 

【五,其他】

 

    在运用AssetBundle的开发过程中,我们经常会对资源停止调整,调整之后需求对资源停止打包才干生效,对开发效率有很大的影响,所以在开发中我们运用ResourceBundle兼容的方式

 

    首先将资源管理封装到一个Manager中,从BundleLoad资源还是从Resource里面Load资源,都由它决议,这样能够保证上层逻辑代码不需求关怀当前的资源管理类型

 

    当然,我们一切要打包的对象,都在Resource目录下,并且运用严厉的目录标准,然后运用脚本对象,来记载每个资源所在的Bundle,以及所对应的Resource目录,在资源发作变化的时分,更新脚本对象,Manager在运转时运用脚本对象的配置信息,这里的脚本对象我们是运用代码自动生成的,当然,你也能够用配置表,效果也是一样的

 

    版本管理也能够交由脚本对象来完成,每次打包的资源,需求将其版本号+1,脚本对象可存储一切资源的版本号,版本号能够用于LoadFromCacheOrDownload时传入,也能够手动写入配置表,在我设计的脚本对象中,每个资源都会有一个所属BundleResource下相对途径,版本号等三个属性

 

    在版本发布的时分,你需求先打包一次Bundle,并且将Resource目录改成其他的名字,然后再打包,确保Resource目录下的资源没有被反复打包,而假如你想打的是Resource版本,则需求将StreamingAssets下的Bundle文件删除

 

    脚本对象的运用如下:

    1.先设计好存储构造

    2.写一个继承于ScriptObject的类,用可序列化的容器存储数据构造(List或数组),Dictionary等容器无法序列化,public之后在

 

 

[Serializable]

public class ResConfigData

{

    public string ResName; //资源名字

    public string BundleName; //包名字

    public string Path; //资源途径

    public int Vesrion; //版本号

}

 

[System.Serializable]

public class ResConfig : ScriptableObject

{

    public List ConfigDatas = new List();

}

 

 

    4.在指定的途径读取对象,读取不到则创立对象

 

 

ResConfig obj = (ResConfig)AssetDatabase.LoadAssetAtPath(path, typeof(ResConfig));

if (obj == null)

{

   obj = ScriptableObject.CreateInstance();

   AssetDatabase.CreateAsset(obj, path);

}

 

 

    3.写入数据,直接修正obj的数组,并保管(不保管下次启动Unity数据会丧失)

 

EditorUtility.SetDirty(obj);

 

    由于数组操作不便当,所以我们能够将数据转化为便当各种增删操作的Dictionary容器存储,在坚持时将其写入到耐久化的容器中

 

上篇:上一篇:常州手游开发-U3D 关于资源加载(Resources和Asset
下篇:下一篇:常州手游开发培训-Unity3d资源管理分析