obsidian/笔记文件/2.笔记/关于Unity3D的AssetBundle打包的建议.md
2025-03-26 00:02:56 +08:00

8.3 KiB
Raw Blame History

#unity/日常积累

在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替换是一个比较好的方式。

!Pasted image 20240104201050.png

2、关于内置的资源在Unity3D资源导入的时候模型、特效、场景等经常会引用内置的材质球 Default-Diffuse、Default-Material、Default-Particle、Default-Skybox、Sprites-Default等。这会导致项目依赖大量的内置图片、材质和shader特别是引用了Standard这个shader。在打包后都被隐式的包含在AssetBundle中导致大量的冗余同样加载到内存中也会导致重复的内存占用。

去除这些资源比较有效的办法是拷贝一份内建资源到项目中用自定义shader替换标准shader然后写一个PostProcessor脚本自动替换这些资源。

Unity5.6版本,内建资源如下:(可以从我的github下载

!Pasted image 20240104201101.png

下面的脚本在资源导入时自动替换内置资源避免依赖内置图片、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、不同平台资源格式是有差异的特别是shaderAndroid、iOS等其他平台的shader是不能在Editor模式使用的。遇到很多朋友在Android平台下打包AssetBundle然后使用Android平台的资源在编辑器模式下运行测试出现大量紫块的问题。如果你的操作系统是Window平台你应该打包Window平台的资源在Editor模式使用如果操作系统是Mac那么Editor模式下你应该使用Mac平台的资源。如果非要在Editor模式下使用Android、iOS的资源也可以在场景加载后使用Shader.Find 加载shader替换场景中所有的shader。