Unity AssetBundle高效批量打包与动态加载实战(场景与Prefab全解析)

张开发
2026/4/7 9:50:46 15 分钟阅读

分享文章

Unity AssetBundle高效批量打包与动态加载实战(场景与Prefab全解析)
1. 为什么需要AssetBundle批量打包在Unity游戏开发中资源管理是个绕不开的话题。想象一下你正在开发一款大型MMORPG游戏里面有上百个场景、上千个角色模型、数不清的UI界面。如果把这些资源全部打包在一个安装包里玩家下载安装时可能会崩溃——动辄几个G的安装包谁受得了这就是AssetBundle的用武之地。它就像游戏资源的快递包裹可以按需打包、按需加载。我做过一个卡牌游戏项目原本安装包有1.2G使用AssetBundle后缩减到300M玩家下载安装后再根据游戏进度动态加载其他资源包。AssetBundle的核心优势有三点减小初始包体积只把核心资源放进安装包支持热更新不用重新发布应用就能更新游戏内容资源模块化可以按功能模块划分资源包2. 批量打包场景与Prefab的完整流程2.1 准备工作资源目录规划好的目录结构是成功的一半。我习惯这样组织资源Assets/ └── AssetBundles/ ├── Scenes/ # 存放场景文件 │ └── V1.0/ # 按版本号分目录 │ └── *.unity └── Prefabs/ # 存放预制体 ├── UI/ ├── Characters/ └── Effects/为什么要分版本在ToB项目中经常需要同时维护多个版本。比如V1.0的场景有bug需要修复但V2.0正在开发中分开管理就不会互相影响。2.2 场景打包实战代码场景打包有个特别要注意的地方不能压缩。我踩过这个坑用LZ4压缩的场景死活加载不出来查了半天文档才发现这个限制。[MenuItem(Tools/Build/Scene Bundles)] static void BuildSceneBundles() { string outputPath Path.Combine(Application.streamingAssetsPath, SceneBundles); if (!Directory.Exists(outputPath)) Directory.CreateDirectory(outputPath); // 获取最新版本目录 var sceneDirs Directory.GetDirectories(Path.Combine(Application.dataPath, AssetBundles/Scenes), V*); string latestVersion sceneDirs.OrderByDescending(d d).First(); // 收集场景文件 var scenePaths Directory.GetFiles(latestVersion, *.unity, SearchOption.AllDirectories) .Select(p p.Replace(Application.dataPath, Assets)) .ToArray(); // 构建AssetBundleBuild数组 var builds new AssetBundleBuild[scenePaths.Length]; for (int i 0; i scenePaths.Length; i) { builds[i] new AssetBundleBuild { assetBundleName Path.GetFileNameWithoutExtension(scenePaths[i]).ToLower() .scene, assetNames new[] { scenePaths[i] } }; } // 开始打包 - 注意使用None选项 BuildPipeline.BuildAssetBundles( outputPath, builds, BuildAssetBundleOptions.None, BuildTarget.StandaloneWindows ); AssetDatabase.Refresh(); Debug.Log($场景打包完成共{scenePaths.Length}个场景); }2.3 Prefab打包的优化技巧Prefab打包可以选择压缩方式这里推荐使用LZ4压缩。它比传统的LZMA压缩加载更快因为不需要解压整个包就能读取单个资源。[MenuItem(Tools/Build/Prefab Bundles)] static void BuildPrefabBundles() { string outputPath Path.Combine(Application.streamingAssetsPath, PrefabBundles); // 按类型分组打包 var prefabGroups new Dictionarystring, Liststring(); // 收集所有Prefab var prefabPaths Directory.GetFiles(Path.Combine(Application.dataPath, AssetBundles/Prefabs), *.prefab, SearchOption.AllDirectories) .Select(p p.Replace(Application.dataPath, Assets)) .ToList(); // 按目录分组 foreach (var path in prefabPaths) { string dirName new FileInfo(path).Directory.Name.ToLower(); if (!prefabGroups.ContainsKey(dirName)) prefabGroups[dirName] new Liststring(); prefabGroups[dirName].Add(path); } // 构建打包信息 var builds new ListAssetBundleBuild(); foreach (var group in prefabGroups) { builds.Add(new AssetBundleBuild { assetBundleName $prefabs_{group.Key}, assetNames group.Value.ToArray() }); } // 使用LZ4压缩 BuildPipeline.BuildAssetBundles( outputPath, builds.ToArray(), BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows ); AssetDatabase.Refresh(); Debug.Log($Prefab打包完成共{prefabPaths.Count}个Prefab分成{prefabGroups.Count}个包); }3. 动态加载的三种姿势3.1 同步加载简单直接同步加载最适合游戏初始化时使用比如加载主界面UI。代码简单直接但会阻塞主线程。public GameObject LoadUIPrefab(string bundleName, string prefabName) { // 加载AssetBundle var bundle AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, $PrefabBundles/{bundleName})); if (bundle null) { Debug.LogError($加载{bundleName}失败); return null; } // 加载Prefab var prefab bundle.LoadAssetGameObject(prefabName); bundle.Unload(false); // 保留加载的资源 return Instantiate(prefab); }3.2 异步加载流畅体验对于大型资源比如场景一定要用异步加载。我做过测试同步加载一个200MB的场景会让游戏卡顿3-5秒而异步加载几乎无感。public IEnumerator LoadSceneAsync(string sceneName) { string bundlePath Path.Combine(Application.streamingAssetsPath, $SceneBundles/{sceneName}.scene); // 异步加载AssetBundle var bundleRequest AssetBundle.LoadFromFileAsync(bundlePath); yield return bundleRequest; if (bundleRequest.assetBundle null) { Debug.LogError($加载{sceneName}场景包失败); yield break; } // 异步加载场景 var sceneRequest SceneManager.LoadSceneAsync( Path.GetFileNameWithoutExtension(sceneName), LoadSceneMode.Additive ); while (!sceneRequest.isDone) { float progress sceneRequest.progress * 100; Debug.Log($场景加载进度: {progress:F0}%); yield return null; } bundleRequest.assetBundle.Unload(false); }3.3 依赖加载解决引用关系当Prefab引用了其他资源比如材质、贴图时需要先加载依赖包。忘记处理依赖是我早期常犯的错误会导致资源显示异常。private AssetBundleManifest _manifest; void LoadDependencies(string bundleName) { if (_manifest null) { var mainBundle AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, PrefabBundles)); _manifest mainBundle.LoadAssetAssetBundleManifest(AssetBundleManifest); } string[] dependencies _manifest.GetAllDependencies(bundleName); foreach (var dep in dependencies) { if (!_loadedBundles.ContainsKey(dep)) { var bundle AssetBundle.LoadFromFile(Path.Combine(Application.streamingAssetsPath, $PrefabBundles/{dep})); _loadedBundles.Add(dep, bundle); } } }4. 内存管理的三个关键点4.1 及时卸载AssetBundle不卸载AssetBundle会导致内存泄漏。但要注意两种卸载方式的区别Unload(true)卸载AB包和所有资源Unload(false)只卸载AB包保留资源void UnloadUnusedAssets() { // 先解除引用 _loadedPrefab null; // 再调用卸载 Resources.UnloadUnusedAssets(); // 或者直接卸载AB包 foreach (var bundle in _loadedBundles.Values) { bundle.Unload(true); } _loadedBundles.Clear(); }4.2 引用计数管理对于频繁使用的资源可以实现简单的引用计数避免重复加载和过早卸载。Dictionarystring, (AssetBundle bundle, int refCount) _bundleRefs new(); public GameObject LoadWithRefCount(string bundleName, string prefabName) { if (!_bundleRefs.ContainsKey(bundleName)) { var bundle AssetBundle.LoadFromFile(/*...*/); _bundleRefs[bundleName] (bundle, 0); } _bundleRefs[bundleName].refCount; return _bundleRefs[bundleName].bundle.LoadAssetGameObject(prefabName); } public void Release(string bundleName) { if (_bundleRefs.ContainsKey(bundleName)) { _bundleRefs[bundleName].refCount--; if (_bundleRefs[bundleName].refCount 0) { _bundleRefs[bundleName].bundle.Unload(true); _bundleRefs.Remove(bundleName); } } }4.3 资源加载策略优化根据设备内存情况动态调整加载策略高端设备可以预加载更多资源低端设备采用按需加载快速卸载策略public enum LoadStrategy { Aggressive, // 预加载 Conservative // 按需加载 } LoadStrategy GetCurrentStrategy() { long memSize SystemInfo.systemMemorySize; return memSize 4000 ? LoadStrategy.Aggressive : LoadStrategy.Conservative; }5. 实战中的常见问题解决5.1 打包后资源丢失问题这个问题我遇到过好几次根本原因是资源路径不正确。确保使用Application.dataPath获取Assets目录绝对路径打包时传入的路径必须以Assets/开头检查资源是否设置了正确的AssetBundle Name5.2 跨平台兼容性问题不同平台的AssetBundle不兼容。解决方法为每个平台创建单独的构建目录使用BuildTarget参数指定目标平台在运行时通过Application.platform判断当前平台string GetPlatformFolder() { switch (Application.platform) { case RuntimePlatform.WindowsPlayer: return Windows; case RuntimePlatform.Android: return Android; case RuntimePlatform.IPhonePlayer: return iOS; default: return Standalone; } }5.3 版本管理方案实现简单的版本控制可以避免资源错乱打包时生成manifest文件记录版本号加载前检查服务器上的版本信息不一致时下载新版本AssetBundle[System.Serializable] public class BundleVersion { public string bundleName; public int version; } public class BundleManager { Dictionarystring, int _localVersions new(); Dictionarystring, int _serverVersions new(); void CheckUpdate() { foreach (var bundle in _serverVersions) { if (!_localVersions.ContainsKey(bundle.Key) || _localVersions[bundle.Key] bundle.Value) { StartCoroutine(DownloadBundle(bundle.Key)); } } } }在最近的一个商业化项目中我们实现了完整的AssetBundle热更新系统使游戏资源更新包体积减少了70%玩家更新成功率从85%提升到了98%。关键就在于合理的打包策略和可靠的加载机制。

更多文章