1. 项目概述:为什么WebGL下的AssetBundle加载是个“烫手山芋”
Unity部署到WebGL平台这件事,干过的人心里都有数——它不像打包成Windows或Android那样直来直去。你点下Build,等几分钟,出来一个build文件夹,里面一堆js、data、wasm文件,看着挺像那么回事;可一旦涉及运行时动态加载资源,比如用AssetBundle做热更、分包、按需加载场景或UI预制体,问题就一层层冒出来了。我带过的几个Unity项目里,有某高校实验室做的跨平台教育交互系统,也有某公司开发的轻量级3D产品展示页,全卡在“AssetBundle加载失败”这一步上,报错五花八门:Failed to load asset bundle: 0、TypeError: Cannot read property 'get' of undefined、RangeError: Maximum call stack size exceeded……最后发现,90%的问题根本不是代码写错了,而是对WebGL平台的底层限制理解不到位。
核心关键词就三个:Unity、WebGL、AssetBundle加载。这不是一个简单的“调个LoadFromFileAsync就能跑”的功能,而是一整套需要重新设计的资源交付链路。WebGL没有本地文件系统权限,不能像PC端那样直接读取磁盘路径;它依赖HTTP协议加载资源,但浏览器同源策略、CORS配置、MIME类型、压缩格式、内存模型全都和原生平台不同;更关键的是,AssetBundle在WebGL下无法使用LoadFromMemory或LoadFromStream这类低层API——Unity官方文档里白纸黑字写着:“WebGL不支持从内存或流中加载AssetBundle”。这意味着你必须走LoadFromWeb(实际是UnityWebRequest.GetAssetBundle)这条路,而这条路的每一步,都得和浏览器握手、和服务器协商、和Unity的WebGL内存管理斗智斗勇。
适合谁来看这篇?如果你正面临这些情况:刚把Unity项目切到WebGL平台,发现原来好好的AB加载逻辑全崩了;你正在规划一个需要热更新的Web 3D应用,但不确定AB方案是否可行;或者你已经看到控制台飘红却查不到根源,反复改路径、加CORS头、换压缩格式还是不行……那这篇就是为你写的。它不讲泛泛而谈的“原理”,只讲我在真实项目里踩过坑、测过数据、改过三次构建脚本后总结出的实操路径。下面所有内容,都是基于Unity 2021.3 LTS到2022.3 LTS版本实测验证的,不适用老版本(如5.x)或预览版(如2023.2+),这点必须 upfront 说清楚。
2. 整体设计思路:放弃“移植思维”,重建WebGL资源交付模型
很多人第一次做WebGL AB加载,第一反应是“我把PC版的AB加载逻辑复制过来,改个路径就行”。结果一运行,404、CORS、解压失败轮番轰炸。这不是Unity Bug,而是你没意识到:WebGL不是“另一个平台”,它是一个被浏览器沙箱彻底重构的执行环境。所以第一步,必须扔掉“移植”思维,建立一套专属于WebGL的资源交付模型。
2.1 为什么不能照搬原生AB流程?
先看原生平台典型流程:
- 构建阶段:
BuildPipeline.BuildAssetBundles("Assets/ABs", BuildAssetBundleOptions.ChunkBasedCompression, BuildTarget.StandaloneWindows64) - 运行时:
AssetBundle.LoadFromFile("D:/MyGame/ABs/ui.unity3d")→ 返回bundle对象 →bundle.LoadAsset<GameObject>("LoginPanel")
这套流程在WebGL下完全失效,原因有三:
路径语义失效:
LoadFromFile在WebGL中是空实现(Unity源码里直接return null),因为浏览器根本不允许JS代码随意访问本地文件系统。你传进去的路径,Unity连解析都不解析,直接跳过。内存模型冲突:WebGL运行在WASM线程上,主线程(JS)和Unity主线程(C#)之间通过
Module.HEAP32共享内存。AssetBundle解压后的二进制数据必须从WASM内存拷贝到JS堆,再由Unity C#层读取。这个过程涉及跨线程序列化,如果AB过大(>50MB),极易触发RangeError: Maximum call stack size exceeded——这不是栈溢出,而是JS引擎在处理超大ArrayBuffer时的递归深度限制。网络协议强约束:WebGL强制走HTTP(S),而HTTP有默认超时(通常30秒)、无连接复用、不支持断点续传。你用
UnityWebRequest.GetAssetBundle加载一个200MB的AB,中途网络抖动一次,整个请求就废了,且Unity不会自动重试。
提示:Unity官方推荐的替代方案是
Addressables系统,但它在WebGL下同样受限——底层仍是UnityWebRequest,只是封装了一层。如果你的项目已重度依赖AB,强行切Addressables成本极高,不如把AB流程本身打磨到极致。
2.2 WebGL专用AB交付模型的三大支柱
我们最终落地的模型,围绕三个不可妥协的原则构建:
静态托管优先:所有AB文件必须提前构建并部署到HTTP服务器(Nginx/Apache),禁止运行时生成或动态拼接URL。WebGL不支持
WWWForm上传、不支持File.WriteAllText写入,一切资源必须是“只读静态文件”。分片加载兜底:单个AB文件体积严格控制在≤30MB(实测安全阈值)。超过此值,必须拆分为多个子AB(如
ui_main.unity3d+ui_icons.unity3d+ui_fonts.unity3d),并通过依赖关系管理器(Dependency Manager)按需串行/并行加载。CORS与MIME双保险:服务器必须正确配置
Access-Control-Allow-Origin: *(或指定域名),且AB文件的HTTP响应头Content-Type必须为application/octet-stream。很多团队用Python SimpleHTTPServer测试,结果Content-Type是text/plain,导致Unity无法识别为二进制流,静默失败。
这个模型不是凭空想出来的。我们在某跨平台工业培训系统中实测对比过:单AB 80MB方案,首屏加载平均耗时12.7秒,失败率23%;拆成4个20MB AB并行加载,首屏耗时降至4.1秒,失败率0%。数据不会骗人——WebGL的性能瓶颈不在CPU,而在网络IO和内存拷贝效率。
2.3 构建流程重构:从“一键打包”到“分步可控”
原生平台构建AB是一条龙:选路径→点Build→完事。WebGL下必须拆成三步:
AB构建阶段:用
BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DeterministicAssetBundle选项构建,确保哈希一致(便于CDN缓存);禁用BuildAssetBundleOptions.UncompressedAssetBundle(WebGL不支持未压缩AB)。清单生成阶段:必须导出
AssetBundleManifest,且单独构建为assetbundlemanifest.unity3d。这是WebGL下做依赖分析的唯一依据——manifest.GetAllAssetBundles()返回所有AB名,manifest.GetDirectDependencies("ui.unity3d")返回其依赖项。Web部署阶段:将AB文件、manifest、以及一个
ablist.json(纯文本,记录所有AB名及版本号)全部上传至Web服务器同一目录。ablist.json结构如下:
{ "version": "1.2.3", "bundles": [ {"name": "ui.unity3d", "size": 28456789, "hash": "a1b2c3d4..."}, {"name": "models.unity3d", "size": 45678901, "hash": "e5f6g7h8..."} ] }这个文件是前端做增量更新的基础——下次加载前先GET它,比对hash,只下载变更的AB。
注意:Unity 2021.3+ 的WebGL构建会自动生成
WebGLBuild/StreamingAssets目录,但该目录仅用于Editor内模拟,实际Web部署时必须把AB文件放到Web服务器根目录或子目录下,和HTML同级或在其子路径中。很多人误以为把AB放进StreamingAssets就能自动加载,结果404到怀疑人生。
3. 核心细节解析:从URL构造到内存释放的12个生死关
WebGL AB加载看似就一行代码UnityWebRequest.GetAssetBundle(url),但背后藏着至少12个必须亲手把控的细节。漏掉任何一个,轻则加载失败,重则页面卡死、内存泄漏。以下全是血泪经验,按执行顺序排列。
3.1 URL构造:协议、路径、查询参数一个都不能错
WebGL下AB的URL必须是绝对路径,且协议必须匹配。常见错误:
- ❌ 错误1:
"Assets/ABs/ui.unity3d"—— 相对路径,Unity会尝试在StreamingAssets里找,找不到就报Failed to load。 - ❌ 错误2:
"http://localhost:8000/ABs/ui.unity3d"—— 开发时能跑,上线后因混合内容(HTTPS页面加载HTTP资源)被浏览器拦截。 - ❌ 错误3:
"/ABs/ui.unity3d?v=1.0"—— 查询参数v=1.0看似防缓存,实则可能被CDN或反向代理截断,导致404。
✅ 正确做法:
- 开发环境:
"http://localhost:8000/ABs/ui.unity3d"(确保本地服务器运行) - 生产环境:
"https://cdn.example.com/ab/v1.2.3/ui.unity3d"(CDN域名+版本路径) - 绝对避免查询参数,用路径版本化(
/ab/v1.2.3/)替代。
实操心得:我们用Python写了个小脚本,在每次构建AB后自动重命名文件为
ui_v1.2.3.unity3d,并更新ablist.json。这样URL永远稳定,CDN缓存也最友好。
3.2 CORS配置:不只是加个Header那么简单
你以为在Nginx里加一行add_header 'Access-Control-Allow-Origin' '*';就完了?错。WebGL AB加载会触发预检请求(Preflight),浏览器在发GET请求前,先发一个OPTIONS请求确认权限。如果服务器没正确响应OPTIONS,后续GET直接被拦。
Nginx正确配置示例:
location /ab/ { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } }重点在if ($request_method = 'OPTIONS')块——必须显式返回204状态码,且Content-Length: 0。我们曾因漏掉add_header 'Content-Length' 0;,导致Chrome OPTIONS响应体为空但header有长度,触发解析错误,AB加载无限pending。
3.3 MIME类型:Unity认的就是这个“身份证”
Unity WebGL加载AB时,会检查HTTP响应头的Content-Type。如果不是application/octet-stream,它会直接拒绝解析,控制台只打印一句模糊的Failed to load asset bundle,不报具体原因。
Apache配置:
<IfModule mime_module> AddType application/octet-stream .unity3d AddType application/octet-stream .assetbundle </IfModule>Nginx配置:
types { application/octet-stream unity3d; application/octet-stream assetbundle; }验证方法:用curl命令检查:
curl -I https://cdn.example.com/ab/ui.unity3d # 应返回:Content-Type: application/octet-stream如果返回text/plain或application/x-unity3d,立刻修正服务器配置。这是最容易被忽略却最致命的一环。
3.4 加载超时与重试:别让一次抖动毁掉整个体验
WebGL默认超时是30秒,但实际网络中,2G/3G弱网下加载30MB AB可能耗时45秒以上。Unity不会自动重试,UnityWebRequest失败后isNetworkError为true,但webRequest.error字符串为空,很难区分是超时还是404。
我们的解决方案是封装一个带重试的加载器:
public static async Task<AssetBundle> LoadBundleWithRetry(string url, int maxRetries = 2) { for (int i = 0; i <= maxRetries; i++) { using (var webRequest = UnityWebRequest.GetAssetBundle(url)) { // 关键:手动设置超时为60秒 webRequest.timeout = 60; var operation = webRequest.SendWebRequest(); while (!operation.isDone) await Task.Yield(); if (webRequest.result == UnityWebRequest.Result.Success) { return DownloadHandlerAssetBundle.GetContent(webRequest); } else { Debug.LogWarning($"AB load failed ({i+1}/{maxRetries+1}): {url} | Error: {webRequest.error}"); if (i == maxRetries) return null; // 最后一次失败,返回null await Task.Delay(1000 * (int)Mathf.Pow(2, i)); // 指数退避:1s, 2s, 4s } } } return null; }注意三点:
webRequest.timeout = 60必须显式设置,否则用默认30秒;await Task.Delay用指数退避(Exponential Backoff),避免雪崩式重试;- 失败时
Debug.LogWarning打印完整URL和错误,方便定位是哪个AB挂了。
3.5 内存管理:WebGL的“内存墙”比你想象的更低
WebGL的WASM内存是固定大小的(默认256MB),且Unity会预留一部分给引擎自身。实测发现,当AB解压后内存占用超过180MB时,UnityWebRequest的DownloadHandlerAssetBundle在拷贝数据时极易触发RangeError。
破解方法只有两个:
- AB体积硬限制:单个AB ≤25MB(留5MB缓冲)。用Unity的
BuildReport查看每个AB大小:
var report = BuildPipeline.BuildAssetBundles("Assets/ABs", options, target); foreach (var bundle in report.results) { Debug.Log($"{bundle.assetBundleName}: {bundle.size} bytes"); }- 加载后立即卸载:
AssetBundle.Unload(false)释放原生内存,但保留已加载的Asset引用;AssetBundle.Unload(true)则连Asset一起卸,慎用。我们采用混合策略:
// 加载AB var bundle = await LoadBundleWithRetry(url); // 从中加载需要的Prefab var prefab = bundle.LoadAsset<GameObject>("LoginPanel"); // 立即卸载AB,但保留prefab在内存中 bundle.Unload(false); // 关键! // 后续使用prefab... Instantiate(prefab);Unload(false)是WebGL下保命操作——它把AB的原生内存还给WASM堆,只留下C#层引用的Asset对象,内存占用立降40%。
3.6 依赖管理:没有manifest,AB就是一盘散沙
WebGL下AB依赖不能靠AssetDatabase.GetDependencies(Editor-only API),必须用运行时AssetBundleManifest。流程如下:
- 先加载
assetbundlemanifest.unity3d; - 用它获取目标AB的所有依赖AB名;
- 并行加载所有依赖AB(注意:必须先于目标AB加载完成);
- 最后加载目标AB。
代码骨架:
// 1. 加载manifest var manifestBundle = await LoadBundleWithRetry("https://cdn/ab/assetbundlemanifest.unity3d"); var manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest"); manifestBundle.Unload(false); // 2. 获取依赖 string[] dependencies = manifest.GetDirectDependencies("ui.unity3d"); // 3. 并行加载依赖 var depTasks = dependencies.Select(dep => LoadBundleWithRetry($"https://cdn/ab/{dep}")).ToArray(); await Task.WhenAll(depTasks); // 4. 加载目标AB var uiBundle = await LoadBundleWithRetry("https://cdn/ab/ui.unity3d");注意:
GetDirectDependencies只返回一级依赖。如果A依赖B,B依赖C,GetDirectDependencies("A")只返回B,不返回C。所以必须递归解析,直到无新依赖为止。我们写了个工具类自动处理这个递归树。
3.7 版本控制:如何让AB更新“悄无声息”
WebGL没有Application.persistentDataPath那样的本地存储,所有AB都从CDN拉。要实现热更新,必须解决两个问题:
- 如何知道哪些AB变了?
- 如何避免用户重复下载没变的AB?
答案是ablist.json+ 客户端本地Storage缓存。流程:
- 首次加载:GET
ablist.json→ 下载所有AB → 将ablist.json内容存入PlayerPrefs.SetString("ablist_cache", json); - 后续启动:先读
PlayerPrefs里的缓存 → 对比当前CDN的ablist.json→ 只下载hash不同的AB; - 下载完成后,更新本地缓存。
关键代码:
private async Task<string> GetRemoteAbList() { using (var req = UnityWebRequest.Get("https://cdn/ab/ablist.json")) { await req.SendWebRequest(); return req.downloadHandler.text; } } private bool ShouldDownloadBundle(BundleInfo remote, BundleInfo local) { return remote.hash != local.hash || !File.Exists(Path.Combine(Application.persistentDataPath, remote.name)); }这里BundleInfo是解析ablist.json得到的对象。注意:Application.persistentDataPath在WebGL下指向浏览器IndexedDB,可用,但容量有限(通常50MB),所以只存ablist.json这种小文件,AB本体仍放CDN。
3.8 调试技巧:绕过浏览器限制看真相
WebGL调试最大的障碍是:浏览器开发者工具看不到Unity内部的AB加载过程。你只能看到Network标签页里的HTTP请求,但不知道Unity是否成功解析了响应体。
我们的破局方法是:
- 在
UnityWebRequest后加一层“响应体快照”:
var webRequest = UnityWebRequest.GetAssetBundle(url); webRequest.downloadHandler = new DownloadHandlerBuffer(); // 先存原始字节 await webRequest.SendWebRequest(); Debug.Log($"Response size: {webRequest.downloadHandler.data.Length} bytes"); // 打印字节数 if (webRequest.downloadHandler.data.Length < 1000) Debug.LogError("AB response too small! Likely 404 or MIME error.");用
DownloadHandlerBuffer捕获原始响应体,检查长度。正常AB最小也有几KB(含Unity header),如果<1KB,基本是404或服务器返回了HTML错误页(如Nginx 404页面)。Chrome里按F12 → Network → 找到AB请求 → Right Click → “Save response as…” → 保存为
.unity3d文件 → 用Unity Editor的AssetBundleExtractor工具打开,验证是否真能解析。这是终极验真手段。
3.9 构建后处理:自动修复AB路径与依赖
Unity构建AB时,如果资源路径含中文或特殊字符(如角色_立绘.unity3d),WebGL加载会失败(URL编码问题)。我们写了构建后处理器,自动重命名:
[PostProcessBuild(100)] public static void OnPostprocessBuild(BuildTarget target, string pathToBuiltProject) { if (target != BuildTarget.WebGL) return; string abDir = Path.Combine(pathToBuiltProject, "ABs"); if (!Directory.Exists(abDir)) return; foreach (string file in Directory.GetFiles(abDir, "*.unity3d")) { string name = Path.GetFileNameWithoutExtension(file); // 转为纯英文+数字+下划线 string safeName = Regex.Replace(name, @"[^\w]", "_"); string newName = Path.Combine(Path.GetDirectoryName(file), safeName + ".unity3d"); if (file != newName) File.Move(file, newName); } }同时,这个脚本还会扫描所有AB,用AssetBundleManifest校验依赖完整性,缺失依赖则报错中断构建,避免上线后才发现AB加载失败。
3.10 错误分类表:一眼定位故障根源
| 错误现象 | 可能原因 | 快速验证方法 | 解决方案 |
|---|---|---|---|
| 控制台无日志,AB加载一直pending | 服务器未响应OPTIONS预检请求 | curl -X OPTIONS URL,看是否返回204 | 检查Nginx/Apache的OPTIONS配置 |
Failed to load asset bundle: 0 | URL 404 或 CORS被拒 | 浏览器Network标签页看AB请求状态码 | 检查URL路径、服务器CORS、MIME类型 |
TypeError: Cannot read property 'get' of undefined | DownloadHandlerAssetBundle.GetContent()返回null | 在GetContent后加if (bundle == null) Debug.LogError("Bundle is null") | 确保UnityWebRequest.result == Success后再调用GetContent |
RangeError: Maximum call stack size exceeded | AB过大(>50MB)或内存不足 | 查看AB文件大小;监控浏览器内存占用 | 拆分AB;Unload(false)及时释放 |
| 加载成功但Instantiate报NullReference | AB未包含所需Asset或Asset名拼写错误 | 用AssetBundleExtractor打开AB,检查Asset列表 | 确保BuildAssetBundleOptions包含CollectDependencies |
这张表是我们团队贴在工位上的“排错速查卡”,90%的问题30秒内定位。
3.11 性能优化:从“能用”到“丝滑”的5个关键点
CDN选择:国内用腾讯云CDN或阿里云CDN,海外用Cloudflare。测试发现,Cloudflare对
.unity3d文件的Brotli压缩支持更好,传输体积比Gzip小18%。HTTP/2启用:确保CDN和源站都开启HTTP/2。它支持多路复用,多个AB请求可复用同一TCP连接,减少TLS握手开销。
预加载提示:在HTML中加入
<link rel="preload" href="ui.unity3d" as="fetch" crossorigin>,提示浏览器提前DNS解析和连接。AB压缩算法:Unity 2021.3+默认用LZ4,比旧版LZMA快3倍,解压时间从800ms降至220ms(实测i7-10870H)。
加载队列限流:并发加载AB数控制在3~4个。太多并发会挤占网络带宽,反而拖慢总耗时。我们用
SemaphoreSlim控制:
private static readonly SemaphoreSlim _abLoadSemaphore = new SemaphoreSlim(3, 3); private async Task<AssetBundle> LoadWithLimit(string url) { await _abLoadSemaphore.WaitAsync(); try { return await LoadBundleWithRetry(url); } finally { _abLoadSemaphore.Release(); } }3.12 卸载与清理:WebGL下没有“垃圾回收”这回事
WebGL的内存不会自动回收。AssetBundle.Unload(true)后,如果还有C#变量引用着其中的Texture或Mesh,Unity不会释放它们,内存持续增长。我们强制推行“作用域卸载”规范:
// ✅ 正确:用using或明确作用域 public async void LoadAndUseUi() { AssetBundle bundle = null; try { bundle = await LoadBundleWithRetry("ui.unity3d"); var panel = bundle.LoadAsset<GameObject>("LoginPanel"); Instantiate(panel); } finally { bundle?.Unload(false); // 立即释放AB原生内存 } } // ❌ 错误:bundle变量逃逸出作用域 private AssetBundle _cachedBundle; public void CacheBundle() { _cachedBundle = LoadBundleWithRetry("ui.unity3d"); } // 后续忘记Unload,内存永久泄漏团队Code Review时,只要看到AssetBundle字段声明在类级别,直接打回重写。这是WebGL项目的生命线。
4. 实操全流程:从零开始搭建可上线的AB加载系统
现在,我们把前面所有知识点串起来,走一遍完整的、可直接复制粘贴的实操流程。以Unity 2021.3.30f1为例,目标:构建一个ui.unity3dAB,部署到本地Nginx,用C#代码加载并实例化其中的LoginPanel预制体。
4.1 准备工作:环境与工具安装
- Unity版本:2021.3.30f1(LTS),确保安装WebGL模块;
- 本地服务器:Nginx for Windows/macOS/Linux(不要用Python SimpleHTTPServer);
- AB分析工具:Unity官方
AssetBundleExtractor(GitHub搜); - 浏览器:Chrome 110+(调试WebGL必备)。
提示:Mac用户注意,Unity 2021.3 macOS版有WebGL构建Bug(生成的wasm文件损坏),务必用Windows或Linux构建机,或升级到2022.3。
4.2 第一步:创建AB并构建
- 在Unity中新建文件夹
Assets/ABs/UI,把LoginPanel.prefab拖进去; - 选中该Prefab → Inspector → AssetBundle Name 输入
ui(小写,无空格); - 创建构建脚本
BuildABs.cs:
using UnityEditor; using UnityEngine; public class BuildABs { [MenuItem("Assets/Build AssetBundles")] static void BuildAllAssetBundles() { string assetBundleDirectory = "Assets/ABs"; if (!Directory.Exists(assetBundleDirectory)) Directory.CreateDirectory(assetBundleDirectory); BuildPipeline.BuildAssetBundles( assetBundleDirectory, BuildAssetBundleOptions.ChunkBasedCompression | BuildAssetBundleOptions.DeterministicAssetBundle, BuildTarget.WebGL ); // 自动构建manifest BuildPipeline.BuildAssetBundles( assetBundleDirectory, BuildAssetBundleOptions.None, BuildTarget.WebGL ); } }- 点击菜单
Assets → Build AssetBundles,等待完成。生成文件:Assets/ABs/ui.unity3dAssets/ABs/assetbundlemanifest.unity3dAssets/ABs/AssetBundles(文件夹,含ui和assetbundlemanifest两个文件)
4.3 第二步:配置Nginx并部署AB
- 下载Nginx,解压;
- 修改
conf/nginx.conf,在http块内添加:
server { listen 8000; server_name localhost; location /ab/ { alias /path/to/your/ABs/; # 替换为你的ABs绝对路径 add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; add_header 'Access-Control-Expose-Headers' 'Content-Length,Content-Range'; if ($request_method = 'OPTIONS') { add_header 'Access-Control-Allow-Origin' '*'; add_header 'Access-Control-Allow-Methods' 'GET, OPTIONS'; add_header 'Access-Control-Allow-Headers' 'DNT,User-Agent,X-Requested-With,If-Modified-Since,Cache-Control,Content-Type,Range'; add_header 'Access-Control-Max-Age' 1728000; add_header 'Content-Type' 'text/plain; charset=utf-8'; add_header 'Content-Length' 0; return 204; } types { application/octet-stream unity3d; } } }- 启动Nginx:
nginx.exe(Windows)或sudo nginx(macOS/Linux); - 访问
http://localhost:8000/ab/ui.unity3d,应直接下载文件(说明Nginx配置成功)。
4.4 第三步:编写WebGL专用加载器
在Unity中创建脚本WebGLABLoader.cs:
using System.Collections; using System.Collections.Generic; using System.Threading.Tasks; using UnityEngine; using UnityEngine.Networking; public class WebGLABLoader : MonoBehaviour { private AssetBundleManifest _manifest; public async void Start() { // 1. 加载manifest var manifestBundle = await LoadBundleWithRetry("http://localhost:8000/ab/assetbundlemanifest.unity3d"); _manifest = manifestBundle.LoadAsset<AssetBundleManifest>("AssetBundleManifest"); manifestBundle.Unload(false); // 2. 加载ui AB及其依赖 string[] deps = _manifest.GetDirectDependencies("ui.unity3d"); var depTasks = new List<Task<AssetBundle>>(); foreach (string dep in deps) { depTasks.Add(LoadBundleWithRetry($"http://localhost:8000/ab/{dep}")); } await Task.WhenAll(depTasks); // 3. 加载ui var uiBundle = await LoadBundleWithRetry("http://localhost:8000/ab/ui.unity3d"); if (uiBundle == null) { Debug.LogError("Failed to load ui.unity3d"); return; } // 4. 加载并实例化Prefab var panel = uiBundle.LoadAsset<GameObject>("LoginPanel"); if (panel != null) { Instantiate(panel, transform); } else { Debug.LogError("LoginPanel not found in ui.unity3d"); } uiBundle.Unload(false); } private async Task<AssetBundle> LoadBundleWithRetry(string url, int maxRetries = 2) { for (int i = 0; i <= maxRetries; i++) { using (var webRequest = UnityWebRequest.GetAssetBundle(url)) { webRequest.timeout = 60; var operation = webRequest.SendWebRequest(); while (!operation.isDone) await Task.Yield(); if (webRequest.result == UnityWebRequest.Result.Success) { return DownloadHandlerAssetBundle.GetContent(webRequest); } else { Debug.LogWarning($"Load failed ({i+1}/{maxRetries+1}): {url} | {webRequest.error}"); if (i == maxRetries) return null; await Task.Delay(1000 * (int)Mathf.Pow(2, i)); } } } return null; } }将此脚本挂到Main Camera上。
4.5 第四步:构建WebGL并测试
File → Build Settings → Platform: WebGL → Switch Platform;Player Settings → Publishing Settings → Compression Format: LZ4(非LZMA);Build,输出到WebGLBuild文件夹;- 将
WebGLBuild内所有文件(含index.html)复制到Nginx的html目录下; - 重启Nginx,访问
http://localhost:8000; - 打开Chrome DevTools → Console,应看到
LoginPanel成功加载日志; - Network标签页,确认
ui.unity3d和assetbundlemanifest.unity3d请求状态为200。
4.6 第五步:生产环境加固
上线前必做5件事:
- CDN接入:把
/ab/目录映射到CDN,URL改为https://cdn.example.com/ab/; - HTTPS强制:Nginx配置
return 301 https://$host$request_uri;; - 错误监控:在
LoadBundleWithRetry里加入Sentry上报:
if (webRequest.result != UnityWebRequest.Result.Success) { SentrySdk.CaptureMessage($"AB Load Failed: {url}", scope => scope.SetTag("error", webRequest.error)); }- 降级方案:如果AB加载失败3次,自动切换到内置资源(
Resources.Load):
var fallback = Resources.Load<GameObject>("FallbackLoginPanel"); if (fallback != null) Instantiate(fallback, transform);- 性能埋点:记录每个AB的加载耗时、大小、成功率,用ECharts画Dashboard。
5. 常见问题与排查技巧实录:那些年我们填过的坑
5.1 问题:AB加载成功,但Instantiate时Texture显示为粉红色(Missing Texture)
现象:控制台无报错,Prefab实例化后,Image组件显示粉红,Inspector里Texture字段为None。
排查过程:
- 用
AssetBundleExtractor打开ui.unity3d,发现LoginPanel.prefab引用的bg_texture.png在AB里,但bg_texture.png本身没被打进AB(因为它的AssetBundle Name为空); - 检查
bg_texture.png的Inspector,发现AssetBundle Name字段是空的,而LoginPanel.prefab的依赖