Unity实践—Unity 内置资源独立打包

针对内置资源重复打包冗余的问题,编写 Addressables Build 脚本将内置资源独立打包
本人原博:Warl-G's Blog - Unity实践—Unity 内置资源独立打包

什么是内置资源

Unity 提供了一些内置资源,可在编辑器中找到内置资源包unity_builtin_extra

  • Windows: ~/Editor/Data/Resources/unity_builtin_extra
  • MacOS:~/Unity.app/Contents/Resources/unity_builtin_extra

unity_builtin_extra 中包含了一系列默认 Shader 和贴图等资源,可在编辑器中直接选择

image
image

由上图可见内置贴图资源路径为 Resources/unity_builtin_extra,在代码中可使用AssetDatabase.GetAssetPath 得到同样的路径

但无法通过该路径读取资源,编辑器下可用接口AssetDatabase.GetBuiltinExtraResource加载内置资源,以下为内置贴图路径

"UI/Skin/UISprite.psd"
"UI/Skin/Background.psd"
"UI/Skin/InputFieldBackground.psd"
"UI/Skin/Knob.psd"
"UI/Skin/Checkmark.psd"
"UI/Skin/DropdownArrow.psd"
"UI/Skin/UIMask.psd"

另外还有Runtime还有接口Resources.GetBuiltinResource,但目前没有明确用法

为什么要将内置资源打包

若制作多个使用了同样内置资源的 Prefab 且被分到了不同的 Bundle 中,AddressablesDefault Build Script是不会统计这些引用而单独分包的,会导致内置资源被重复打进不同的 Bundle 中

可通过创建使用KnobUISprite的 Image Prefab 各两个,并分别打成四个 Bundle

image
image

通过对四个 Bundle 解包可看到使用相同资源的 Bundle 都有类似如下的内容(KnobUISprite),data 部分就是资源实际的数据内容,被重复打进了两个包

ID: 5424255917358561739 (ClassID: 213) Sprite
    m_Name "Knob" (string)
    m_Rect  (Rectf)
        x 12 (float)
        y 12 (float)
        width 40 (float)
        height 40 (float)
    m_Offset (0 0) (Vector2f)
    m_Border (0 0 0 0) (Vector4f)
    m_PixelsToUnits 200 (float)
    m_Pivot (0.5 0.5) (Vector2f)
    m_Extrude 1 (unsigned int)
    m_IsPolygon 0 (bool)
    m_RenderDataKey  (pair)
        first 0000000000000000f000000000000000 (GUID)
        second 10913 (SInt64)
    m_AtlasTags  (vector)
        size 0 (int)
...............................
...............................
            size 184 (int)
            data (UInt8) #0: 205 204 204 61 205 204 76 61 0 0 0 0 92 143 194 61 205 204 204 189 0 0 0 0 205
            data (UInt8) #25: 204 204 61 123 20 174 189 0 0 0 0 123 20 174 61 205 204 204 61 0 0 0 0 205 204
            data (UInt8) #50: 76 189 205 204 204 61 0 0 0 0 10 215 163 189 205 204 204 189 0 0 0 0 92 143 194
            data (UInt8) #75: 189 41 92 143 61 0 0 0 0 205 204 204 189 143 194 245 60 0 0 0 0 205 204 204 189
            data (UInt8) #100: 174 71 97 189 0 0 0 0 0 0 0 0 62 62 62 143 62 62 62 143 62 62 62 143 62
            data (UInt8) #125: 62 62 143 62 62 62 143 73 73 73 137 116 116 116 135 146 146 146 94 255 255 255 0 255 255
            data (UInt8) #150: 255 0 146 146 146 50 117 117 117 134 55 55 55 143 62 62 62 143 62 62 62 143 62 62 62
            data (UInt8) #175: 143 62 62 62 143 62 62 62 143
        m_Bindpose  (vector)
            size 0 (int)

...............................
...............................

此时一个 Bundle 的大小约为 8 KB

image

若内置资源使用范围比较广泛且分包较多,也是有可能造成一定的空间浪费,因此可重写Addressables打包脚本,将使用的内质资源独立打包

编写 Addressables 打包脚本

默认打包脚本

首先可以查看Addressables的默认打包流程,在Packages/Addressables/Editor/Build/DataBuilders下可找到Addressables提供的几种预设打包模式脚本,其中BuildScriptPackedMode.cs即为Default Build Script

