在开始使用之前, 建议先导入到一个空的工程里, 通过ReadMe的一步步引导使你对整个框架以及文件结构进行熟悉, 之后再考虑导入到现有工程中使用, 完整看完教程大概需要2个小时左右. 先看看文件夹结构:
它在AssetBundleMaster文件夹下有几个子文件夹:
AssetBundleMasterExample 是基本的API使用Demo, 包括资源加载, 场景加载, 以及相关的卸载操作.
AssetBundleMasterExample2 是针对Built-In Shader 的多次编译以及其优化的测试场景.
Editor 是编辑器脚本文件夹. 包含打包面板等.
Plugins 是运行时脚本文件夹. 包含所有核心代码. 因为资源加载是最基础的逻辑层, 所以放在Plugins里面.
让我们先在编辑器下运行基础API场景, 工具栏上打开AssetBundleMaster的编辑器面板:
外观如图:
我们在编辑器运行, 只需要设定两个变量即可, 对开发者比较友好, 其它设定在打包的时候才需要了解, 我们在后面进行说明, 现在先忽略:
1. Editor Asset LoadMode : 编辑器下的资源读取方式. 先把它设为AssetDataBase_Editor, 直接读取资源.
2. Build Root : 它是资源读取的根文件夹, 按照图中设置即可进行Demo场景读取, 点击[Change Build Root...]按钮, 选择AssetBundleMasterExample文件夹即可. 接下来打开初始场景 StartScene1, 它在AssetBundleMasterExample/Scenes 下面:
运行后左下角有场景选择 :
这些就是所有API的Demo场景了, 选择任意一个场景可以看到测试代码与场景同名. 找到场景下同名的GameObject, 脚本就在上面, 打开查看即可.
我们先说明一下场景加载的过程, 在我们从下拉列表中选择某个场景之后, 它会把之前加载的所有东西都卸载掉, 所以读取新场景之后每个场景中加载的资源都是全新的, 方便测试. 建议一直打开Profiler全程查看. 卸载资源会有一个等待过程, 这在你切换场景之后可以在Hierarchy的 [AssetLoadManager] 这个对象的检视面板中查看到, 比如我们切换一下场景 :
然后就可以在[AssetLoadManager]组件面板上看到一个倒计时Slider, 它表示卸载资源的等待时间 :
因此如果你使用Profiler 的 Memory > Detailed > Take Sample 查看资源是否被正确卸载, 在它执行之后查看.
下面来说明各个场景以及API的使用, 我们刚才在编辑器面板设置了Build Root 这个路径, 所有资源的读取路径都是跟它的相对路径, 这些资源被称为主要资源, 是通过代码加载的资源, 比如场景等, 而放在这个文件夹之外的资源为引用资源, 比如场景中引用到的Mesh等不会直接通过代码加载的资源, 不需要将所有资源都放到Build Root文件夹下, 这里只是放在一起方便用户删除Demo资源. 之后不另外说明.
PS : 在使用AssetDataBase_Editor 模式时, 是没有异步加载的, 所有异步加载都会变为同步加载, 因为使用的加载方式为AssetDataBase.LoadAssetAtPath的编辑器加载方式. 在资源的卸载方式上也不同, 它是靠 Resources.UnloadUnusedAssets(); 的方式卸载资源的, 而AssetBundle模式的话是通过 AssetBundle.Unload(); 和 Resources.UnloadUnusedAssets(); 的组合方式进行卸载的, 效率更高.
会被加载到的资源文件就在AssetBundleMasterExample文件夹下, 请自行查看.
这样编辑器下的运行设置就完成了, 对于开发人员来说并没有任何学习成本, 只需要修改Build Root为你们的工程的资源文件夹即可, 然后你可以看到编辑器下多出来几个文件夹和文件, 这些就是AssetBundleMaster的配置信息了, 只要上传到SVN等就可以同步设置给其他人了.
下面是Demo场景以及API的说明, 最终发布AssetBundle包的过程在文章末尾.
零. StartScene1 / StartScene2 (脚本 : StartScene.cs)
Demo 中有两个初始场景 StartScene1 和 StartScene2, 一般情况下你不需要对 AssetBundleMaster 进行初始化操作, 只有在加载远程 AssetBundle 的模式下需要等待初始化完成之后再进行资源读取, 因为需要预先下载一些基础文件, 我们可以先看看 StartScene.cs 脚本中的 Start() 函数 :
AssetLoadManager 在命名空间 AssetBundleMaster.AssetLoad 之下.
把它当做逻辑的入口, 游戏逻辑在 AssetLoadManager.Instance.OnAssetLoadModuleInited() 的回调后进行.
一. Example_LoadAsset (脚本 Example_LoadAsset.cs)
此Demo主要演示怎样读取一般资源, 读取控制由 ResourceLoadManager 控制.
读取API (命名空间 AssetBundleMaster.ResourceLoad):
ResourceLoadManager.Instance.Load<T>(string loadPath)
ResourceLoadManager.Instance.LoadAsync<T>(string loadPath, System.Action<T> loaded)
ResourceLoadManager.Instance.LoadAll<T>(string loadPath)
它主要读取了Sprites 和 Textures 文件夹下的文件, 使用方法很容易理解, "Sprites/Pic1" 这个路径下的资源比较特殊, 特别说明一下:
它有三个相同名称的文件 Pic1.jpg / Pic1.png / Pic1.txt, 这是非常特殊的情况, 因为我们当前读取资源的路径是不带后缀名的( 与Resources.Load等API统一 ), 所以这三个资源的读取路径是一样的, 而我们的泛型读取API可以通过类型限定来进行读取, 比如 ResourceLoadManager.Instance.Load<TextAsset>("Sprites/Pic1").text; 它就读取了这个路径下的第一个TextAsset类型的资源, 而ResourceLoadManager.Instance.LoadAll<Sprite>("Sprites/Pic1"); 则是返回了所有该路径下的Sprite资源数组, 如果你把"Sprites/Pic1" 改为 "Sprites", 它是一个文件夹 , 也是一样能读取出文件夹下的资源的, 逻辑上与UnityEngine.Resources 提供的API相似. 这样就解决了Android这种在StreamingAssets下无法获取某个文件夹下面的文件, 进而无法实现LoadAll方法的问题.
想当然的, 通过不带后缀名的路径读取资源的话, 如果有同名资源, 想要读出指定类型的资源肯定要进行一次厉遍读取, 这样就增加了无谓的资源读取开销. 而如果带后缀名的话, 在操作系统层面就保证了资源的唯一性(不能有重名文件), 因此我们强化了读取API, 添加了带后缀名的读取路径的支持, 这样在像上面的TextAsset读取 :
ResourceLoadManager.Instance.Load<TextAsset>("Sprites/Pic1").text;
就可以写成 :
ResourceLoadManager.Instance.Load<TextAsset>("Sprites/Pic1.txt").text;
这样就没有额外的资源厉遍开销了, 在Demo中所有的资源读取都不带后缀名是为了符合用户习惯与 Resources.Load 等API统一 , 在实际使用中如果文件夹有同名资源并且只读取单个资源, 请使用带后缀名的路径, 以减少开销 ( 良好的命名以及资源分类才是最好的减少运行开销的方法 ).
特别说明的是, AssetBundle模式下, 在异步读资源时, 用户又同时请求了同步读取的话, 异步回调和同步回调都能正确触发, 只是会在编辑器Console面板中出现错误信息, 不过不影响程序的正确性, 请看下图:
代码在Demo中没有提供, 仅供参考.
这是因为Unity没有提供停止异步读取AssetBundle的API, 并且在异步读取中进行同步读取就会报错, AssetBundleMaster 的Low-Level API对此进行了处理, 保证了回调的正确性, 用户不需要对此担心, 你可以在发布AssetBundle之后进行测试.
其余API在ResourceLoadManager.cs 中可查, 看下图, 泛型和非泛型版本都有, 方便使用Lua脚本语言的用户 :
二. Example_LoadPrefab (脚本 Example_LoadPrefab.cs)
此Demo演示怎样读取和实例化GameObject对象, 使用的是对象池. 由 PrefabLoadManager 进行控制. 实例化对象有同步和异步读取模式, 在异步读取逻辑中可以有多种方法, 会对性能有些微影响, 请看下图 :
读取API (命名空间 AssetBundleMaster.ResourceLoad):
PrefabLoadManager.Instance.Spawn(string loadPath, string poolName = null, bool active = true) // Spawn是直接实例化GameObject.
PrefabLoadManager.Instance.SpawnAsync(string loadPath, System.Action<GameObject> loaded = null, string poolName = null, bool active = true) // SpawnAsync是异步读取资源后实例化GameObject.
PrefabLoadManager.Instance.LoadAssetToPoolAsync(string loadPath, System.Action<GameObject, GameObjectPool> loaded = null, string poolName = null) // LoadAssetToPoolAsync是将资源读取到对应的对象池.
上图中第二种异步读取方法只有在资源还未读取时, 相对第一种异步方法有微小性能优势, 不过使用起来比较麻烦, 推荐直接使用 PrefabLoadManager.Instance.SpawnAsync 减少代码复杂度. 其它接口请查看PrefabLoadManager.cs, 我们强烈建议所有需要实例化的GameObject通过PrefabLoadManager 这个类进行加载以及实例化(角色, UI, 特效, 武器等等, 所有Prefab皆可), 这样用户不仅获得了对象池的控制, 也获得了资源的自动控制. 资源的自动控制逻辑请参看附录.
你也可以调用 LoadAssetToPoolAsync 方法来进行预加载如下 :
PrefabLoadManager.Instance.LoadAssetToPoolAsync("Prefabs/Cube", null, "Characters");
这个方法把Cube加载到"Character"对象池中, 我们知道在加载资源时会产生I/O, 解压缩, 反序列化, Shader Parse等行为, 在系统空闲时进行预加载可以提高游戏体验.
注意这些API的读取路径都可以添加文件后缀名.
三. Example_LoadScene (脚本 Example_LoadScene.cs)
此Demo演示怎样读取场景以及读取场景的一些特点, 注意所有被加载的场景, 都不需要添加到Build Settings里面, 在开发时可以减少这方面的工作, 在发布时如没有特殊需求不要添加任何场景, 请注意:
当前编辑器运行不需要添加任何场景, 跟资源一样直接使用相对路径加载.
读取场景使用的API (命名空间 AssetBundleMaster.ResourceLoad):
SceneLoadManager.Instance.LoadScene(string sceneLoadPath,
AssetBundleMaster.AssetLoad.LoadThreadMode loadMode = AssetBundleMaster.AssetLoad.LoadThreadMode.Asynchronous,
UnityEngine.SceneManagement.LoadSceneMode loadSceneMode = LoadSceneMode.Single,
System.Action<int, Scene> loaded = null )
它与资源一样通过相对路径来进行读取, 可以选择同步异步以及加载模式, Unity在读取场景时无论是同步或是异步, 都需要等待场景读取完成的回调 UnityEngine.SceneManagement.SceneManager.sceneLoaded 的触发, 所以无论同步异步, 都建议用户通过 loaded 回调进行下一步操作. 此函数的返回是一个哈希码, 它代表的就是此场景的ID, 用户可以通过这个ID对场景进行控制, 详见Demo七 Example_UnloadScene. 注意这些API的读取路径都可以添加文件后缀名.
四. Example_SpawnDespawn (脚本 Example_SpawnDespawn.cs)
此Demo演示受对象池控制的GameObject的实例化和归还到对象池的操作.
主要API (命名空间 AssetBundleMaster.ResourceLoad):
PrefabLoadManager.Instance.Spawn(string loadPath, string poolName = null, bool active = true);
PrefabLoadManager.Instance.Despawn(GameObject go, string poolName = null);
我们自动加载并实例化Prefab的时候可以设定对象池的名称poolName(如果不填将会使用一个默认名称), 并且指定对象的active, 在我们归还到对象池的时候, 也需要归还到对应名称的池中去, 遵循从哪个池中来, 回到哪个池中去的原则. 归还到池中的GameObject, active会被自动设置为false, 并且parent节点会被修改为相应的池, 如果用户有特殊的需求, 可以自行修改相关代码 : AssetBundleMaster.ObjectPool.GameObjectPool.Despawn 函数.
PS : 当poolName不正确的时候, 它会自动查询所有的pool, 直到找到正确的pool然后归还对象, 如果对象池数量非常多的时候, 用户要注意poolName的正确性, 以免自动查询造成性能问题.
五. Example_SpawnPoolAndUnload (脚本 Example_SpawnPoolAndUnload.cs)
此Demo演示了怎样销毁对象池内的资源, 以及自动资源控制是怎样工作的.
主要API (命名空间 AssetBundleMaster.ResourceLoad):
PrefabLoadManager.Instance.DestroyTargetInPool(string loadPath, string poolName = null, bool tryUnloadAsset = true);
此API的功能是清除目标对象池中的目标资源, 资源是通过读取路径进行标记的, 如果tryUnloadAsset 那么PrefabLoadManager 会对引用资源进行检查, 如果没有被使用的话, 会进行资源清除.
操作: 点击Spawn Cubes按钮之后, 场景中实例化了两个对象池, 并且每个对象池创建了一些Cube, 看Hierarchy面板中显示的, 有些Cube的主节点被故意修改了, 这里是告诉用户对象池与实例化对象的关联性与节点无关, 即使对象不在对象池节点下, 仍然是被对象池控制的 :
点击DestroyCubesInPool:MyCubes[1] 按钮, 此时将MyCubes[1]对象池中的Cubes删除了, 可以看到对象池还在, 可是实例化的对象被删除了.
因为Cube的资源仍然被MyCubes[2]对象池引用, 此时不触发任何删除资源的操作, 再点击DestroyCubesInPool:MyCubes[2]按钮, 此时所有实例化对象都被销毁了, 查看AssetLoadManager 的检视面板, 可以看到资源删除倒计时:
所以PrefabLoadManager 对GameObject的资源能够实现自动清除控制. 在团队开发的时候每个人都可以命名自己的对象池, 并只需要对自己的对象池进行控制即可, 资源是否需要卸载的控制交给自动控制逻辑决定.
PS : 当前仍然使用的是AssetDataBase_Editor模式, 资源卸载仍然使用的是 Resources.UnloadUnusedAssets(); 如果在AssetBundle模式下, 由于GameObject以及场景的资源是完全自动控制模式, 所以卸载资源使用的是 AssetBundle.Unload(true); 是更精准和快速的资源卸载方式( 也有其它情况, 请参看附录 ) ,
六. Example_UnloadAsset (脚本 Example_UnloadAsset.cs)
此Demo演示一般资源的卸载过程. 加载过程与 Example_LoadAsset 没有太大差别, 在这里不再进行描述, 下图为卸载资源的API:
主要API (命名空间 AssetBundleMaster.ResourceLoad):
ResourceLoadManager.Instance.UnloadAsset(UnityEngine.Object target); // 不推荐使用
ResourceLoadManager.Instance.UnloadAsset(string loadPath, System.Type systemTypeInstance, bool inherit = false); // 推荐使用
ResourceLoadManager.Instance.UnloadAsset<T>(string loadPath, bool inherit = false); // 推荐使用
我们有几种卸载资源的方式, 你可以使用 ResourceLoadManager.Instance.UnloadAsset(UnityEngine.Object target), 它就像 UnityEngine.Resources.UnloadAsset 的卸载方式, 比较容易理解. 或者使用 路径+类型 的方式, 这是因为同一个资源可能通过不同的类型加载出来, 比如说一张贴图可以加载为 Texture2D 或是 Object, 所以卸载逻辑也做了细分. 如上图所示, 卸载类型也是可以选择是否继承的, ResourceLoadManager.Instance.UnloadAsset<Object>("Sprites/Pic1", true); 的意思是卸载所有继承于 Object 并从 "Sprites/Pic1" 加载来的资源, 然后 ResourceLoadManager.Instance.UnloadAsset<Sprite>("Sprites/Pic2", false); 的意思是只卸载从 "Sprites/Pic2" 路径读取来的 Sprite 类型资源. 当然即使通过不同类型进行加载, 在内存上它们基本仍然是同一份, 并不会造成错误卸载的情况. 在这个框架下更推荐使用后两种 读取路径+类型 的方式, 因为你的资源对象并不一定是加载出来的资源( 比如你要使用的是Prefab上引用的另一张图片, 你使用UnloadAsset(xxx)传入了图片对象, 它在资源加载中并不存在 ), 而路径能很明显指代你加载的资源对象.
此API的主要逻辑就是通过资源的读取路径以及读取类型来唯一确定需要卸载的资源对象, 然后在底层的逻辑中减少对该资源的引用计数, 如果资源的引用计数为零的话, 就会触发资源卸载的过程, 在上图中我们将引用到图片的对象都置空了, 这样在后面的资源卸载中就能正确被卸载了. 而如果用户并没有将引用资源的对象置空, 卸载过程也会正常运行, 只不过运行之后也不会将资源卸载, 因为通过ResourceLoadManager加载的资源, 卸载是基于 Resources.UnloadUnusedAssets();调用的, 有强引用的资源将不会被卸载. 而在中间层ResourceLoadManager 中, 该资源是被弱引用对象引用的, 也就是说在这种情况下弱引用对象也是能一直存在的, 那么在下一次用户请求加载这些没有被释放的资源的时候, 返回的就是这个资源, 而不会触发资源加载的逻辑, 这就是保证资源在内存中不会重复的核心逻辑. 因此开发团队的成员都不需要对资源的控制进行额外的工作了, 只要在需要时加载, 不用时卸载即可, 因为资源不会因为错误的请求而在内存中重复, 所有人对资源的交叉控制都是非耦合的 ( 仍然有例外的情况, 请参看附录 ).
PS: 注意资源卸载过程在AssetDataBase_Editor和AssetBundle模式下是不一样的, AssetDataBase_Editor是通过 Resources.UnloadUnusedAssets(); 实现的, 而AssetBundle模式下是通过自动释放逻辑决定使用 AssetBundle.Unload(true); 或是 AssetBundle.Unload(false); 与 Resources.UnloadUnusedAssets(); 的组合来释放资源的, 在AssetBundle模式下更高效且是免维护的.
每当你通过ResourceLoadManager释放一个资源, 会给AssetLoadManager的计数器增加一个计数, 如果大于 AssetLoadManager.Instance.unloadAssetCountGreater 的话就会触发Resources.UnloadUnusedAssets();操作并重置计数, 你可以修改这个数值以符合你的工程要求. 用户自行可修改的变量也只有两个, 很简单 :
1. AssetLoadManager.Instance.unloadAssetCountGreater : 请求卸载至少多少个资源后触发卸载逻辑, 如果你不希望频繁进行资源卸载, 增大这个数值.
2. AssetBundleMaster.AssetLoad.AssetLoadManager.Instance.unloadPaddingTime : 触发卸载逻辑到执行卸载的等待时间, 因为异步加载通过回调返回资源, 一般回调函数是委托, 它们对返回的资源进行了内部引用, 理论上必须等待GC将它们回收之后才能执行资源卸载, 可是由于CG触发的不确定性, 以及为了减少资源的重复加载, 我们给卸载过程添加的等待时间就是这个变量, 用户可以自行调整. 实际项目运行时因为经常会触发卸载逻辑, 你不需要过于关注它.
七. Example_UnloadScene (脚本 Example_UnloadScene.cs)
此Demo演示卸载场景的一些特性, 因为场景有同步和异步加载过程, 并且场景加载完成都是通过SceneManager.sceneLoaded 回调触发的, 所以场景的卸载过程有些复杂( 过程复杂, 可是用户调用很简单 ):
主要API (命名空间 AssetBundleMaster.ResourceLoad, AssetBundleMaster.AssetLoad):
AssetLoadManager.Instance.UnloadLevelAssets(int hashCode); // 尽量不要使用它
这是Low-Level API 提供的清除场景资源的API, 在AssetBundle模式下只能清除一些AssetBundle的序列化数据, 不建议使用, 在其它模式下没有什么意义, 只有在序列化数据占据大量内存导致内存问题的情况下再使用, 然而这会增加资源在内存中被重复加载的风险. 一般情况下请不要使用.
SceneLoadManager.Instance.UnloadScene(int id, System.Action<Scene> unloaded = null);
场景读取API ( SceneLoadManager.Instance.LoadScene ) 返回的是一个HashCode, 它指代的就是被加载出来的场景, 所以卸载场景的API参数也是HashCode, 用来卸载指定场景. 场景资源也是完全自动控制的, 卸载场景都应该使用这个API. 因为读取场景有很多异步过程, 所以卸载流程如下图( 如果非AssetBundle模式则没有AssetBundle加载步骤 ):
场景读取可能的异步过程有
1. 异步读取AssetBundle
2. 异步读取场景
3. 等待异步回调, 不管同步加载还是异步加载场景, 都需要等待这个回调
所以UnloadScene函数内对其中的各个步骤都进行了封装, 即使在各个异步过程还在执行中进行场景卸载, 也能保证场景能够正确被卸载.
这个测试功能就是对卸载的测试, 它在开始异步加载场景后经过Wait Frame 10帧之后执行场景卸载, 把这个修改为1, 2, 3...等数值可以看到在异步过程中卸载场景也是正确的.
以上就是全部的资源加载, 通过三个控制器对 Asset, GameObject, Scene 进行读取和卸载逻辑(命名空间 AssetBundleMaster.ResourceLoad):
1. ResourceLoadManager
2. PrefabLoadManager
3. SceneLoadManager
我们如果运行在AssetDataBase_Editor模式下, 有些功能可能无法测试, 比如资源异步加载和资源重复的测试, 接下来就进行资源打包创建AssetBundle, 并在编辑器下加载AssetBundle测试, 之后再进行发布测试.
PS : StartScene1 左下角的下拉列表里还有一个场景 Example_UnloadAssetEfficiency 是用于说明资源卸载的相关特点的, 在附录中进行说明.
--------------------------------------- Build AssetBundles -----------------------------------------
还是先打开编辑器界面
我们的读取模式有6种, 而发布后的支持的模式有4种
编辑器下可选模式
发布时可选模式
1. Resoueces 模式 : 这个是使用Resources文件夹下的资源的模式, 没有太多意义, 只是作为保留模式
2. AssetBundle_StreamingAssets 模式 : 从StreamingAssets路径下读取AssetBundle.
3. AssetBundle_PersistentDataPath 模式 : 作为可更新资源模式, 在读取资源时会先检查PersistentDataPath下是否有资源, 没有的话会到StreamingAssets路径下读取资源. 查看附录的资源更新支持.
4. AssetBundle_Remote 模式 : 远程AssetBundle加载模式, 可以从服务器等加载AssetBundle资源.
5. AssetBundle_EditorTest 模式: 打包的AssetBundle会被放到临时路径下, 这个就是从临时路径读取AssetBundle.
6. AssetDataBase_Editor 模式 : 不需要打包直接进行编辑器下资源加载, 这个是开发者模式.
如果你使用 AssetBundle_Remote 模式, 将会出现下列选项 :
Remote URL 就是远程服务器地址, 跟你本地加载时选择的Root Folder相似. Download FailedTimes 是下载AssetBundle可以失败重试的次数, 将会把AssetBundle下载到本地 Cache文件夹, 根据不同Unity版本使用 API 为 UnityWebRequestAssetBundle / UnityWebRequest / LoadFromCacheOrDownload. 如果你使用 http 服务器进行加载测试, 注意 CORS 和 MIME 的问题.
接下来我们把面板上的所有设置看一遍:
1. Editor Asset LoadMode (EnumPop) : 编辑器下加载资源方式
2. Runtime Asset LoadMode (EnumPop) : 发布后的资源加载方式
3. Platform Selection (EnumPop) : AssetBundle的目标平台, 点击 [Set Current Platform] 即可设置为你当前的平台
4. Set Bundle Version (Text) : 可以设置不同版本的AssetBundle
5. BuildAssetBundleOptions (EnumFlagPop) : 打包设置
6. Build Update File (CheckBox) : 自动创建版本Patch文件, Patch是AssetBundleMaster.AssetLoad.LocalVersion.UpdateInfo 的Josn序列化.
7. Clean TempBuildFolder (CheckBox) : AssetBundle被打包到临时文件夹, 临时文件夹可能含有非当前版本文件, 清除冗余文件.
8. Built-In Shader Collection (Scroll View CheckBox) : 对使用相同內建shader的材质进行整合, 减少重复编译.
9. Build Root (Label, Button) : 资源文件夹根目录, 所有此文件夹下的资源可以被读取.
10. Step1 Clear Old Datas (Button) : 清除已经被设置的AssetBundleName. 可以减少冗余资源被打包.
11. Step 2 Set AssetBundle Names (Button) : 自动设置包名, 其中包含了资源的自动处理, 处理过程参看附录.
12. Step 3 Start Build AssetBundles (Button) : 开始打包.
以上就是所有设置项. 我们先把编辑器和发布的加载选项都选为AssetBundle_StreamingAssets, 这个是最简单的发布方式. 然后点击
Step1...
Step2...
Step3...
这三步就会自动设置和打包AssetBundle了. 每一步都有提示操作.
Step1
Step2
Step3
当创建完成后会询问是否从临时文件夹复制AssetBundle到StreamingAssets文件夹下, 选择Yes即可.
可以看到StreamingAssets下面有打包好的文件:
这样就打包好了, 如果在编辑器下运行, 只需要再打开 StartScene1 场景即可, 重复上面的API测试步骤, 可以看到异步读取打印的Log信息确实是异步的了.
我们在AssetDataBase_Editor模式和AssetBundle_StreamingAssets模式下读取资源的内容也不一样了, 对比一下检视面板[AssetLoadManager] :
AssetDataBase_Editor 没有AssetBundle相关资源
AssetBundle_StreamingAssets模式下读取了AssetBundle
这样就完成了打包和在编辑器下读取AssetBundle的流程了, 如果要发布到目标平台, 那么在打包面板的 Platform Selection 选项中选择目标平台打包即可, 接下来是最重要的一点, 场景与Build Settings :
在发布时, AssetBundle模式是不需要把场景加到Build Settings中的, 因为如果场景在Build Root文件夹中, 场景会被打包成AssetBundle, 而加入到Build Settings中的场景也会被自动打包到Resouces里面, 这样场景就会被打包两次, 浪费时间和空间. 比如我们当前的工程, 如果想发布的话, 我们怎样让程序启动之后自动加载初始场景StartScene1呢?
第一种方法是把StartScene1加到Build Settins中, 因为StartScene1很小很简单, 被两次打包也不会产生什么影响:
第二种方法是使用初始化调用属性来读取初始场景(可以写在任何代码中), 这种方式更好, 因为规避了二次打包的风险 :
如果你使用的是 AssetBundle_Remote 模式 :
这段代码请自行添加到任意脚本中. 这时 Build Settings 里面就不需要加场景了. 虽然需要写代码.
以上就是所有API以及AssetBundle创建和发布的流程了.
现在开始说明打包功能 8. Built-In Shader Collection 的作用, 我们直接修改Build Root读取AssetBundleMasterExample2 :
只简单修改了一下版本, 打包2.0.0版本, 因为我们之前Build了1.0.0版本, 所以Exists Versions里面显示出了1.0.0版本, 这里我们试一试Update文件创建, 勾选它, 修改Build Root为AssetBundleMasterExample2, 我们看看这个资源文件夹下的文件:
Example_ShaderCollection场景中有一些建筑, 使用了大量标准材质(Standard Shader):
这个场景引用了很多标准材质, 会造成一些性能问题. 我们现在要加载的初始场景为StartScene2(它自动读取Example_ShaderCollection场景), 把刚才的初始场景改一改:
第一种方式 :
第二种方式 :
如果你使用的是 AssetBundle_Remote 模式 :
再次执行
Step1...
Step2...
Step3...
这次在执行完Step3时出现了新的提示信息:
这是因为我们已经有1.0.0版本的AssetBundle包了, 如果我们是增量打包, 复制 1.0.0 的文件到 2.0.0 版本文件夹下, 再开始打包会比重新打包快很多, 不过 2.0.0 打包的是AssetBundleMasterExample2的资源而 1.0.0 打包的是AssetBundleMasterExample的资源, 完全不同. 所以功能 7. Clean TempBuildFolder (CheckBox) 的功能就体现出来了, 它能清除打包完后不属于这个版本的文件. 不管你选择哪个都相当于重新打包, 等到打包结束, 就开始Build一个app来查看这个场景的特点吧.
在打包完成之后, 我们可以 Build app 然后运行来测试一下, 不要忘了在Build Settings勾选 [Development Build] 和 [Autoconnect Profiler]. 我的平台为Win10, 当前测试使用Unity2020.
全部 AssetBundle 大小 : 5.11MB(包含 .manifest 文件)
Task Manager 显示的运行时APP内存占用 : 279.8MB
Profiler Shader 113.0MB, Standard Shader 被多次编译了
问题出在Standard Shader上, 我们使用Shader Collection的方式再打一个3.0.0的版本看看, 这次把Shader Collection中的Standard选上, 因为Shader的变更不会造成引用资源的变更, 所以我们把BuildAssetBundleOption的选项加上ForeRebuildAssetBundle, 否则会造成材质丢失 :
PS : 默认BuildAssetBundleOption选项ChunkBasedCompression是为了测试时更快速的打包, 用户可以自己进行修改. 编辑器下的配置文件在下图的文件夹内, 你可以上传至SVN等同步给其他人:
同样重新执行
Step1...
Step2...
Step3...
然后 Build App 运行测试 :
全部 AssetBundle 大小 : 1.77MB(包含 .manifest 文件)
Task Manager 显示暂用内存 122.0MB
Profiler : Shader 10M, Standard Shader 只进行了一次编译, 被引用了44次
可以看到两个版本的巨大差异, 在2.0.0版本浪费了更多的I/O时间, 消耗了更多的内存. 这个问题主要是 Built-In Shader 引起的( 根据使用方法的不同有不同的结果, 比如UGUI用的材质是内置材质, 都是同一个, 所以不会产生多次编译的问题 ).
这个测试在各种版本之间都存在( Unity5/Unity2017/Unity2018/Unity2019/Unity2020 ), 几乎得到同样的结果.
我们来看一下 AssetBundle 临时文件夹, 它在跟Assets文件夹同一层的 :
Assets/../AssetBundles/StandaloneWindows64/
你可以看到不同版本的文件夹以及升级补丁文件, 它是Json格式文件, 内容如下(Update_2.0.0_to_3.0.0.txt) :
这意思是你从2.0.0版本设计到3.0.0版本的话, 需要从服务器或CDN下载 "updateList" 里面的文件, 而删除 "deleteList" 里面的本地文件. 这个文件的反序列化是 AssetBundleMaster.AssetLoad.LocalVersion.UpdateInfo 类型.
注意 : 这里只提供了升级补丁文件( update-patch files ), 并没有提供下载更新文件的逻辑, 用户必须自己实现下载更新的系统.
注意2: 如果你只是需要从远程地址加载AssetBundle,你只要把加载模式 AssetLoadMode 设为 AssetBundle_Remote 即可, 它将会从本地或远程地址读取最新的资源.
使用 AssetBundle_Remote从服务器读取资源的例子. 只需要把打包好的资源放在服务器下即可 :
复制资源到服务器路径( 此处使用一个 IIS 服务器, 它的物理目录在 E:LocalServerWebGL_Raw )
复制到服务器的文件可以忽略 .manifest 文件.
AssetBundle_EditorTest 模式就是从临时文件夹内读取资源的, 所以可以很方便地在已经打包好的各个版本间切换, 方便测试. 读取的平台和版本就是编辑器面板中的当前设定:
临时文件为 Assets/../AssetBundles/StandaloneWindows64/x.x.x/
PS : 你运行的时候可以看LOG信息, 它也会显示相关路径信息.
工具栏其余功能:
1. 从临时文件夹复制AssetBundle到StreamingAssets文件夹 :
一般在打包完成后会出现提示框提示用户是否要复制到StreamingAssets文件夹, 这个是手动复制.
2. 清除AssetBundleMaster产生的数据 :
它会清除一些编辑器产生的序列化文件. 没有这些文件AssetBundleMaster将无法运行.
3. Open Caching Folder
如果你使用 AssetBundle_Remote 从服务器加载, 你可以看到 Cache 文件夹里有缓存文件, Unity5 显示的缓存地址没有缓存文件可能哪里有错误, Unity2017~Unity2020 缓存文件夹显示正常.
以上就是AssetBundleMaster的全部说明, 它就像介绍说的, 是一个自动打包以及资源读取的整合解决方案, 提供了开发者很友好的开发流程, 并实现了大部分的资源自动管理逻辑, 提供了一些资源自动处理方案以及对资源更新的友好支持. 还是那句话 : 我们建议在任何情况下都不应该人为地手动去设置AssetBundle分包, 或不设置任由其自动分包, 即使你基于游戏更新或其它方面的考虑, 大部分情况下都与引擎特性不符. Have Fun.
附录
资源自动处理, 编辑器代码在AssetBundleBuildWindow.cs 文件中, 其中资源处理有几个部分:
1. SpriteAtlas, 在Unity5中无法自己创建SpriteAtlas, 在Unity2017之后可以创建, 代码在AssetBundleBuildWindow::1762行 CreateSpriteAtlasAssets 函数内, 通过把所有相同Packing Tag的Sprite资源设置到同一个AssetBundle包内, 规避了SpriteAtlas可能被重复打包以及造成额外DrawCall的问题(Unity5中同样适用). 这里使用了Packing Tag作为分包的依据, 可是在Unity2019中检视面板没有显示Packing Tag 不过它仍然存在于序列化数据之中, 因此AssetBundleMaster提供了一个重载的检视面板, 添加了Packing Tag设置, 编辑器代码为 TextureImporterInspector.cs :
Unity5
Unity2019
AssetBundleMaster重载了Unity2019检视面板
注意图集如果因为一些原因导致Include in Build的信息丢失(比如资源更新等), ResourceLoadManager通过对UnityEngine.U2D.SpriteAtlasManager.atlasRequested 添加了回调, 对这种情况作了补偿操作, 最大限度保证读取的正确性, 代码在 ResourceLoadManager.RequestAtlas, 用户如果碰到此问题请到这里进行调试(Unity2017以及之后的版本).
SpriteAtlas会被自动创建到 Assets/AssetBundleMasterSpriteAtlas 文件夹下(Unity2017以及之后的版本).
2. EditorConfigSettings, 自动调整一些编辑器下的设置, 用户可以根据自己的工程修改此处代码. 代码在AssetBundleBuildWindow::1679行 EditorConfigSettings 函数内, 它首先修改了UnityEditor.EditorSettings.spritePackerMode以方便创建SpriteAtlas, 并修改了GraphicsSettings的相关变量, 防止打包后场景中物体LightMap信息被错误剥离. 见图 :
3. TerrainData, 它使用的贴图需要设置isReadable属性, 如果不设置在编辑器下不会报错, 直到打包运行之后才会报错. 将自动把被引用到的贴图设置isReadable.
AssetBundle模式下的自动资源释放逻辑, 我们可以通过Demo : Example_UnloadAssetEfficiency 进行说明. 用户请选择AssetBundle模式并打包, 然后打开此场景进行资源读取. 我们在读取资源的时候, 只有三个读取资源的Manager类( 都在 AssetBundleMaster.ResourceLoad 命名空间下 ) :
1. ResourceLoadManager 非完全控制类资源, 不能通过 AssetBundle.Unload(true); 释放资源
2. PrefabLoadManager 完全控制类资源, 通过 AssetBundle.Unload(true); 释放资源
3. SceneLoadManager 完全控制类资源, 通过 AssetBundle.Unload(true); 释放资源
也就是说在一般情况下, PrefabLoadManager 和 SceneLoadManager 进行读取的对象, 同样通过它们进行卸载的话, 只会调用 AssetBundle.Unload(true); 释放资源, 具有很高的效率. 而ResourceLoadManager读取的资源一般作为公共资源, 通过它卸载的资源调用的是 AssetBundle.Unload(false); + Resources.UnloadUnusedAssets(); 的方式, 在低效率的同时保证了团队开发中多个开发人员交叉控制资源的安全性, 不会因为其它人的资源卸载导致资源上的问题.
在这些引用资源有交叉的时候, AssetBundle的释放方式也可能会被改变. 比如某个Prefab使用了图片Pic3, 被 PrefabLoadManager 加载出来后如果卸载那么Pic3的AssetBundle应该通 AssetBundle.Unload(true); 释放资源, 而在被释放之前如果通过 ResourceLoadManager 对Pic3进行显式加载, 那么Pic3就成了非完全控制资源了, 这时候它的释放就成了AssetBundle.Unload(false); 了. 相关逻辑在 AssetBundleMaster.AssetLoad.AssetBundleTarget 中, 并且自动释放受成员变量 unloadable 控制.
我们打开场景Example_UnloadAssetEfficiency, 注意要用AssetBundle加载模式:
点击LoadCubeButton读取GameObject, 可以看到所有AssetBundle都是完全控制的.
点击UnloadCubeButton, 看到删除资源逻辑使用的是AssetBundle.Unload(true);
我们这次把Cube和Pic3都显式加载出来, 点击LoadCubeButton, LoadTextureButton:
可以看到这时textures/pic3.ab被设置为了 unloadable : False, 意思是这个Pic3已经成了非完全控制资源了, 如果我们点击UnloadCubeButton, 看到删除资源逻辑变成了:
textures/pic3.ab这个AssetBundle被保留了, 因为它被 ResourceLoadManager 显式加载了, 并且有引用, 我们继续点击UnloadTextureButton:
结果就是Pic3资源通过 AssetBundle.Unload(false); + Resources.UnloadUnusedAssets(); 的方式进行了卸载.
以上就是卸载的逻辑基础了, 用户通过自己的修改以及对自己项目的整合, 可以很容易地支持大型项目.
Update support :
1. 我们没有实现更新系统, 只实现了生成更新补丁文件, 只要有新的版本生成, 它就会刷新所有的补丁文件信息, 这样你的更新系统就可以使用补丁文件作为参考.
Assets/../AssetBundles/[Platform]/
你可以通过 LocalVersion 中的 VersionInfo 来获取当前资源版本的信息 :
2. 如果你使用 AssetBundle_PersistantDataPath 模式, AssetBundleMaster 会优先从 Application.persistentDataPath 路径中读取资源, 如果该路径没有文件, 则会从 Application.streamingAssetsPath 中去读取. 这就是说你可以发一个基础版本(1.0.0或2.0.0)并在StreamingAssets之中包含该版本的资源, 然后你可以下载新版本资源到 Application.persistentDataPath 路径下, 运行时就会读取新的资源了. 几乎所有的平台都是 persistentDataPath 可读写的, 你的资源版本就是可以更新的了.
3. 如果你选择 AssetBundle_Remote 模式, 你就是从远程地址读取 AssetBundle, 你就不需要做更新系统了, 它总是读取最新的资源(从本地或者远程地址, 缓存到本地).