
1. 项目概述与核心痛点最近在项目里折腾Unity的Addressable系统发现一个挺有意思但又容易踩坑的需求如何把通过Addressable打包出来的资源直接保存在项目工程目录里而不是一股脑全扔到构建输出路径比如Library或StreamingAssets里。乍一听这需求有点反直觉Addressable的设计初衷不就是把资源从项目里剥离出去实现动态加载和热更新吗干嘛还要“保存”回项目里但实际开发中这种需求还真不少见。比如你正在开发一个编辑器工具或者插件需要生成一些预制体、材质球或者配置表这些资源后续还要在编辑器里被其他流程引用或修改。又或者你的团队有一套基于Git的版本管理流程希望某些通过脚本或工具动态生成的Addressable资源也能被纳入版本控制方便团队成员同步。再比如你只是想临时把打包出来的资源文件放到一个指定的项目子目录下进行归档或备份方便后续查找和复用。这个需求的核心其实是在利用Addressable强大的依赖分析和打包能力的同时又希望保留对产出物AssetBundle文件及其清单的“项目内”控制权。它考验的是我们对Addressable底层工作流和Unity AssetDatabase的理解。网上相关的讨论比较零散很多教程只讲了怎么打包到远程服务器或者默认的本地路径对于“回流”到项目内部这个场景往往一笔带过。我自己也是踩了几个坑才把流程理顺今天就把完整的思路、配置方法和避坑经验分享出来。2. Addressable资源打包流程深度解析2.1 Addressable系统基础与打包目标要理解如何将资源“保存”在项目中首先得清楚Addressable标准的打包流程和输出目标。Addressable Asset System是Unity官方推出的新一代资源管理系统它核心解决的是资源依赖管理、按需加载和分发本地/远程的问题。当你点击Build New Build Default Build Script时Addressable会执行一系列操作资源分析扫描所有标记为Addressable的资源分析它们之间的依赖关系比如Prefab引用的Texture、Material。分组与打包根据你在Addressable Groups窗口中设置的策略如按标签、依赖关系、显式分组将资源打包成一个或多个AssetBundle文件.bundle。生成清单创建关键的JSON格式清单文件主要是catalog.json或settings.json它记录了每个资源的唯一地址Address、所在的AssetBundle、依赖信息以及加载路径Load Path。输出到构建路径默认情况下这些生成的AssetBundle文件和清单文件会被输出到你在Addressable Asset Settings中Build Path和Load Path所指定的位置。对于本地构建Local Build这个路径通常是项目内的一个临时或构建专用目录例如[ProjectRoot]/Library/com.unity.addressables/aa/[Platform]。这里的关键在于Build Path和Load Path。Build Path决定了打包时文件被写入到哪里而Load Path决定了运行时或在编辑器下使用Addressables.LoadAssetAsync时系统去哪里寻找这些文件。默认的Local模式这两个路径通常指向项目内较深的、不纳入版本控制的目录。我们的目标就是改变这个输出目的地让它指向一个我们自定义的、位于项目Assets目录下的某个文件夹。2.2 自定义构建路径的核心Scriptable Build ScriptAddressable系统的强大之处在于其高度的可扩展性。整个构建流程是由一系列可脚本化的“构建脚本”Scriptable Build Script驱动的。我们要干预输出路径最核心、最标准的方法就是创建一个自定义的构建脚本。Unity提供了IDataBuilder接口和BuildScriptBase基类允许我们介入构建过程。对于修改输出路径我们主要关注的是BuildScriptPackedMode这个类它处理本地打包模式但更直接的是继承BuildScriptBase并重写关键方法。创建一个新的C#脚本例如CustomBuildScriptWithProjectPath.cs。核心思路是重写BuildDataImplementation方法在其中修改构建上下文AddressableAssetsBuildContext中的相关路径变量。using UnityEditor.AddressableAssets.Build; using UnityEditor.AddressableAssets.Build.DataBuilders; using UnityEditor.AddressableAssets.Settings; using UnityEditor.AddressableAssets.Settings.GroupSchemas; using UnityEngine; using System.IO; [CreateAssetMenu(fileName CustomBuildToProjectPath.asset, menuName Addressables/Custom Builders/Custom Build to Project Path)] public class CustomBuildScriptWithProjectPath : BuildScriptBase { // 自定义的项目内保存路径相对于项目根目录 public string projectRelativeOutputPath Assets/AddressableBuilds; public override string Name { get { return Custom Build to Project Path; } } protected override TResult BuildDataImplementationTResult(AddressableAssetsBuildContext context) { // 1. 首先调用父类或基础构建流程完成资源打包和Bundle生成。 // 这里为了简化我们假设使用默认的打包逻辑但修改输出目录。 // 在实际复杂场景中可能需要更精细地控制。 // 一个更实用的方法是先使用默认的构建流程生成到临时目录 // 然后再将文件复制到目标项目路径。 var tempPath Path.Combine(Application.dataPath, .., TempAddressableBuild); var originalBuildPath context.Settings.profileSettings.GetValueByName(context.Settings.activeProfileId, LocalBuildPath); var originalLoadPath context.Settings.profileSettings.GetValueByName(context.Settings.activeProfileId, LocalLoadPath); // 临时修改构建和加载路径到临时目录避免污染原设置 context.Settings.profileSettings.SetValue(context.Settings.activeProfileId, LocalBuildPath, tempPath); context.Settings.profileSettings.SetValue(context.Settings.activeProfileId, LocalLoadPath, tempPath); try { // 调用默认的PackedMode构建脚本或你选择的基础脚本 var defaultBuilder CreateInstanceBuildScriptPackedMode(); var result defaultBuilder.BuildDataTResult(context); // 2. 构建完成后将文件从临时目录复制到项目目标路径 string targetFullPath Path.GetFullPath(Path.Combine(Application.dataPath, .., projectRelativeOutputPath)); if (Directory.Exists(tempPath)) { // 清空或创建目标目录 if (Directory.Exists(targetFullPath)) Directory.Delete(targetFullPath, true); Directory.CreateDirectory(targetFullPath); // 复制所有文件 foreach (var file in Directory.GetFiles(tempPath, *, SearchOption.AllDirectories)) { var relativePath file.Substring(tempPath.Length 1); var destPath Path.Combine(targetFullPath, relativePath); var destDir Path.GetDirectoryName(destPath); if (!Directory.Exists(destDir)) Directory.CreateDirectory(destDir); File.Copy(file, destPath, true); } Debug.Log($Addressable构建文件已复制到项目路径: {targetFullPath}); // 3. 刷新AssetDatabase让Unity编辑器识别新导入的资源文件.bundle UnityEditor.AssetDatabase.Refresh(); // 4. 可选将复制到Assets下的.bundle文件的导入类型设置为“AssetBundle” // 这能防止Unity错误地尝试将其作为普通资源导入。 string targetAssetPath Path.Combine(Assets, projectRelativeOutputPath.Replace(Assets/, )); SetAssetBundleImportSettings(targetAssetPath); } // 恢复原始路径设置 context.Settings.profileSettings.SetValue(context.Settings.activeProfileId, LocalBuildPath, originalBuildPath); context.Settings.profileSettings.SetValue(context.Settings.activeProfileId, LocalLoadPath, originalLoadPath); return result; } catch (System.Exception e) { // 发生异常时也要恢复路径 context.Settings.profileSettings.SetValue(context.Settings.activeProfileId, LocalBuildPath, originalBuildPath); context.Settings.profileSettings.SetValue(context.Settings.activeProfileId, LocalLoadPath, originalLoadPath); Debug.LogError($自定义构建失败: {e.Message}); throw; } } void SetAssetBundleImportSettings(string assetFolderPath) { // 查找所有.bundle文件 var bundleGuids UnityEditor.AssetDatabase.FindAssets(t:Object, new[] { assetFolderPath }); foreach (var guid in bundleGuids) { var path UnityEditor.AssetDatabase.GUIDToAssetPath(guid); if (path.EndsWith(.bundle)) { var importer UnityEditor.AssetImporter.GetAtPath(path); if (importer ! null) { // 将AssetBundle的Variant标记设为非空Unity会将其识别为AssetBundle文件。 // 或者更简单地确保其AssetBundleName不为空。 importer.assetBundleName custom_bundle; // 可以设一个通用名或根据情况设置 importer.SaveAndReimport(); } } } } }注意上述示例是一个原理性演示采用了“构建到临时目录再复制”的稳妥策略。在实际生产环境中你需要更严谨地处理错误、路径格式确保是Unity可识别的相对路径Assets/...、以及可能涉及的清单文件路径更新。更优雅的做法是直接继承并修改BuildScriptPackedMode中计算输出路径的逻辑。创建好这个ScriptableObject后你需要在Addressable Asset Settings的Build and Play Mode Scripts列表中将新建的CustomBuildToProjectPath脚本拖进去。然后在构建时选择这个自定义的构建脚本而不是默认的Default Build Script。2.3 配置Profile与路径变量除了自定义脚本另一种相对轻量级的方法是巧妙配置Addressable的Profile配置文件。Profile允许你为不同环境开发、测试、生产定义不同的路径变量。打开Addressables窗口进入SettingsProfiles。你可以编辑或新建一个Profile。关注两个关键变量[UnityEngine.AddressableAssets.Addressables.BuildPath] 构建路径。[UnityEngine.AddressableAssets.Addressables.LoadPath] 加载路径。你可以将它们设置为项目内的路径例如Build Path:{UnityEngine.Application.dataPath}/../MyProjectAddressables/BuildLoad Path:{UnityEngine.Application.dataPath}/../MyProjectAddressables/Build注意{UnityEngine.Application.dataPath}对应的是Assets文件夹的绝对路径。/../表示上一级目录所以{UnityEngine.Application.dataPath}/../就是项目根目录。上面的例子会把文件输出到项目根目录下的MyProjectAddressables/Build文件夹。如果你想放在Assets文件夹内可以设为{UnityEngine.Application.dataPath}/AddressableBuilds。但是强烈不建议将构建输出直接放在Assets或其子目录下因为Unity会尝试导入这些.bundle文件可能导致意外行为。放在项目根目录的同级或子目录是更安全的选择。保存Profile并在Addressable Asset Settings的Profile下拉框中选择它。这种方法简单直接但有一个重大限制当你构建完成后需要手动将输出目录如MyProjectAddressables移动到Assets目录下并刷新AssetDatabaseUnity才能真正将其识别为项目资源。而且移动后加载路径也需要对应更新否则运行时找不到。因此Profile配置法更适合半自动化的流程或者作为自定义构建脚本中路径设定的来源。3. 实操将打包资源整合回项目工作流3.1 创建与管理项目内的资源缓存目录明确了方法后我们来规划一个具体的实操流程。目标是构建完成后Addressable资源自动出现在Assets/ExternalAddressableAssets目录下并且能正确被Unity编辑器管理和被运行时加载。第一步规划目录结构在项目Assets目录下创建一个清晰的文件夹结构例如Assets/ ├── ExternalAddressableAssets/ # 主目录 │ ├── Bundles/ # 存放所有 .bundle 文件 │ │ ├── windows/ │ │ ├── android/ │ │ └── ... │ ├── Catalogs/ # 存放 catalog.json 等清单文件 │ └── SettingsOverride/ # 可选存放覆盖的运行时设置这种结构将资源文件与清单分离便于管理。注意.bundle文件是二进制资源包而.json是文本清单。第二步编写后处理脚本我们采用“默认构建 后处理复制”的方案因为它更稳定对Addressable内部流程侵入最小。创建一个编辑器脚本PostprocessAddressableBuild.cs并使用UnityEditor.Build.IPostprocessBuildWithReport接口。using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; using System.IO; using UnityEditor; public class PostprocessAddressableBuild : IPostprocessBuildWithReport { // 指定回调顺序在Addressable构建完成后执行 public int callbackOrder { get { return 1000; } } public void OnPostprocessBuild(BuildReport report) { // 仅处理包含Addressable本地构建的情况可通过判断输出目录是否存在特定文件来触发 // 这里我们假设有一个标识文件或直接检查默认输出目录 string defaultBuildOutputPath Path.Combine(Application.dataPath, .., Library/com.unity.addressables/aa); // 检查这个路径是否存在且包含构建平台目录 string targetPlatformPath Path.Combine(defaultBuildOutputPath, EditorUserBuildSettings.activeBuildTarget.ToString()); if (!Directory.Exists(targetPlatformPath)) { // 可能不是Addressable构建或路径不同直接返回 return; } // 定义项目内目标路径 string projectTargetRoot Path.Combine(Application.dataPath, ExternalAddressableAssets); string projectTargetBundlesPath Path.Combine(projectTargetRoot, Bundles, EditorUserBuildSettings.activeBuildTarget.ToString()); string projectTargetCatalogsPath Path.Combine(projectTargetRoot, Catalogs); // 清空旧资源 if (Directory.Exists(projectTargetBundlesPath)) Directory.Delete(projectTargetBundlesPath, true); if (Directory.Exists(projectTargetCatalogsPath)) Directory.Delete(projectTargetCatalogsPath, true); Directory.CreateDirectory(projectTargetBundlesPath); Directory.CreateDirectory(projectTargetCatalogsPath); // 复制AssetBundle文件 CopyDirectoryContents(Path.Combine(targetPlatformPath, Bundles), projectTargetBundlesPath, *.bundle); // 复制清单文件catalog.json, settings.json等 CopyDirectoryContents(targetPlatformPath, projectTargetCatalogsPath, *.json); // 可能还有其他.hash文件等 CopyDirectoryContents(targetPlatformPath, projectTargetCatalogsPath, *.hash); Debug.Log($Addressable构建产物已复制到: {projectTargetRoot}); // 关键步骤刷新AssetDatabase并设置.bundle文件的Importer AssetDatabase.Refresh(); SetBundleImporters(projectTargetBundlesPath); // 可选生成或更新一个运行时加载路径的配置文件指向新的Assets内路径 UpdateRuntimeLoadPathConfig(projectTargetRoot); } void CopyDirectoryContents(string sourceDir, string targetDir, string searchPattern *) { if (!Directory.Exists(sourceDir)) return; var files Directory.GetFiles(sourceDir, searchPattern); foreach (var file in files) { var fileName Path.GetFileName(file); var destPath Path.Combine(targetDir, fileName); File.Copy(file, destPath, true); } // 如果需要递归子目录可以在这里处理 // var dirs Directory.GetDirectories(sourceDir); // foreach (var dir in dirs) { ... } } void SetBundleImporters(string bundleFolderAssetPath) { // 将路径从绝对路径转换为相对于Assets的路径 string relativePath Assets bundleFolderAssetPath.Replace(Application.dataPath, ); var assetGUIDs AssetDatabase.FindAssets(, new[] { relativePath }); foreach (var guid in assetGUIDs) { string assetPath AssetDatabase.GUIDToAssetPath(guid); if (assetPath.EndsWith(.bundle)) { var importer AssetImporter.GetAtPath(assetPath); if (importer ! null) { // 设置assetBundleName这会让Unity将其识别为AssetBundle。 // 可以设置为一个固定的虚拟包名或者根据原包名设置。 // 这里设置为文件名不含扩展名作为包名。 string bundleName Path.GetFileNameWithoutExtension(assetPath); importer.assetBundleName bundleName; // 设置assetBundleVariant可以进一步控制这里留空。 importer.assetBundleVariant ; importer.SaveAndReimport(); } } } } void UpdateRuntimeLoadPathConfig(string projectTargetRoot) { // 这是一个高级步骤你可能需要创建一个ScriptableObject或JSON配置文件 // 在游戏启动时告诉Addressables系统从新的Assets内路径加载。 // 因为默认的Load Path是构建时设置的复制到Assets后路径变了。 // 一种方法是在初始化Addressables之前修改其运行时设置。 // 示例创建一个配置在游戏启动脚本中读取并应用。 string configPath Path.Combine(projectTargetRoot, RuntimeLoadPath.asset); // ... 创建或更新配置资源的代码 ... } }这个脚本会在每次构建包括常规的Player构建完成后执行。它会检查默认的Addressable构建输出目录如果发现新构建的文件就将其复制到我们预设的Assets/ExternalAddressableAssets目录下并正确设置文件的导入器属性。3.2 处理资源依赖与AssetDatabase刷新将.bundle文件复制到Assets目录下后Unity的AssetDatabase会检测到新文件并尝试导入。对于.bundle文件Unity默认可能不知道如何处理。这就是为什么在SetBundleImporters方法中我们要主动设置assetBundleName属性。assetBundleName的作用当你在一个资源的导入器Importer上设置了这个属性Unity就会将该文件标记为一个AssetBundle。这带来两个好处在编辑器里你能在Project窗口看到这些文件被特殊标记通常会有个捆扎的小图标方便识别。更重要的是这能防止Unity错误地尝试将其作为纹理、模型等普通资源进行导入解析避免产生错误日志或元文件冲突。刷新时机AssetDatabase.Refresh()必须在文件复制操作之后立即调用。这个操作是同步的会阻塞主线程直到完成。对于包含大量资源的大构建可能会引起编辑器短暂的卡顿这是正常现象。务必确保在刷新完成后再进行后续的导入器设置操作。一个常见的坑如果你在复制文件后没有正确设置.bundle文件的assetBundleNameUnity可能会为这些文件生成.meta文件但将其类型识别为DefaultAsset。这本身不会导致运行时错误但会让项目管理变得混乱并且如果你不小心移动或删除了.meta文件可能会导致资源丢失的警告。3.3 运行时加载路径的修正策略这是整个流程中最关键也最棘手的一环。Addressable系统在运行时包括编辑器播放模式是根据构建时生成的catalog.json中记录的LoadPath来定位资源文件的。现在我们把文件从原来的构建输出目录如Library/...移动到了Assets/ExternalAddressableAssets/...加载路径就失效了。有几种策略来解决这个问题策略一构建后修改Catalog推荐但较复杂在复制文件到项目目录后直接修改catalog.json文件将其中的m_InternalIds内部ID即加载路径批量替换为新的路径。新的路径需要使用file://协议或Assets/...这样的相对路径在编辑器下。例如将Library/com.unity.addressables/aa/Windows/bundles/assetbundle.bundle替换为Assets/ExternalAddressableAssets/Bundles/Windows/assetbundle.bundle。你需要编写一个脚本来解析JSON进行字符串替换。注意catalog文件可能有哈希校验直接修改可能导致校验失败。Addressable系统在加载catalog时会计算哈希如果不对则会报错。你可能需要同时更新对应的.hash文件或者更稳妥的方法是在构建脚本中就直接指定最终的加载路径。策略二使用Addressables运行时API动态重定向灵活需编码在游戏初始化时Addressables.InitializeAsync之前或之后通过代码覆盖默认的加载路径。Addressables 提供了ResourceManager来管理加载器。你可以实现一个自定义的IResourceProvider或修改现有的BundledAssetProvider的路径解析逻辑。一个更简单的方式是利用Addressables.RuntimePath属性或通过AddressableAssetSettings在运行时修改。但请注意这些API可能不是公开的或者用法在版本间有变化。一个相对稳定的方法是使用Addressables.LoadContentCatalogAsync加载你修改后的catalog文件保存在项目Assets目录下的那个并指定其对应的依赖路径即Bundle所在的项目内路径。// 在游戏启动脚本中 using UnityEngine.AddressableAssets; using UnityEngine.ResourceManagement.ResourceProviders; using UnityEngine; public class AddressablePathOverride : MonoBehaviour { [SerializeField] private string customCatalogPathInAssets Assets/ExternalAddressableAssets/Catalogs/catalog.json; [SerializeField] private string customBundleFolderPathInAssets Assets/ExternalAddressableAssets/Bundles; async void Start() { // 先初始化默认的Addressables可能会失败因为路径不对 // 更好的做法是直接加载我们自定义的Catalog var catalogLocation new ResourceLocationBase( CustomCatalog, customCatalogPathInAssets, typeof(ContentCatalogProvider).FullName, typeof(object) ); // 加载自定义Catalog并指定其依赖的根路径 var loadResult await Addressables.LoadContentCatalogAsync(customCatalogPathInAssets, false).Task; if (loadResult ! null) { Debug.Log(自定义Catalog加载成功); // 现在Addressables会使用新Catalog中定义的路径应该是我们修改过的来加载资源 } else { Debug.LogError(自定义Catalog加载失败); } } }这种方法要求你事先已经修改好了catalog文件中的路径。它提供了最大的灵活性但需要处理catalog的修改和加载逻辑。策略三使用Symlink符号链接仅限特定环境这是一个“取巧”的办法不移动文件而是在项目目录下创建一个指向原始构建输出目录的符号链接Symbolic Link。这样文件物理上还在原处但在Unity项目视图中看起来像是在项目目录里。同时运行时加载路径仍然是原始路径所以不会出错。Windows (命令提示符管理员模式):mklink /J D:\YourProject\Assets\LinkedAddressables D:\YourProject\Library\com.unity.addressables\aa\WindowsmacOS/Linux (终端):ln -s /path/to/your/project/Library/com.unity.addressables/aa/StandaloneOSX /path/to/your/project/Assets/LinkedAddressables这种方法几乎零成本但有几个缺点1) 符号链接可能不被所有版本控制系统良好支持Git可以但需要小心。2) 团队成员需要各自创建符号链接。3) 清理构建时需要注意不要误删链接指向的真实文件。4. 常见问题、排查技巧与进阶优化4.1 构建与加载失败问题排查在实施上述流程时你几乎一定会遇到各种构建失败、加载失败的问题。下面是一个快速排查清单问题现象可能原因排查步骤与解决方案构建成功但文件未复制到项目目录1. 后处理脚本未执行。2. 源目录路径不正确。3. 脚本执行顺序问题。1. 检查脚本是否继承了正确的接口IPostprocessBuildWithReport并放在Editor文件夹。2. 在脚本中添加Debug.Log输出源目录和目标目录的完整路径确认路径存在且正确。3. 检查callbackOrder确保在Addressable构建完成后执行。文件已复制但Unity中显示为“未知类型”或无图标.bundle文件的AssetImporter未正确设置assetBundleName。1. 在Project窗口选中该文件在Inspector查看其Import Settings。2. 确保AssetBundle Name字段不为空。如果为空运行SetBundleImporters脚本或手动赋值。3. 右键文件选择Reimport。编辑器播放模式加载资源失败报错“Invalid path”或“Unable to load asset”运行时加载路径Load Path仍然是旧的构建路径而非新的项目内路径。1. 打开复制到项目内的catalog.json用文本编辑器搜索m_InternalIds或InternalId查看其值是否指向Assets/ExternalAddressableAssets/...。2. 如果路径不对需要实施3.3节的路径修正策略。3. 临时测试可以尝试在代码中使用Addressables.LoadAssetAsync时直接使用Assets/...路径的地址如果该资源地址是直接路径类型。构建Player后运行时加载失败1. 项目内的资源未正确包含在Player构建中。2. 移动端平台iOS/Android对Assets目录下的.bundle文件访问权限或路径处理不同。1. 确保.bundle文件及其.meta文件被包含在构建中通常位于Assets目录下的资源都会自动包含。2. 对于移动平台Assets目录在安装后是只读的。确保你的加载路径使用的是StreamingAssets路径Application.streamingAssetsPath或可写路径。将资源放在Assets目录下构建后会被打包进APK/IPA运行时是只读的这符合热更新前的基础资源分发场景。3. 使用Addressables.InitializeAsync的返回值或事件来检查初始化是否成功。“Catalog hash mismatch” 错误修改了catalog.json文件但没有更新对应的.hash文件或者更新的哈希值计算不正确。1. 不要手动修改哈希文件。最安全的方式是让Addressable系统在构建时直接生成正确的路径和哈希。2. 考虑使用**策略三符号链接**避免修改catalog。3. 如果必须修改需要按照Addressable计算哈希的相同算法通常是CRC或MD5重新计算修改后catalog文件的哈希并更新.hash文件。这比较繁琐容易出错。4.2 性能考量与版本管理建议性能考量构建时间增加文件复制和后处理步骤会略微增加整体构建时间。对于大型项目复制数GB的AssetBundle文件可能需要几十秒。编辑器内存将AssetBundle放在Assets目录下Unity编辑器在导入时会为其创建内存中的表示虽然.bundle本身不作为可直接编辑的资源。如果数量极多可能会增加编辑器的内存占用。通常这不是大问题。运行时加载从Assets目录在移动端即StreamingAssets加载与从默认的Addressables.RuntimePath加载在性能上没有本质区别都是同步或异步的二进制文件读取。版本管理Git/SVN等该提交什么建议将Assets/ExternalAddressableAssets/Catalogs/下的所有.json和.hash文件纳入版本控制。这些是文本文件记录了资源的索引和依赖关系。不该提交什么强烈建议将Assets/ExternalAddressableAssets/Bundles/目录添加到.gitignore或版本控制系统的忽略列表。.bundle是二进制文件体积巨大且会频繁随资源更改而重建。提交它们会迅速膨胀仓库大小。团队协作时应由CI/CD系统或专门的资源服务器来管理和分发构建好的AssetBundle而不是通过代码仓库。工作流开发者本地构建后Bundle文件存在于本地。通过CI系统进行正式构建时CI会生成新的Bundle并上传到资源服务器。版本库中只保存catalog文件它定义了资源的“蓝图”。其他成员更新版本库后可以根据catalog从资源服务器下载对应的Bundle。4.3 自动化与CI/CD集成思路对于团队项目将这个过程自动化是必然选择。自定义构建脚本作为标准流程将我们创建的CustomBuildScriptWithProjectPath或完善的后处理脚本作为项目Addressable构建的唯一入口。可以在编辑器菜单中添加一个自定义菜单项一键执行“构建并复制到项目”的全流程。命令行集成确保你的自定义构建逻辑可以通过Unity命令行调用。例如Unity.exe -batchmode -projectPath [项目路径] -executeMethod [你的构建方法入口] -quit你的构建方法应该依次调用AddressableAssetSettings.BuildPlayerContent()以及你的后处理复制逻辑。CI/CD管道在CI服务器如Jenkins, GitLab CI, GitHub Actions上配置一个构建任务。这个任务拉取代码包含catalog和资源定义。调用上述命令行执行Addressable资源构建。将构建产出的、位于项目目录下的Catalogs文件夹内容提交回版本库的一个特定分支或作为构建产物存档。将构建产出的Bundles文件夹内容上传到公司的资源存储服务器如AWS S3, 阿里云OSS或游戏内容分发网络CDN。更新资源服务器的版本索引使得游戏客户端能知道有新资源可用。通过这样一套流程就实现了资源定义的版本化管理catalog在Git中和资源内容的分发管理Bundle在对象存储中两者解耦又通过构建流程紧密关联。这正是Addressable系统赋能现代游戏开发工作流的核心价值之一而我们“将资源保存在项目中”的操作则是这个工作流中适应特定需求如编辑器工具链集成、资源归档的一个巧妙变通。