概述

  • 自定义Manifest文件,维护资源版本等版本,各种AB资源,以及自定义的非AB资源。
  • 不支持变体,SBP决定的
  • 资源依赖分析,杜绝资源冗余,支持U2D SpriteAtlas。不使用AssetBundleName,通过相应接口获取资源引用关系后,自己维护AB引用。
  • 资源分组策略,支持基于文件夹,文件等方式快速资源分组。
  • 一键打包支持,提供设置面板,反射接口,可以自定义穿插打包逻辑,可快速接入云构建系统。
  • 文件名使用文件路径hash;文件流hash(md5),作为文件内容是否一致的判断

使用ScriptableBuildPipline

Unity于2018推出了ScriptableBuilPipline,以下会简称SBP,将原本c++层的构建管线,移植到了c#层,方便用户扩展构建管线。

改用此管线后,最大的优势是,少了很多manifest文件,减少了IO压力。打包没有用Cache,比老管线时间长一些,有了Cache会快一些。可以打包内置资源,杜绝资源冗余。

另外有一点需要注意,SBP不支持AB变体。

SBP打包方法和内置资源处理逻辑

SBP打包脚本
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEditor.Build.Pipeline.Tasks;

namespace Saro.XAsset.Build
{
    public static class SBPBuildAssetBundle
    {
        public static ReturnCode BuildAssetBundles(
            string outputPath,
            IList<AssetBundleBuild> assetBundleBuilds,
            BuildAssetBundleOptions options,
            BuildTarget buildTarget,
            out IBundleBuildResults results)
        {
            var buildContent = new BundleBuildContent(assetBundleBuilds);

            var buildParams = new BundleBuildParameters(buildTarget, BuildPipeline.GetBuildTargetGroup(buildTarget), outputPath);

            buildParams.ContiguousBundles = true;
            buildParams.WriteLinkXML = true;
            buildParams.AppendHash = false;


            if (options.HasFlag(BuildAssetBundleOptions.ChunkBasedCompression))
                buildParams.BundleCompression = UnityEngine.BuildCompression.LZ4;
            else if (options.HasFlag(BuildAssetBundleOptions.UncompressedAssetBundle))
                buildParams.BundleCompression = UnityEngine.BuildCompression.Uncompressed;

            if (options.HasFlag(BuildAssetBundleOptions.DisableWriteTypeTree))
                buildParams.ContentBuildFlags |= ContentBuildFlags.DisableWriteTypeTree;

            if (!options.HasFlag(BuildAssetBundleOptions.ForceRebuildAssetBundle))
            {
                // TODO 需要搞CacheServer

                //BuildCache

                //Set build parameters for connecting to the Cache Server
                buildParams.UseCache = true;
                //buildParams.CacheServerHost = "buildcache.unitygames.com";
                //buildParams.CacheServerPort = 8126;
            }

            var customTasks = CustomBuildTasks();
            ReturnCode exitCode = ContentPipeline.BuildAssetBundles(buildParams, buildContent, out results, customTasks);
            return exitCode;
        }

        public static IList<IBuildTask> CustomBuildTasks()
        {
            var buildTasks = new List<IBuildTask>();

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

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

            // Dependency
            buildTasks.Add(new CalculateSceneDependencyData());
#if UNITY_2019_3_OR_NEWER
            buildTasks.Add(new CalculateCustomDependencyData());
#endif
            buildTasks.Add(new CalculateAssetDependencyData());
            buildTasks.Add(new StripUnusedSpriteSources());
            buildTasks.Add(new CreateBuiltInResourcesBundle(
                "unitybuiltindata" + XAssetPath.k_AssetExtension,
                "unitybuiltinshader" + XAssetPath.k_AssetExtension));
            //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 AppendBundleHash());
            //buildTasks.Add(new PostWritingCallback());

            // Generate manifest files
            // TODO: IMPL manifest generation

            return buildTasks;
        }
    }
}
打包内置资源
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEditor;
using UnityEditor.Build.Content;
using UnityEditor.Build.Pipeline;
using UnityEditor.Build.Pipeline.Injector;
using UnityEditor.Build.Pipeline.Interfaces;
using UnityEngine;

namespace Saro.XAsset.Build
{
    public class CreateBuiltInResourcesBundle : IBuildTask
    {
        private static readonly GUID k_unity_builtin_extra = new GUID("0000000000000000f000000000000000"); // Resources/unity_builtin_extra
        private static readonly GUID k_unity_default_resources = new GUID("0000000000000000e000000000000000"); // Library/unity default resources

        [InjectContext(ContextUsage.In, false)]
        private IDependencyData m_DependencyData;

        [InjectContext(ContextUsage.InOut, true)]
        private IBundleExplictObjectLayout m_Layout;

        public int Version => 1;

        public string ShaderBundleName { get; private set; }

        public string DataBundleName { get; private set; }

        public CreateBuiltInResourcesBundle(string dataBundleName, string shaderBundleName)
        {
            DataBundleName = dataBundleName;
            ShaderBundleName = shaderBundleName;
        }

        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_unity_builtin_extra));

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

            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 : DataBundleName);
            }

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

            return ReturnCode.Success;
        }
    }
}

资源分组策略

  1. res一般只放需要动态加载的资源,其他资源直接被prefab/scene引用即可,目的是减少路径序列化,从而减小manifest文件大小

资源依赖分析

SpriteAtlas打包问题

可以直接看 SpriteAtlas与AssetBundle最佳食用方案,讲得非常好了,其他的文章所写的方式,都挺难用的,不需要什么LateBinding啥的。

另外,作为SpriteAtlas的资源图片,还作为Texture来使用,会有双份图片打包,即Atlas图片里一份,原图一份,需要注意。

由于是直接使用散图的,所以,不会引用到SpriteAtlas,打包时需要特殊处理下,将图集里的散图打到同一个ab里。

经测试,上述仅限于legacy打包管线!sbp打包管线冗余,图集,散图都被打进包了!需要参考下Addressable的处理方式!

自定义Manifest

由于我们自己处理了AB依赖,所以需要自己搞一套AB依赖数据结构。同时也支持非AB资源纳入资源清单,例如Wwise音效资源等等,方便直接从资源服直接下载,这也跟资源更新机制实现有关,资源更新以后再说。

打包工作流

打包设置面板

基于接口反射,实现穿插自定义打包方法,提供一个BuiltIn实现,可以仿照着自行实现一个新的。

打包面板
配置面板

AssetBundle浏览器

改造了官方AssetBundleBrowser插件,给本项目使用,移除了编辑功能,添加了打包后AB大小的对比功能

AssetBundleBrowser

资源打包常见问题

1.场景光照异常

可能是场景设置不对,光照信息打AB后被剔除掉了。

2.模型材质异常

例如,没有高光,显示效果异常,和编辑器下有出入。重新收集下变体,保存成svc文件,重新打包Shader AB,观察结果。尽量避免将Standard等复杂Shader丢到IncludeShaders里去,不然变体会爆炸。

3.组件class丢失

il2cpp将class裁剪掉了,需要配置link文件。例如,AnimatorController打包后,真机丢失,动画失效。但AnimatorContorller为Editor下的类,link文件貌似没有效果,解决方案是Resources文件夹下,弄一个预制体,挂载Animator组件,并赋值一个空的Controller。

TODO List

  1. Asset Graph 自动化分配group。
  2. sbp打包SpriteAtlas,使用include build时,冗余问题。
  3. 扫描材质,自动收集变体(通过代码开启的变体,需要特殊处理)。
  4. 分组也可能会考虑添加GUI编辑支持。估计弄成Addressable那样的。