Unity PAD自动化配置:脚本化构建Android资源包,告别编辑器卡顿
1. 项目概述为什么我们需要绕开官方插件如果你是一个Unity项目的技术负责人或者负责构建流程的开发者大概率对“Play Asset Delivery”这个名词不会陌生。简单来说它是Google Play商店为Android游戏提供的一种资源分发机制允许你将游戏资源比如高清贴图、视频、音频、预制体等打包成独立的“资源包”在游戏安装后按需下载。这能有效减少初始安装包的大小提升用户下载和安装的意愿。Unity官方为这套机制提供了插件支持也就是我们通常说的“Google Play Asset Delivery Plugin”。在Unity编辑器的菜单里你可以找到Google Android App Bundle Asset Delivery Settings这个选项。通过这个图形化界面你可以选择文件夹、配置分发模式安装时、快速跟进、按需看起来非常方便。然而正是这个“方便”的图形界面在项目资源量稍大、或者需要频繁迭代构建时往往会成为效率的“杀手”。我亲身经历过在一个资源量达到几十GB的中大型项目中每次打开这个设置窗口Unity编辑器都会卡顿十几秒甚至更久。更糟糕的是当你点击“Add Folder”选择包含数百个AssetBundle的目录时整个编辑器界面直接“无响应”是家常便饭。这还不是最致命的最致命的是在自动化构建服务器上你无法通过图形界面操作。官方虽然提供了AssetPackConfig等API但文档分散示例零碎很多关键细节比如如何处理依赖、如何与Addressables结合、如何确保构建一致性都需要开发者自己去摸索和踩坑。因此这篇文章的核心目的就是彻底抛弃那个卡顿的图形界面完全通过C#脚本代码来批量、自动化地配置PAD资源包。我会从设计思路、核心API解析、完整实操代码到构建集成和避坑指南为你呈现一套可直接用于生产环境的解决方案。告别卡死拥抱高效和可控的自动化流程。2. 核心思路与方案设计2.1 官方方案的瓶颈分析在深入代码之前我们先剖析一下官方插件卡顿的根源。其图形界面本质上是一个实时扫描和序列化工具。当你选择一个文件夹时它需要递归遍历所有文件识别出Unity可识别的资源如Prefab、Scene、AssetBundle并构建一个内部的数据结构来映射资源与资源包的关系。当资源数量庞大、文件结构复杂时这个扫描和序列化的过程就会消耗大量CPU和I/O时间导致UI线程阻塞。此外图形界面的配置是保存在项目的Assets目录下的通常是Assets/Plugins/Android下的某些文件这不利于版本控制时的清晰管理也容易在团队协作中因误操作而被修改。2.2 代码驱动方案的优势采用纯代码驱动的方案可以带来以下几个核心优势性能与稳定性配置过程仅在构建脚本执行时发生避免了在编辑器中实时扫描带来的卡顿。脚本可以优化遍历逻辑甚至利用缓存机制。可版本控制配置逻辑以C#脚本的形式存在可以清晰地纳入Git等版本控制系统变更历史一目了然。自动化与集成可以轻松集成到CI/CD持续集成/持续部署流水线中实现无人值守的自动化构建。灵活性与可维护性你可以根据自己项目的特定目录结构、命名规范来定制资源包的分组策略逻辑集中且易于调整。可预测性脚本的执行结果是确定的避免了图形界面操作可能带来的偶然错误。2.3 方案架构设计我们的目标是创建一个编辑器脚本它能够在构建Android App Bundle (AAB) 之前动态生成AssetPackConfig。整个流程可以集成到BuildPlayer的流程中。核心步骤设计如下资源收集与规则定义编写一个类定义如何扫描项目目录例如Assets/AssetBundles/Android并根据预设规则如按文件夹、按前缀、按类型将资源文件归类到不同的资源包Asset Pack中。配置对象构建使用Google.Android.AppBundle.Editor命名空间下的AssetPackConfig类将上一步归类好的资源以代码形式添加进去并指定分发模式AssetPackDeliveryMode。配置序列化与注入将构建好的AssetPackConfig对象通过AssetPackConfigSerializer.SaveConfig方法保存到项目特定位置。这个保存的配置会在后续执行Google Build Android App Bundle菜单命令时被读取。构建流程集成创建自定义的构建管道如[MenuItem(MyBuild/Build AAB with PAD)]在构建函数中先执行步骤1-3生成配置再调用Bundletool.BuildBundle或触发标准的AAB构建流程。处理依赖与冗余确保一个资源文件只被包含在一个资源包中并处理AssetBundle之间的依赖关系防止资源重复打包导致AAB体积膨胀。注意本方案主要针对使用AssetBundle方式的PAD集成。如果你的项目使用Unity 2019.4并采用了Addressables系统其与PAD的集成方式更为现代和推荐但核心的“代码配置”思想是相通的。Addressables有自己的AddressableAssetSettings可进行脚本化配置与PAD的对接主要通过PlayAssetDelivery的API在运行时进行构建时的资源包划分则由Addressables的Profile和Group设置来决定。3. 核心API详解与封装要实现代码化配置我们需要深入了解几个关键的API。它们主要来自Google.Android.AppBundle.Editor和Google.Play.AssetDelivery.Editor这两个编辑器命名空间。3.1 AssetPackConfig资源配置的核心AssetPackConfig类是描述所有需要打入AAB的资源包的容器。每个资源包需要定义三个关键属性Name资源包的名称。这是你在运行时通过PlayAssetDelivery.RetrieveAssetBundleAsync(packName)请求时使用的标识符。名称必须唯一且最好使用小写字母、数字和下划线。DeliveryMode分发模式。这是一个AssetPackDeliveryMode枚举包含InstallTime安装时分发。资源包会包含在基础APK中安装后立即可用。适用于启动必备资源。FastFollow快速跟进分发。应用安装后几乎立即开始在后台下载。OnDemand按需分发。只有在应用代码明确请求时才下载。AssetBundles / AssetsFolder资源包包含的内容。你可以直接添加已构建好的AssetBundle文件.assetbundle也可以指定一个文件夹构建系统会自动将该文件夹下的所有文件打包进资源包。创建与添加资源的示例using Google.Android.AppBundle.Editor; using UnityEngine; public static class AssetPackConfigBuilder { public static AssetPackConfig CreateDemoConfig() { var config new AssetPackConfig(); // 方式一添加一个包含单个AssetBundle文件的资源包 string bundlePath Assets/AssetBundles/Android/base_assets.assetbundle; config.AddAssetBundle(base_pack, bundlePath, AssetPackDeliveryMode.InstallTime); // 方式二添加一个包含整个文件夹的资源包构建系统会打包文件夹内所有文件 string folderPath Assets/StreamingAssets/HighResTextures; config.AddAssetsFolder(textures_pack, folderPath, AssetPackDeliveryMode.OnDemand); // 方式三添加多个AssetBundle到同一个资源包这些bundle在运行时属于同一个pack config.AddAssetBundle(level_pack, Assets/AssetBundles/Android/level1.assetbundle); config.AddAssetBundle(level_pack, Assets/AssetBundles/Android/level2.assetbundle); // 为同一个包设置分发模式只需设置一次或最后统一设置 config.SetAssetPackDeliveryMode(level_pack, AssetPackDeliveryMode.FastFollow); return config; } }3.2 AssetPackConfigSerializer配置的持久化生成AssetPackConfig对象后需要将其保存到磁盘这样官方的构建流程才能读取到。AssetPackConfigSerializer类提供了这个方法。using Google.Android.AppBundle.Editor; public static void SaveAssetPackConfig(AssetPackConfig config) { // 这个方法会将config序列化到项目特定的路径通常是Library目录下。 // 当通过Unity编辑器菜单 Google Build Android App Bundle 构建时会读取这个配置。 bool success AssetPackConfigSerializer.SaveConfig(config); if (success) { Debug.Log(AssetPackConfig saved successfully.); } else { Debug.LogError(Failed to save AssetPackConfig.); } }重要提示SaveConfig保存的配置是临时性的它主要作用于下一次通过Unity编辑器GUI触发的构建。对于完全脚本化的构建我们更倾向于直接使用Bundletool.BuildBundle方法将AssetPackConfig作为参数传入这样更直接且不依赖编辑器状态。3.3 Bundletool.BuildBundle脚本化构建的终极武器这是实现完全自动化构建的关键。Bundletool类提供了一个静态方法可以直接接收BuildPlayerOptions和AssetPackConfig输出AAB文件。using Google.Android.AppBundle.Editor; using UnityEditor; public static void BuildAABWithAssetPacks() { // 1. 准备构建选项 BuildPlayerOptions buildOptions new BuildPlayerOptions(); buildOptions.scenes GetEnabledScenePaths(); // 获取所有启用场景的路径 buildOptions.locationPathName Builds/MyGame.aab; buildOptions.target BuildTarget.Android; buildOptions.options BuildOptions.None; // 或根据需要添加CompressWithLz4HC等 // 2. 生成资源包配置 AssetPackConfig assetPackConfig CreateYourAssetPackConfig(); // 调用你自己的配置逻辑 // 3. 执行构建 string buildError Bundletool.BuildBundle(buildOptions, assetPackConfig); if (string.IsNullOrEmpty(buildError)) { Debug.Log(AAB with Asset Packs built successfully: buildOptions.locationPathName); } else { Debug.LogError(Build failed: buildError); } }使用Bundletool.BuildBundle的好处是它绕过了编辑器菜单的构建流程直接将配置与构建动作绑定非常适合集成到自动化脚本或命令行构建中。4. 完整实战批量配置资源包脚本下面我将展示一个完整的、可投入生产的编辑器脚本。它实现了从一个根目录自动扫描所有AssetBundle并按子目录结构自动创建资源包的功能。4.1 脚本结构设计我们创建一个PADBatchConfigurator类它提供以下功能指定AssetBundle的输出根目录。定义分发模式映射规则例如Base文件夹下的包用InstallTimeLevels下的用OnDemand。执行扫描和配置生成。提供编辑器菜单项来执行配置和构建。4.2 完整代码实现// PADBatchConfigurator.cs using UnityEngine; using UnityEditor; using Google.Android.AppBundle.Editor; using System.Collections.Generic; using System.IO; using System.Linq; public static class PADBatchConfigurator { // 配置参数AssetBundles的输出目录相对于项目根目录 private const string ASSET_BUNDLE_ROOT Assets/AssetBundles/Android; // 配置参数分发模式规则。Key为目录名小写Value为对应的分发模式。 private static readonly Dictionarystring, AssetPackDeliveryMode DeliveryModeRules new Dictionarystring, AssetPackDeliveryMode { { base, AssetPackDeliveryMode.InstallTime }, // 基础资源安装时必备 { shared, AssetPackDeliveryMode.FastFollow }, // 公共资源安装后尽快下载 { levels, AssetPackDeliveryMode.OnDemand }, // 关卡资源按需下载 { characters, AssetPackDeliveryMode.OnDemand }, // 角色资源按需下载 // 可以继续添加更多规则 }; // 配置参数默认分发模式当目录不匹配任何规则时使用 private const AssetPackDeliveryMode DEFAULT_DELIVERY_MODE AssetPackDeliveryMode.OnDemand; [MenuItem(Custom PAD/1. Generate Asset Pack Config)] public static void GenerateAssetPackConfig() { Debug.Log(开始扫描并生成Asset Pack配置...); if (!Directory.Exists(ASSET_BUNDLE_ROOT)) { Debug.LogError($AssetBundle根目录不存在: {ASSET_BUNDLE_ROOT}。请先构建AssetBundle。); return; } AssetPackConfig config new AssetPackConfig(); int packCount 0; int bundleCount 0; // 获取根目录下所有第一级子目录 string[] subDirectories Directory.GetDirectories(ASSET_BUNDLE_ROOT); foreach (string dirPath in subDirectories) { string dirName Path.GetFileName(dirPath).ToLower(); AssetPackDeliveryMode deliveryMode GetDeliveryModeForDirectory(dirName); // 获取该目录下所有的.assetbundle文件 string[] bundleFiles Directory.GetFiles(dirPath, *.assetbundle, SearchOption.AllDirectories); if (bundleFiles.Length 0) { Debug.LogWarning($目录 {dirName} 下未找到任何.assetbundle文件已跳过。); continue; } // 为这个目录创建一个资源包包名使用目录名 string packName $pack_{dirName}; // 添加前缀避免冲突确保包名合法 foreach (string bundlePath in bundleFiles) { // 路径需要是相对于项目根目录的路径 string relativePath bundlePath.Replace(\\, /); config.AddAssetBundle(packName, relativePath); bundleCount; } // 为该资源包设置分发模式 config.SetAssetPackDeliveryMode(packName, deliveryMode); packCount; Debug.Log($已配置资源包: {packName}模式: {deliveryMode}包含 {bundleFiles.Length} 个AssetBundle。); } // 保存配置供编辑器菜单构建使用 bool saveSuccess AssetPackConfigSerializer.SaveConfig(config); if (saveSuccess) { Debug.Log($配置生成完成共创建 {packCount} 个资源包包含 {bundleCount} 个AssetBundle。配置已保存。); // 可选将配置也保存一份到Assets目录下便于版本管理 SaveConfigToAssets(config); } else { Debug.LogError(保存AssetPackConfig失败); } } [MenuItem(Custom PAD/2. Build AAB (With Current Config))] public static void BuildAndroidAppBundle() { // 此菜单项会直接触发Unity编辑器标准的PAD构建流程它将自动读取上一步SaveConfig保存的配置。 EditorApplication.ExecuteMenuItem(Google/Build Android App Bundle); } [MenuItem(Custom PAD/3. Clean Build AAB (Full Automation))] public static void CleanAndBuildAAB() { // 完整的自动化构建流程示例 Debug.Log(开始全自动构建流程...); // 步骤1: 清理旧的AssetBundle根据你的项目构建流程决定 // CleanAssetBundles(); // 步骤2: 构建AssetBundle假设你有一个构建AssetBundle的脚本 // BuildAllAssetBundles(); // 步骤3: 生成PAD配置 GenerateAssetPackConfig(); // 步骤4: 准备构建选项 BuildPlayerOptions buildOptions new BuildPlayerOptions(); buildOptions.scenes EditorBuildSettings.scenes.Where(s s.enabled).Select(s s.path).ToArray(); buildOptions.locationPathName $Builds/Android/{PlayerSettings.productName}_{PlayerSettings.bundleVersion}.aab; buildOptions.target BuildTarget.Android; buildOptions.options BuildOptions.None; // 如果开发调试可以加上 BuildOptions.Development // buildOptions.options BuildOptions.Development | BuildOptions.AllowDebugging; // 步骤5: 重新加载刚刚生成的配置因为SaveConfig后我们需要一个对象传给Bundletool AssetPackConfig config AssetPackConfigSerializer.LoadConfig(); if (config null) { Debug.LogError(未能加载AssetPackConfig构建终止。); return; } // 步骤6: 使用Bundletool直接构建AAB string buildError Bundletool.BuildBundle(buildOptions, config); if (string.IsNullOrEmpty(buildError)) { Debug.Log($AAB构建成功路径: {Path.GetFullPath(buildOptions.locationPathName)}); // 可选自动打开输出目录 EditorUtility.RevealInFinder(buildOptions.locationPathName); } else { Debug.LogError($AAB构建失败: {buildError}); } } /// summary /// 根据目录名获取对应的分发模式。 /// /summary private static AssetPackDeliveryMode GetDeliveryModeForDirectory(string dirName) { foreach (var rule in DeliveryModeRules) { if (dirName.Contains(rule.Key)) { return rule.Value; } } return DEFAULT_DELIVERY_MODE; } /// summary /// 将配置序列化为JSON保存到Assets目录便于版本控制非必需但推荐。 /// /summary private static void SaveConfigToAssets(AssetPackConfig config) { // 注意AssetPackConfig 没有提供直接的序列化方法。 // 这里我们只是保存一个我们自定义的规则摘要或元数据文件用于记录本次配置的快照。 // 真正的配置已被AssetPackConfigSerializer保存在Library等临时目录。 string infoPath Assets/Editor/AssetPackConfig_Info.json; var summary new { GeneratedTime System.DateTime.Now.ToString(yyyy-MM-dd HH:mm:ss), AssetBundleRoot ASSET_BUNDLE_ROOT, RuleCount DeliveryModeRules.Count, DefaultMode DEFAULT_DELIVERY_MODE.ToString() }; string json JsonUtility.ToJson(summary, true); File.WriteAllText(infoPath, json); AssetDatabase.Refresh(); Debug.Log($配置摘要已保存至: {infoPath}); } }4.3 脚本使用说明安装依赖确保你的Unity项目已通过Package Manager或.unitypackage正确安装了Google Play Asset Delivery插件。这会在项目中引入必要的Google.Android.AppBundle.Editor等程序集。放置脚本将上面的PADBatchConfigurator.cs脚本放在项目的Assets/Editor/目录下或任何名为Editor的文件夹中。构建AssetBundle在运行脚本前你需要确保Assets/AssetBundles/Android/目录下已经存在构建好的AssetBundle文件并且按你预设的目录结构如base,levels等组织好。你可以使用Unity自带的AssetBundle构建管线或Addressables来构建它们。执行配置在Unity编辑器顶部菜单栏你会看到Custom PAD菜单。点击1. Generate Asset Pack Config控制台会输出扫描和配置结果。执行构建点击2. Build AAB (With Current Config)会调用Unity原生的构建菜单使用上一步生成的配置。点击3. Clean Build AAB (Full Automation)会执行一个完整的、从生成配置到调用Bundletool.BuildBundle输出AAB的一键式流程。这是推荐的生产环境构建方式。5. 高级技巧与避坑指南在实际项目中使用代码配置PAD你可能会遇到一些官方文档没有详细说明的“坑”。这里分享一些关键的经验和技巧。5.1 资源包命名与大小限制命名规范资源包名称只能包含小写字母a-z、数字0-9和下划线_且必须以字母开头。我建议像示例中那样加一个pack_前缀避免目录名以数字开头导致错误。包大小限制单个资源包的大小没有明确上限但Google Play对通过移动网络下载的包有200MB的警告阈值。超过此大小下载前会要求用户确认。对于FastFollow和OnDemand包强烈建议进行合理的分包单个包最好控制在150MB以内以优化下载体验。总大小限制一个应用的所有资源包包括基础APK总大小不能超过Google Play的发布限制目前通常是150MB用于APK通过PAD可以扩展到很大但需合理规划。5.2 处理AssetBundle依赖这是最容易出错的地方。假设level1.assetbundle依赖于shared_assets.assetbundle中的材质。如果你把它们放到了不同的资源包比如pack_levels和pack_shared中并且pack_shared是OnDemand的而pack_levels是InstallTime的那么在安装后立即加载level1时可能会因为依赖的shared_assets尚未下载而失败。解决方案依赖分析在构建AssetBundle时使用BuildAssetBundleOptions.DeterministicAssetBundle选项并确保依赖关系明确。你可以编写脚本在生成PAD配置前分析AssetBundle之间的依赖图。依赖提升将具有依赖关系的AssetBundle放置到同一个资源包中。这样它们会作为一个整体被下载和管理。在示例脚本中我们按目录分包一个目录下的所有bundle都在一个pack里这天然解决了同目录bundle间的依赖问题。安装时包含核心依赖确保所有InstallTime包所依赖的资源其本身也必须是InstallTime的或者被包含在基础APK中。对于跨包的复杂依赖最稳妥的方式是将所有有相互依赖关系的bundle合并到一个更大的、采用合适分发模式的资源包中。5.3 纹理压缩格式定位如果你的游戏需要支持不同GPU架构的设备如ARMv7, ARM64, x86并且使用了特定格式的纹理如ASTC, ETC2你需要配置纹理压缩格式定位。这可以通过代码设置AssetPackConfig的TextureCompressionFormat属性来实现。// 为特定的资源包设置纹理压缩格式 config.SetTextureCompressionFormat(pack_high_res_textures, TextureCompressionFormat.Astc); // 或者为所有资源包设置默认格式 // config.DefaultTextureCompressionFormat TextureCompressionFormat.Default;在构建AAB时BundleTool会为每种指定的格式生成对应的APKGoogle Play会根据设备GPU分发明正确的版本。5.4 在CI/CD流水线中集成对于自动化构建你需要确保批处理模式使用-batchmode -quit -executeMethod命令行参数来调用Unity并执行我们编写的PADBatchConfigurator.CleanAndBuildAAB方法。Unity.exe -batchmode -quit -projectPath C:\YourProject -executeMethod PADBatchConfigurator.CleanAndBuildAAB -logFile build.log环境一致性确保构建服务器上的Unity版本、JDK、SDK、NDK版本与开发环境一致特别是Google Play Asset Delivery插件的版本。错误处理在构建脚本中增加更完善的日志记录和错误处理将构建结果成功/失败以及关键日志输出到CI系统的控制台便于排查。5.5 运行时API调用注意事项配置好资源包只是第一步在游戏运行时正确调用PAD API来下载和加载它们同样重要。这里补充几点关键提示异步操作PlayAssetDelivery.RetrieveAssetBundleAsync是异步操作务必使用协程Coroutine或async/await需Unity 2017.4并启用 .NET 4.x进行等待避免阻塞主线程。状态检查在下载过程中持续检查PlayAssetBundleRequest.Status和DownloadProgress。对于WaitingForWifi和RequiresUserConfirmation状态必须按照文档提示处理显示对话框或等待用户操作。错误处理务必检查bundleRequest.Error。常见的错误有AssetDeliveryErrorCode.NetworkError网络问题、InsufficientStorage存储空间不足、Canceled请求被取消等需要给用户友好的提示。内存管理通过bundleRequest.AssetBundle加载到内存后记得在适当的时机如场景切换后使用AssetBundle.Unload(true)来卸载释放内存。对于已下载但暂时不需要的按需资源包可以考虑调用PlayAssetDelivery.RemoveAssetPack来删除磁盘文件但下次使用需重新下载。6. 常见问题排查实录即使按照上述步骤操作在实际部署中仍可能遇到问题。下面记录了一些典型问题及其解决方法。问题1构建成功但安装后游戏崩溃日志显示找不到AssetBundle。可能原因A运行时请求的资源包名称与构建时配置的名称不匹配。检查确保PlayAssetDelivery.RetrieveAssetBundleAsync(pack_levels)中的pack_levels与AssetPackConfig中设置的包名完全一致大小写敏感但包名应全小写。可能原因BAssetBundle本身构建时使用的构建目标BuildTarget不对。检查确保构建AssetBundle时选择的平台是Android而不是Standalone或其他。可能原因C资源包没有成功打入AAB。排查使用bundletool命令行工具解压生成的.aab文件检查asset.pb文件或BUNDLE-METADATA中是否存在你配置的资源包信息。java -jar bundletool.jar dump resources --bundleyour_app.aab问题2在编辑器菜单执行构建后AAB文件体积异常小似乎没有包含资源包。可能原因AssetPackConfigSerializer.SaveConfig(config)调用后配置没有被正确应用到接下来的构建中。解决优先使用Bundletool.BuildBundle(buildOptions, config)方法进行构建这是最可靠的方式。如果必须用编辑器菜单确保在点击Google Build Android App Bundle前没有其他操作清空了临时配置。问题3下载资源包时进度一直卡在0%最后返回错误。可能原因A设备没有安装Google Play服务或版本过低。PAD功能依赖Google Play服务。检查在真机上测试并确保Google Play服务已更新。可能原因B测试时使用了内部测试轨道但上传的AAB没有发布到该轨道版本。解决确保你下载安装的测试版本其版本号Version Code与你在Play控制台该测试轨道上传的包含资源包的AAB版本号一致。可能原因C网络问题或防火墙限制。尝试切换网络或使用Android模拟器的原生API级别镜像带Google Play服务进行测试。问题4我想动态更新资源包内的内容而不发布新的应用版本。解答Play Asset Delivery不支持动态更新已发布资源包内的文件内容。如果你需要更新资源必须发布新版本的应用增加Version Code并上传新的AAB。资源包版本与包含它的应用版本绑定。这是与AssetBundle热更新方案的主要区别设计资源分发策略时需要提前考虑。通过这套代码化的配置方案我们不仅摆脱了编辑器插件的卡顿更重要的是获得了对构建流程的完全掌控力。它将资源包的配置从一次性的、易出错的手动操作转变为可重复、可版本控制、可集成的自动化步骤。对于需要频繁构建、资源量大的项目而言这种效率提升和稳定性保障是至关重要的。