static IList<IBuildTask> RuntimeDataBuildTasks(string builtinShaderBundleName)
{
    var buildTasks = new List<IBuildTask>();

    // Setup
    buildTasks.Add(new SwitchToBuildPlatform());
    buildTasks.Add(new RebuildSpriteAtlasCache());

    // Player Scripts
    if (!s_SkipCompilePlayerScripts)
        buildTasks.Add(new BuildPlayerScripts());
    buildTasks.Add(new PostScriptsCallback());

    // Dependency
    buildTasks.Add(new CalculateSceneDependencyData());
    buildTasks.Add(new CalculateAssetDependencyData());
    buildTasks.Add(new AddHashToBundleNameTask());
    buildTasks.Add(new StripUnusedSpriteSources());
    buildTasks.Add(new CreateBuiltInShadersBundle(builtinShaderBundleName));
    buildTasks.Add(new PostDependencyCallback());

    // Packing
    buildTasks.Add(new GenerateBundlePacking());
    buildTasks.Add(new UpdateBundleObjectLayout());
    buildTasks.Add(new GenerateBundleCommands());
    buildTasks.Add(new GenerateSubAssetPathMaps());
    buildTasks.Add(new GenerateBundleMaps());
    buildTasks.Add(new PostPackingCallback());

    // Writing
    buildTasks.Add(new WriteSerializedFiles());
    buildTasks.Add(new ArchiveAndCompressBundles());
    buildTasks.Add(new GenerateLocationListsTask());
    buildTasks.Add(new PostWritingCallback());

    return buildTasks;
}

protected virtual TResult DoBuild<TResult>(AddressablesDataBuilderInput builderInput, AddressableAssetsBuildContext aaContext) where TResult : IDataBuilderResult
{
  //////////////////////
  //////////////////////
  var builtinShaderBundleName = Hash128.Compute(GetProjectName()) + "_unitybuiltinshaders.bundle";
  var buildTasks = RuntimeDataBuildTasks(builtinShaderBundleName);
  buildTasks.Add(extractData);
  
  IBundleBuildResults results;
    using (m_Log.ScopedStep(LogLevel.Info, "ContentPipeline.BuildAssetBundles"))
    using (new SBPSettingsOverwriterScope(ProjectConfigData.generateBuildLayout)) // build layout generation requires full SBP write results
    {
        var exitCode = ContentPipeline.BuildAssetBundles(buildParams, new BundleBuildContent(m_AllBundleInputDefs), out results, buildTasks, aaContext, m_Log);

        if (exitCode < ReturnCode.Success)
            return AddressableAssetBuildResult.CreateResult<TResult>(null, 0, "SBP Error" + exitCode);
   }
  //////////////////////
  //////////////////////
}

抛弃代码中对资源的预分析和配置过程,如上代码为开始构建的核心部分,在DoBuild方法中创建构建任务队列,使用ContentPipeline.BuildAssetBundles开始构建打包

RuntimeDataBuildTasks任务队列中有一个任务CreateBuiltInShadersBundle的功能是找到打包资源中使用到的内置 Shader 并独立打包,分析其中核心方法

public ReturnCode Run()
{
  //获取所有依赖资源中的内置资源,内置资源的GUID都统一为 0000000000000000f000000000000000
    HashSet<ObjectIdentifier> buildInObjects = new HashSet<ObjectIdentifier>();
    foreach (AssetLoadInfo dependencyInfo in m_DependencyData.AssetInfo.Values)
        buildInObjects.UnionWith(dependencyInfo.referencedObjects.Where(x => x.guid == k_BuiltInGuid));

    foreach (SceneDependencyInfo dependencyInfo in m_DependencyData.SceneInfo.Values)
        buildInObjects.UnionWith(dependencyInfo.referencedObjects.Where(x => x.guid == k_BuiltInGuid));

    ObjectIdentifier[] usedSet = buildInObjects.ToArray();
    Type[] usedTypes = BuildCacheUtility.GetTypeForObjects(usedSet);

    if (m_Layout == null)
        m_Layout = new BundleExplictObjectLayout();

  //从依赖的内置资源中找到所有的 Shader 资源,并记录在指定的 Bundle 名下
    Type shader = typeof(Shader);
    for (int i = 0; i < usedTypes.Length; i++)
    {
        if (usedTypes[i] != shader)
            continue;

        m_Layout.ExplicitObjectLocation.Add(usedSet[i], ShaderBundleName);
    }

    if (m_Layout.ExplicitObjectLocation.Count == 0)
        m_Layout = null;

    return ReturnCode.Success;
}

脚本改写

由上述代码可见,默认的打包脚本已经帮助我们筛选出了所有的内置资源,只是额外添加了 Shader 单一类型的筛选,因此直接改造CreateBuiltInShadersBundle即可

  1. 创建一个新的实现IBUildTask的类 CreateBuiltInBundle,主要代码内容与CreateBuiltInShadersBundle保持一致,构造方法记录两个 Bundle 名ShaderBundleName 和 BundleName ,一个用于打包内置 Shader,一个用于打包其他内置资源,并对做出如下修改
