进阶-分布式构建与MOD支持篇章14-进阶篇状态完成阅读时间约 30 分钟一、引言在大型游戏项目中将所有资源放在一个 Package 中会导致几个问题。一是构建时间过长修改一个资源也要重新打包所有资源。二是更新粒度太粗一个 Bug 修复也需要下载完整的大包。三是扩展性受限第三方 MOD 创作者无法独立开发资源包。YooAsset 的多 Package 机制为这些问题提供了解决方案。本章将深入讲解如何利用多 Package 架构实现分布式构建和 MOD 支持。二、分布式构建2.1 为什么需要分布式构建分布式构建的核心思想是将一个大型项目拆分为多个独立的 Package每个 Package 可以被不同的团队独立构建和发布。这种架构的收益在项目规模达到一定程度后会非常显著。以一个典型的手机游戏为例项目包含核心资源角色、场景、UI、活动资源节日活动、限时活动和 DLC 资源资料片、扩展包。如果所有资源都在一个 Package 中每次活动更新都要打包所有资源构建时间可能超过一小时。而分布式构建将活动资源放在独立的 Package 中构建时间缩短到五分钟。在团队协作层面分布式构建允许不同部门独立工作。美术团队可以独立构建角色资源包策划团队可以独立构建关卡资源包互不干扰。每个团队只关注自己的 Package减少了协作冲突。2.2 多 Package 划分策略Package 的划分需要平衡几个因素更新频率、依赖关系、团队边界和运行时开销。更新频率是最重要的划分依据。将高频更新的资源放在独立的 Package 中可以减少每次更新的下载量。例如活动资源每周更新一次而角色资源可能几个月才更新一次它们就不应该在同一个 Package 中。依赖关系是另一个关键因素。如果两个 Package 之间存在大量的资源依赖将它们分开反而会增加运行时开销。在这种情况下将它们合并为一个 Package 或者提取共享包是更好的选择。团队边界是组织层面的考虑。如果两个团队分别负责不同的功能模块将它们管理的资源放在独立的 Package 中可以减少冲突。运行时开销指多 Package 带来的额外内存和加载时间。每个 Package 都有自己的运行时数据结构包括清单缓存、Provider 池和引用计数表。将 Package 划分得过细会显著增加运行时开销。实际操作中推荐按照以下标准划分每个 Package 的大小在 100MB 到 500MB 之间每个 Package 包含的功能模块不超过三个更新频率相近的资源放在同一个 Package 中。这样既能获得分布式构建的好处又不会引入过多的运行时开销。2.3 分布式构建的工程实现在 YooAsset 中实现分布式构建只需要配置多个 Package。每个 Package 对应一个独立的构建配置在构建时单独调用构建 API。using YooAsset.Editor; using System.Collections.Generic; public class DistributedBuild { public static void BuildAllPackages() { var packages new Liststring { CoreResources, ActivityResources, DLCResources }; foreach (var packageName in packages) { var buildParams new BuildParameters { OutputRoot $BuildOutput/{packageName}, BuildTarget BuildTarget.StandaloneWindows64, BuildPipeline ScriptableBuildPipeline, BuildMode EBuildMode.IncrementalBuild, PackageName packageName, PackageVersion GetVersion(packageName), EnableSharePackRule true, CompressOption ECompressOption.LZ4 }; var builder new AssetBundleBuilder(); var result builder.Run(buildParams); if (result.Success) { UnityEngine.Debug.Log(${packageName} 构建成功); } } } private static string GetVersion(string packageName) { // 每个 Package 有独立的版本号 var versions new Dictionarystring, string { { CoreResources, 2.1.0 }, { ActivityResources, 1.5.3 }, { DLCResources, 1.0.0 } }; return versions.ContainsKey(packageName) ? versions[packageName] : 1.0.0; } }三、MOD 支持3.1 MOD 系统的设计思路MOD 支持是分布式构建的一个重要应用场景。通过将 MOD 作为独立的 YooAsset Package 加载可以在不修改主游戏的情况下扩展游戏内容。MOD 系统的核心需求是隔离性。MOD Package 不能影响主游戏的资源管理反之亦然。YooAsset 的 Package 隔离机制天然满足这个要求——每个 Package 有独立的资源地址空间和加载生命周期。MOD 系统的第二个需求是动态加载。玩家可能在游戏运行期间下载新的 MOD并在不重启游戏的情况下加载使用。YooAsset 的 ResourcePackage 支持运行时创建和销毁可以在游戏运行中的任意时刻加载新的资源包。MOD 系统的第三个需求是资源覆盖。MOD 可能需要替换游戏中的某些资源比如替换角色的外观或场景的贴图。YooAsset 支持在不同 Package 之间设置加载优先级从而实现在不修改主 Package 的情况下覆盖资源。3.2 MOD Package 的实现实现 MOD Package 的流程分为两个阶段。首先是创建 MOD 资源包MOD 创作者使用 YooAsset 的 Collector 配置自己的资源然后构建为一个独立的 Package。构建产物是一个包含清单和资源文件的文件夹。using YooAsset; using UnityEngine; using System.Collections; public class ModManager : MonoBehaviour { private Dictionarystring, ResourcePackage modPackages new Dictionarystring, ResourcePackage(); /// summary /// 加载 MOD 资源包 /// /summary public IEnumerator LoadModPackage(string modId, string modUrl) { if (modPackages.ContainsKey(modId)) { Debug.LogWarning($MOD {modId} 已加载); yield break; } // 创建 MOD Package var package YooAssets.CreatePackage($Mod_{modId}); modPackages[modId] package; // 初始化 MOD Package使用 HostPlayMode 从服务器下载 var initParams new HostPlayModeParameters { DefaultHostServer modUrl, FallbackHostServer modUrl }; var initOp package.InitializeAsync(initParams); yield return initOp; if (initOp.Status EOperationStatus.Succeed) { // 更新 MOD 版本 var versionOp package.UpdatePackageVersionAsync(); yield return versionOp; if (versionOp.Status EOperationStatus.Succeed) { var manifestOp package.UpdatePackageManifestAsync(versionOp.PackageVersion); yield return manifestOp; } Debug.Log($MOD {modId} 加载成功); } } /// summary /// 从 MOD 加载资源 /// /summary public AssetHandle LoadModAsset(string modId, string location) { if (!modPackages.ContainsKey(modId)) { Debug.LogError($MOD {modId} 未加载); return null; } return modPackages[modId].LoadAssetAsyncUnityEngine.Object(location); } /// summary /// 卸载 MOD /// /summary public void UnloadMod(string modId) { if (modPackages.TryGetValue(modId, out var package)) { YooAssets.DestroyPackage(package.PackageName); modPackages.Remove(modId); Resources.UnloadUnusedAssets(); Debug.Log($MOD {modId} 已卸载); } } }3.3 MOD 的资源覆盖策略资源覆盖是指当 MOD 和主游戏中有同名资源时优先使用 MOD 的资源。实现这个功能需要在加载资源时先检查 MOD Package 中是否存在同名资源如果存在则从 MOD 加载否则从主 Package 加载。public class ModAwareResourceLoader { private string mainPackageName DefaultPackage; private ModManager modManager; public AssetHandle LoadWithModOverride(string location, System.Type type) { // 先检查所有已加载的 MOD foreach (var modId in modManager.GetActiveModIds()) { var modPackage modManager.GetModPackage(modId); if (modPackage.IsValid) { // 检查 MOD Package 中是否存在该资源 var assetInfo modPackage.GetAssetInfo(location); if (assetInfo ! null) { return modPackage.LoadAssetAsync(location, type); } } } // 如果没有 MOD 覆盖从主 Package 加载 var mainPackage YooAssets.GetPackage(mainPackageName); return mainPackage.LoadAssetAsync(location, type); } }四、多 Package 的依赖管理4.1 跨 Package 依赖当两个 Package 之间存在资源依赖时需要在加载时确保被依赖的 Package 已经初始化。YooAsset 不自动处理跨 Package 依赖需要开发者手动管理依赖顺序。推荐的方案是将共享资源提取为一个独立的共享 Package在加载任何业务 Package 之前先加载共享 Package。共享 Package 中包含所有被多个 Package 引用的公共资源。4.2 共享 Package 的版本同步共享 Package 的版本变更会影响所有依赖它的业务 Package。因此共享 Package 的更新需要特别谨慎遵循以下原则共享 Package 的新版本必须向后兼容不能删除或修改已有资源的标识符。共享 Package 的版本号使用语义化版本主版本变更时通知所有依赖方。五、性能考量多 Package 架构会增加运行时的开销。每个 Package 都需要维护独立的清单缓存、Provider 池和下载器。在测试中10 个 Package 比 1 个 Package 多消耗约 5MB 内存和 2ms 的每帧开销。建议将 Package 数量控制在 3 到 5 个之间。超过这个数量时需要考虑合并 Package 或者使用 lazy loading 策略只在需要时初始化特定 Package。六、总结分布式构建和 MOD 支持是 YooAsset 多 Package 架构的两个重要应用。分布式构建解决了大型项目的构建效率问题MOD 支持扩展了游戏的内容生态。两者都建立在 YooAsset 的 Package 隔离机制之上体现了架构设计中关注分离的核心思想。六、分布式构建的工程实践6.1 构建系统的架构设计在实际的工程项目中分布式构建不是一个简单的技术选择而是一个涉及团队协作、工具链和发布流程的系统设计问题。构建系统的架构设计需要从几个维度来考量。第一个维度是构建的触发时机。在小型项目中构建通常由开发者手动触发。但在大型项目中构建应该由 CI/CD 系统自动触发。每次 Git 提交后CI 系统自动检测变更的 Package只重新构建发生变更的 Package。这要求 CI 系统能够准确识别代码变更和 Package 之间的映射关系。推荐的做法是在每个 Package 的目录下放置一个配置文件记录该 Package 包含的资源路径。当 Git 提交涉及这些路径中的文件时触发对应 Package 的构建。第二个维度是构建产物的管理。每个 Package 的构建产物需要存储在不同的目录中并标记版本号。构建产物的存储结构应该是平台、版本号、Package 名称的三级结构。这样设计的好处是回滚时只需要切换版本号目录不需要重新构建。第三个维度是构建的并行度。当有多个 Package 需要构建时CI 系统应该并行执行这些构建任务。但并行度不是越高越好过多的并行构建会耗尽 CI 服务器的资源导致单个构建任务变慢。根据经验每台 CI 服务器的并行构建数建议控制在 4 到 8 个之间。6.2 依赖管理的挑战分布式构建最大的技术挑战是跨 Package 的依赖管理。当一个 Package 依赖于另一个 Package 中的资源时构建顺序就变得重要。被依赖的 Package 必须先构建完成依赖它的 Package 才能开始构建。解决这个问题有几种方案。第一种方案是静态依赖声明在每个 Package 的配置文件中声明它所依赖的其他 Package。构建系统按照依赖关系的拓扑顺序执行构建。这种方案实现简单但需要人工维护依赖声明容易遗漏。第二种方案是自动依赖分析构建系统在构建时自动分析所有资源的依赖关系构建依赖图。这种方案的理论基础是资源依赖图是有向无环图可以按照拓扑顺序构建。YooAsset 的 Editor 工具已经实现了资源依赖分析但跨 Package 的依赖分析需要额外的配置。第三种方案是共享 Package 模式将所有可能被依赖的公共资源提取到一个独立的共享 Package 中。业务 Package 只依赖共享 Package不依赖其他业务 Package。这种方案将复杂的多对多依赖关系简化为星型结构大大降低了依赖管理的复杂度。在实际项目中推荐使用第三种方案。虽然共享 Package 的维护需要额外的管理工作但它带来的依赖简化是值得的。6.3 版本同步的策略当多个 Package 同时发布时版本同步是一个关键问题。假设有一个共享 Package A 和两个业务 Package B 和 CB 和 C 都依赖 A。当 A 发布新版本时B 和 C 也需要重新构建和发布否则会出现版本不兼容的问题。解决版本同步问题的一个有效策略是建立一个版本兼容性矩阵。这个矩阵记录了每个 Package 版本与其他 Package 版本之间的兼容关系。在发布时CI 系统自动检查版本兼容性矩阵确保所有依赖关系是一致的。另一个策略是使用语义化版本号。共享 Package 的主版本变更时所有依赖它的业务 Package 必须重新构建。共享 Package 的次版本变更时业务 Package 可以选择升级或者不升级。共享 Package 的修订版本变更时业务 Package 不需要任何变更。七、分布式构建与单体构建的选择在决定是否使用分布式构建时需要考虑几个因素。项目规模是最重要的考虑因素小型项目使用单体构建的效率更高分布式构建的配置和管理成本超过了它带来的收益。团队规模也是重要的考虑因素如果只有一个开发团队分布式构建的意义不大。更新频率同样影响这个决策如果资源每周更新一次分布式构建可以节省大量的构建时间。分布式构建不是银弹它有自己的适用场景和局限性。在选择架构方案时需要根据项目的实际需求做出权衡。没有一个方案是通用的最优解只有最适合当前项目的方案。八、分布式构建的最佳实践总结经过上述分析可以总结出分布式构建的几个最佳实践。第一将共享资源提取为独立的 Package由专门的团队维护。第二每个 Package 使用独立的版本号版本号变更遵循语义化规范。第三建立自动化的 CI/CD 流水线代码提交后自动触发变更 Package 的构建。第四建立版本兼容性矩阵确保所有 Package 的版本是兼容的。第五控制 Package 数量在 3 到 5 个之间避免过多的运行时开销。第六为每个 Package 配置独立的 CDN 分发路径支持独立更新和回滚。在实际操作中还需要注意一些细节。Package 之间的依赖关系需要在设计阶段就明确而不是在开发过程中逐渐形成。共享 Package 的接口需要稳定避免频繁变更。构建系统的输出需要被版本管理工具追踪便于回滚和审计。这些细节看似琐碎但在大型项目中至关重要。遵循这些最佳实践可以显著降低分布式构建的维护成本提高团队的开发效率。九、分布式构建的常见误区分布式构建虽然有很多优点但在实际使用中也有一些常见的误区需要避免。第一个误区是认为分布式构建可以解决所有构建问题。分布式构建只能解决构建时间过长的问题不能解决构建配置错误、资源冗余、依赖循环等根本性问题。在引入分布式构建之前应该先确保资源管理的基础是健康的。如果单体构建的配置都不正确分布式构建只会将问题放大而不是解决。第二个误区是认为 Package 越多越好。每个 Package 都有运行时开销过多的 Package 会导致启动时间变长、内存占用增加。根据实践经验Package 数量控制在 3 到 5 个是比较合理的范围。超过 10 个 Package 时运行时的维护成本会显著增加。第三个误区是忽略 Package 之间的依赖关系。在实际项目中Package 之间往往存在隐式的依赖关系。比如 UI Package 中的界面可能引用了角色 Package 中的模型图标。这种隐式依赖在构建时不会出错但在运行时可能导致资源加载失败。避免这个问题的方法是明确声明所有跨 Package 依赖并在构建时进行验证。第四个误区是认为分布式构建可以完全并行化。虽然多个 Package 可以并行构建但它们之间如果存在依赖关系构建过程就不能完全并行。被依赖的 Package 必须先构建完成。因此在设计 Package 结构时应该尽量减少 Package 之间的依赖关系让构建过程可以更充分地并行化。第五个误区是忽略版本的兼容性。分布式构建的每个 Package 都有自己的版本号当版本号变更时需要确保所有相关的 Package 的版本仍然是兼容的。一个常见的错误是修改了共享 Package 但没有重新构建依赖它的业务 Package导致线上版本不兼容。十、从设计到实现的全流程指南分布式构建的实现需要从设计阶段开始规划。第一步是资源审计对项目中的所有资源进行分类和统计。统计的内容包括资源的类型、大小、引用关系和更新频率。这个审计结果将作为 Package 划分的依据。资源审计可以使用 YooAsset 的 Collector 工具来完成也可以编写自定义的 Editor 脚本来收集数据。审计完成后根据数据将资源划分为多个 Package。每个 Package 应该有明确的功能边界和更新策略。第二步是构建配置为每个 Package 创建独立的 Collector 配置和 Build 配置。配置中需要指定 Package 的输入目录、输出目录、压缩方式和加密方式。这步工作的产出是一个可重复执行的构建脚本。脚本应该支持增量构建每次只重新打包发生变更的资源。构建配置完成后进行试构建验证每个 Package 的输出是否正常。第三步是 CDN 部署将构建产物上传到 CDN 服务器。每个 Package 的产物存储在独立的目录中目录结构按照平台和版本号组织。上传脚本需要支持增量上传只上传发生变更的文件。部署完成后进行下载验证从 CDN 下载所有资源并校验完整性。第四步是运行时适配在游戏客户端中添加对分布式构建的支持。客户端需要能够同时管理多个 Package在需要时动态加载和卸载。适配工作的重点是资源加载的路径管理。在没有分布式构建时资源地址是全局唯一的。引入分布式构建后同一个资源地址可能存在于多个 Package 中需要明确的加载优先级规则。第五步是测试验证在真实设备上测试分布式构建的所有功能。测试内容包括 Package 的初始化、资源加载、热更新和 Package 卸载。特别需要测试的是跨 Package 的资源引用是否正确。测试完成后记录性能数据与单体构建方案进行对比分析。目标和需求是明确的。在开始实施分布式构建之前团队应该充分评估项目的规模、团队的协作方式和资源更新的频率确保分布式构建是必要的。在实施过程中应该循序渐进先在一个模块上试点验证效果后再推广到整个项目。在实施完成后应该持续监控和调优确保分布式构建的效果符合预期。分布式构建的最终目标是提高开发效率和交付质量而不是为了技术上的炫技。任何偏离这个目标的做法都是不可取的。在做出技术决策时应该时刻问自己一个问题这个决策是否真的帮助团队更快地交付更好的产品如果答案是否定的就需要重新思考这个决策。上一篇性能基准测试下一篇自定义文件系统开发