在Unity3D游戏开发中,要做到游戏资源的热更新,只有唯一途径,就是使用AssetBundle来打包资源,使用过AssetBundle的应该都知道,体验不是很好!实话说,从AssetBundle的设计来说就有太多的问题,AssetBundle对使用者来说不是透明的,使用者必须要关心资源所在的包,还要维护其复杂的依赖关系,并且Unity3D的Asset都是非托管对象,还要考虑它的释放等问题。在底层没有全局弱缓存,多次加载同一个AB,会导致内存中存在多份Asset模板对象。Unity3D的Resources、AssetBundle以及AssetDatabase,连加载的路径规则都不一致,有的不要扩展名,有的需要扩展名,有的必须完整路径加载,有的又可以只需要文件名就可以加载,导致使用起来也很迷糊。前面这些问题,有的是可以自己写一个加载插件解决的(比如我写的Loxodon.Framework.Bundle解决了其中大部分问题),有些是除了U3D官方,别人无法解决的(比如全局弱缓存)。吐槽完了,下面来看看使用过程中我遇到的一些问题和建议。
使用AssetBundle的建议
1、 关于内置的shader,除了Standard的2个shader外,尽量配置在Graphics中的Always Included Shaders 的列表中,配置在这个列表中的shader不会在被打包到AssetBundle中。关于Lightmap Modes和Fog Modes请都使用Manual模式,手动配置使用了的灯光贴图模式,不要选择自动,否则会根据当前打开的场景配置这几个参数,因为每次打包时,当前打开的场景可能不一样,会导致很多问题。
这个配置表不要频繁修改,每次修改之后,都要删除AssetBundle的打包目录,重新打包所有的AssetBundle,才会对所有的AssetBundle生效。否则只有有改变的AssetBundle会生效,没有改变的AssetBundle不会生效,也可能导致某些shader显示错误。
关于自定义的shader,可以统一打包到同一个AssetBundle中,在游戏启动时就载入到内存,并且保证这个AssetBundle不要卸载,这样确保在游戏运行过程中,不会出现多份shader,占用多份内存。
关于Standard的shader,最好不要使用,它占用大量内存(经常1兆到几兆),在某些低端机器还会导致GPU显存不够而崩溃,使用自定义shader替换是一个比较好的方式。
2、关于内置的资源,在Unity3D资源导入的时候,模型、特效、场景等经常会引用内置的材质球 Default-Diffuse、Default-Material、Default-Particle、Default-Skybox、Sprites-Default等。这会导致项目依赖大量的内置图片、材质和shader,特别是引用了Standard这个shader。在打包后,都被隐式的包含在AssetBundle中,导致大量的冗余,同样加载到内存中也会导致重复的内存占用。
去除这些资源比较有效的办法是拷贝一份内建资源到项目中,用自定义shader替换标准shader,然后写一个PostProcessor脚本,自动替换这些资源。
Unity5.6版本,内建资源如下:(可以从我的github下载)
下面的脚本在资源导入时自动替换内置资源,避免依赖内置图片、shader、材质球等
using UnityEngine;
using UnityEditor;
using System.Collections.Generic;
using System.Text.RegularExpressions;
class BuiltinAssetReplacePostprocessor : AssetPostprocessor
{
static Regex regex = new Regex("(Default-Material)|(Default-Diffuse)|(Default-Particle)|(Default-Skybox)|(Sprites-Default)|(Default-ParticleSystem)");
static Dictionary<string, Material> materials = new Dictionary<string, Material>();
static Material GetMaterial(string path)
{
Material material;
if (materials.TryGetValue(path, out material))
return material;
material = AssetDatabase.LoadMainAssetAtPath(path) as Material;
if (material == null)
material = new Material(Shader.Find("Mobile/Diffuse"));
materials.Add(path, material);
return material;
}
//替换模型的内置材质球,只有当模型使用了内置的材质球和shader时会替换
protected virtual Material OnAssignMaterialModel(Material previousMaterial, Renderer renderer)
{
if (previousMaterial.name == "" || previousMaterial.name.Contains("Default-Material")
|| previousMaterial.shader.name.StartsWith("Standard") || previousMaterial.name.Contains("-Default")
|| previousMaterial.name.Contains("No Name") || !(assetImporter as ModelImporter).importMaterials)
{
return GetMaterial("Assets/builtin/builtin_materials/Default-Material.mat");
}
return null;
}
//替换prefab中引用的内置资源
static void OnPostprocessAllAssets(string[] importedAsset, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths)
{
if (importedAsset.Length > 0)
{
for (int i = 0; i < importedAsset.Length; i++)
{
var path = importedAsset[i];
if (!path.EndsWith(".prefab"))
continue;
GameObject go = AssetDatabase.LoadAssetAtPath<GameObject>(path);
if (go == null)
continue;
Renderer[] renderers = go.GetComponentsInChildren<Renderer>();
if (renderers == null)
continue;
bool success = false;
foreach (Renderer p in renderers)
{
if (p.sharedMaterials == null || p.sharedMaterials.Length <= 0)
continue;
Material[] materials = p.sharedMaterials;
for (int j = 0; j < materials.Length; j++)
{
var material = materials[i];
if (material == null || Regex.IsMatch(AssetDatabase.GetAssetPath(material), @"Assets/builtin/builtin_materials/"))
continue;
var match = regex.Match(material.name);
if (!match.Success)
continue;
Material newMaterial = GetMaterial(string.Format("Assets/builtin/builtin_materials/{0}.mat", material.name));
if (newMaterial == null)
continue;
materials[i] = newMaterial;
success = true;
}
if (success)
p.sharedMaterials = materials;
}
if (success)
Debug.LogFormat("Replace default material of particle: \"{0}\" ", importedAsset[i]);
}
return;
}
}
}
3、建议使用LZ4压缩格式,弃用以前的压缩格式,LZ4压缩支持分块压缩,虽然压缩比率不如LZMA,但是解压速度更快,而且加载某个特定资源不需要将整个AssetBundle解压,不会到导致大量的内存占用。
4、弃用WWW加载AssetBundle的方式,会占用AssetBundle包大小双倍以上的内存,导致内存峰值很高。使用UnityWebRequest替换WWW。LZ4格式,配合使用AssetBundle.LoadFromFileAsync 和 UnityWebRequest 加载,最大占用内存就是AB解压后的内存,不会导致内存峰值。
5、尽量不要协程并发加载同一个AssetBundle包中的资源,早期的版本(大概Unity3d 5.5)在Android平台发现会导致加载的资源材质或者贴图丢失,Unity3D新的版本不知道这个bug是否解决。
6、不同平台资源格式是有差异的,特别是shader,Android、iOS等其他平台的shader是不能在Editor模式使用的。遇到很多朋友在Android平台下打包AssetBundle,然后使用Android平台的资源,在编辑器模式下运行测试,出现大量紫块的问题。如果你的操作系统是Window平台,你应该打包Window平台的资源在Editor模式使用,如果操作系统是Mac,那么Editor模式下,你应该使用Mac平台的资源。如果非要在Editor模式下使用Android、iOS的资源,也可以在场景加载后,使用Shader.Find 加载shader,替换场景中所有的shader。