public ReturnCode Run()
{
    HashSet<ObjectIdentifier> buildInObjects = new HashSet<ObjectIdentifier>();
    foreach (AssetLoadInfo dependencyInfo in m_DependencyData.AssetInfo.Values)
        buildInObjects.UnionWith(dependencyInfo.referencedObjects.Where(x => x.guid == k_BuiltInGuid));

    foreach (SceneDependencyInfo dependencyInfo in m_DependencyData.SceneInfo.Values)
        buildInObjects.UnionWith(dependencyInfo.referencedObjects.Where(x => x.guid == k_BuiltInGuid));

    ObjectIdentifier[] usedSet = buildInObjects.ToArray();
    Type[] usedTypes = ContentBuildInterface.GetTypeForObjects(usedSet);

    if (m_Layout == null)
        m_Layout = new BundleExplictObjectLayout();
    
  // 将 Shader 和非 Shader 资源分别记录到两个不同的 Bundle 中
    Type shader = typeof(Shader);
    for (int i = 0; i < usedTypes.Length; i++)
    {
        m_Layout.ExplicitObjectLocation.Add(usedSet[i], usedTypes[i] == shader ? ShaderBundleName : BundleName);
    }

    if (m_Layout.ExplicitObjectLocation.Count == 0)
        m_Layout = null;

    return ReturnCode.Success;
}
  1. 创建一个新的 Build Script 继承自BuildScriptBase,所有代码和BuildScriptPackedMode.cs保持一致,菜单名称配置可自定义

    RuntimeDataBuildTasksbuildTasks.Add(new CreateBuiltInShadersBundle(builtinShaderBundleName));替换为改造后的CreateBuiltInBundle,并在DoBuild方法中配置 Bundle 名称

static IList<IBuildTask> RuntimeDataBuildTasks(string builtinShaderBundleName, string builtinBundleName)
{
    var buildTasks = new List<IBuildTask>();

    // Setup
    buildTasks.Add(new SwitchToBuildPlatform());
    buildTasks.Add(new RebuildSpriteAtlasCache());

    // Player Scripts
    if (!s_SkipCompilePlayerScripts)
        buildTasks.Add(new BuildPlayerScripts());
    buildTasks.Add(new PostScriptsCallback());

    // Dependency
    buildTasks.Add(new CalculateSceneDependencyData());
    buildTasks.Add(new CalculateAssetDependencyData());
    buildTasks.Add(new AddHashToBundleNameTask());
    buildTasks.Add(new StripUnusedSpriteSources());
    buildTasks.Add(new CreateBuiltInBundle(builtinShaderBundleName, builtinBundleName));
    buildTasks.Add(new PostDependencyCallback());

    // Packing
    buildTasks.Add(new GenerateBundlePacking());
    buildTasks.Add(new UpdateBundleObjectLayout());
    buildTasks.Add(new GenerateBundleCommands());
    buildTasks.Add(new GenerateSubAssetPathMaps());
    buildTasks.Add(new GenerateBundleMaps());
    buildTasks.Add(new PostPackingCallback());

    // Writing
    buildTasks.Add(new WriteSerializedFiles());
    buildTasks.Add(new ArchiveAndCompressBundles());
    buildTasks.Add(new GenerateLocationListsTask());
    buildTasks.Add(new PostWritingCallback());

    return buildTasks;
}

protected virtual TResult DoBuild<TResult>(AddressablesDataBuilderInput builderInput, AddressableAssetsBuildContext aaContext) where TResult : IDataBuilderResult
{
  //////////////////////
  //////////////////////
  var builtinBundleName = Hash128.Compute(GetProjectName()) + "_unitybuiltin.bundle";
  var builtinShadersBundleName = Hash128.Compute(GetProjectName()) + "_unitybuiltinshaders.bundle";
  var buildTasks = RuntimeDataBuildTasks(builtinShadersBundleName, builtinBundleName);
  buildTasks.Add(extractData);
  //////////////////////
  //////////////////////
}

修改效果

构建 Bundle 后,多出一个大小为 7 KB 的defaultlocalgroup_unitybuiltin.bundle,通过解包可见其中只有之前重复打包的 Knob 和 UISprite 两个内置资源,而之前的四个 Bundle 已不再包含具体的资源数据,仅包含一段简单的引用数据,同时单个包体的大小由之前的 8 KB 减小为 4 KB

image
Builtin 打包前 Builtin 打包后
Bundle 数量 4 5
总 Bundle 大小 32 KB 22 KB
单个包体大小
image
image

源码链接:GRTools.Addressables · Warl-G

参考

Unity内置资源如何打包避免冗余 - 知乎 (zhihu.com)

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容