
上周末整理网盘的时候翻出一个2018年11月的老工程。密码差点忘了解压进去一看是当年大学阶段照着《保卫萝卜》思路写的一个塔防Demo里头有六张手绘地图、三种炮塔、一整套敌人波次管理器代码风格极其奔放单例满天飞全局变量遍地走。那个瞬间我冒出一个念头——要是能把它塞进浏览器里随时打开就能玩该多好。更巧的是现在AI辅助编程已经这么成熟了干脆试试两个小时能不能搞定。说干就干。从下载Unity Hub到把线上链接丢进微信群断断续续算下来真的只用了两个小时。但这个“两个小时”不是AI单枪匹马把活全干了而是“一个懂行的老开发 一个还不错的AI助手”的组合拳。这篇文章就是这次完整迁移过程的技术复盘重点会放在Unity WebGL构建的那些暗坑、浏览器里存档别乱写的教训、还有AI辅助开发里哪些钱省得值哪些坑绕不开。适合手里有老Unity项目想做Web迁移的开发者也适合想搞清楚AI在实际编码任务里到底有多大用处的朋友我会尽量把关键细节直接给出来。1. 项目盘点与整体方案设计1.1 为什么选Unity WebGL而不是推倒重来面对一个2018年的Unity工程想搬到浏览器里有几条路。第一条路是用Phaser或者PixiJS这类HTML5游戏框架把游戏重写一遍工作量约等于重做整个游戏。塔防的炮塔放置寻路逻辑、怪物波次控制器、六张地图的关卡数据、所有UI交互全都要从C#翻译成JavaScript我粗略估算了一下一周起步还未必能还原当年的手感。第二条路是干脆做一个串流方案游戏在云服务器跑浏览器只收画面。这个方案延迟、带宽、服务器成本全都要考虑本身就是大工程。第三条路就是Unity WebGL。Unity官方会把C#代码编译成WebAssembly和JavaScript把整个游戏逻辑原封不动地搬到浏览器里。游戏画面、场景、动画、音频全部复用我只需要处理平台差异和兼容性问题。对于本来就是Unity开发的老项目这是性价比最高的路径没有之一。综合判断下来方案选型没有任何悬念旧工程走Unity WebGLAI在这条路径里扮演文档加速器和代码转换助手的角色而架构上最终拍板的还是我自己。1.2 迁移前的“病案”盘点动手之前我先给这个老工程做了一次体检明确问题和风险点在哪里这一步非常重要。盲改代码就像不拍CT就给病人开刀最后大概率白费功夫。体检结果大概是这样的项目具体信息迁移风险等级Unity版本Unity 2017.4高API过时需要升级代码总量约8000行C#脚本中过时API散落各处资源体量图集、音频、地图素材约50MB高WebGL加载会卡存档方式PlayerPrefs 本地JSON文件读取高WebGL下行为完全不同字体使用UI全部用默认Arial动态字体高WebGL下中文会变方块阴影效果实时点光源阴影中移动浏览器性能堪忧插件依赖无原生插件低省掉很多麻烦这个表列出来之后迁移路线基本就清晰了先升级工程解决编译报错再处理运行时兼容问题接着是资源瘦身和字体最后是存档方案重做和部署上线。每件事都有先后顺序后面的章节我会按时间线把具体操作铺开讲。1.3 AI该插手的边界AI辅助编程现在很流行但要清楚它适合处理什么不适合处理什么。我当时的判断是批量替换API这种机械性很强、规则明确的活AI闭着眼睛都能干得比我好报错日志这种文本类信息丢给AI做模式匹配和搜索也比我自己一行行对着官档查效率高生成独立的小模块代码比如存档导入导出界面AI完全可以胜任。但有的事情AI给不了正确答案。比如整个项目的架构调整AI不了解我当年的设计意图给的方案往往太“教科书”再比如游戏手感、画面表现力的评估AI没有“审美”和“场景感知”还有资源压缩策略的具体取舍AI不懂我的目标用户会用什么样的手机打开这个页面。所以我的原则就一句话用AI把重复劳动吃掉把决策和判断留给自己。后面第5章会展开聊AI具体省了多少事、又在哪里坑了我一把。2. 两个小时从旧工程到可运行WebGL2.1 第0~30分钟环境准备与工程升级工欲善其事必先利其器。我直接装了Unity Hub选了一个稳定版本Unity 2021.3 LTS安装的时候记得勾选WebGL Build Support模块不然等你想构建WebGL的时候会被告知缺少模块还得重新打开安装器。Unity Hub装好后把2017.4的老工程直接用2021.3打开。Unity编辑器会提示“Upgrade project to a newer Unity version”确认之后会自动升级工程文件、转换序列化格式。这里有一件事非常关键打开后先别急着跑观察Console面板里的报错清单。老版本API在新版本里基本都标了过时但不是全部都会在编辑器里标红编译阶段才能看到完整错误。我第一次打开工程时Console面板刷出来一百多条报错。扫一眼基本分三类第一类是UnityEngine.WWW相关的API过期第二类是Application.LoadLevel这类场景加载API被移除第三类是部分UnityEngine.UI命名空间的访问方式变了。这些看着吓人但处理套路非常固定这正好是AI发挥的舞台。2.2 第30~60分钟代码兼容性调整这一步是整个迁移里干活最密集的阶段也是AI帮我省时间最明显的地方。我把Console面板里的报错信息整体复制下来直接丢给AI让它帮我出一份“旧API到新API的对照清单”。AI给了我一张表简单粗暴长这样旧API新APIApplication.LoadLevel / LoadLevelAsyncSceneManager.LoadScene / LoadSceneAsyncWWW类UnityWebRequest类renderer.materialGetComponentRenderer().materialAudioClip.Create旧签名新版需要传更多参数gameObject.activegameObject.SetActive(...)比我预想的干净。但光看表不够我让AI顺手写了一个Python脚本用正则表达式批量扫描Assets文件夹里的C#脚本把最常见的旧API替换成新API。脚本大概长这样import os, re replacements [ (rApplication\.LoadLevel\s*\(\s*([^])\s*\), rSceneManager.LoadScene(\1)), (rApplication\.LoadLevelAsync\s*\(\s*([^])\s*\), rSceneManager.LoadSceneAsync(\1)), (rnew\sWWW\s*\(([^)])\), rUnityWebRequest.Get(\1)), (r\.renderer\.material, r.GetComponentRenderer().material), (rgameObject\.active\s*\s*true, rgameObject.SetActive(true)), (rgameObject\.active\s*\s*false, rgameObject.SetActive(false)), ] root_dir ./Assets for folder, _, files in os.walk(root_dir): for filename in files: if not filename.endswith(.cs): continue path os.path.join(folder, filename) with open(path, r, encodingutf-8) as f: content f.read() new_content content for pattern, repl in replacements: new_content re.sub(pattern, repl, new_content) if new_content ! content: with open(path, w, encodingutf-8) as f: f.write(new_content) print(fupdated: {path})这里有个特别需要注意的坑WWW替换成UnityWebRequest不是简单的换名二者返回数据和异步方式完全不同。旧代码里用yield return www;然后www.text拿数据新代码需要改成yield return request.SendWebRequest();然后request.downloadHandler.text。AI生成的脚本只能做初步的机械替换剩下的这些调用时序修改我花了十几分钟手动过了一遍。跑完脚本重新回到Unity编辑器编译报错从一百多条降到了二十多条。剩下的大多是上下文相关的报错AI帮不上太多我自己手动修掉。这半小时我最大的感触是与其自己写每一条替换规则不如让AI把规则表扔出来我负责检查和兜底。2.3 第60~90分钟构建与运行问题修复编译通过后我打开File Build Settings平台切到WebGL点Build。第一次构建顺利出来了生成了一个WebGL文件夹里面有index.html、Build子目录和TemplateData目录。我随手装了一个http-server本地起静态服务打开页面问题接踵而至。第一个问题是页面黑屏浏览器控制台里报错提示无法解析.framework.js.gz。这是因为我之前顺手在Player Settings里选了Brotli压缩格式而本地静态服务器压根没返回对应的Content-Encoding头。浏览器拿到压缩文件后没法解压Unity加载器直接就挂了。解决方案很简单本地调试用Disable压缩格式部署到线上的时候再用Gzip如果服务器能正常返回br编码头也可以Brotli当时图省事直接切成了Disable重新构建。第二个问题特别经典也是很多做Unity WebGL的人都会撞上的加载进度条卡在42%就不动了。这个要打开浏览器的Network面板看实际上最大的那个.data文件有54MB正卡在网络下载上。原因是我当年的游戏贴图全是2K甚至4K原图音频还有不少无损WAV。我直接进Project Settings调整默认导入设置贴图压缩格式改成ASTC或者ETC2并开启Crunch Compression音频改成压缩格式Vorbis。重新构建后.data从54MB降到了26MB进度条一下子顺畅了。第三个问题让我哭笑不得所有中文UI文字全部变成了“豆腐块”。Unity在WebGL下不支持运行时加载操作系统字体当年用的默认Arial动态字体在浏览器里没有对应字体可用这种问题的典型表现在WebGL里就是中文全变方块。我在第4章会专门讲怎么解决字体问题。2.4 第90~120分钟部署上线修复完本地运行问题之后我直接部署上线。平台的路径很多GitHub Pages、Netlify、Vercel、Cloudflare Pages都能用我当时选的是Netlify因为它支持目录直接拖拽上传不需要配构建命令对Unity这种纯静态产物特别友好。部署前我重新在Player Settings里把Compression Format改成Gzip重新构建了一份然后把构建产物整体拖到Netlify。几秒钟后拿到一个HTTPS链接手机扫码打开游戏流畅跑起来了。这里有两个部署层面的细节值得强调。第一.wasm文件的MIME类型必须是application/wasm虽然Netlify默认支持但如果你用自己服务器一定要检查静态资源的Content-Type配置否则浏览器会拒绝执行WebAssembly模块。第二CDN一般默认开启Gzip压缩但如果你选Brotli就必须确认服务器真的支持Content-Encoding: br否则和我第一次构建遇到的坑一模一样。还有一件事挺重要我部署之前把游戏里的音频播放在Chrome和手机浏览器上试了一下发现默认情况下浏览器不允许页面自动播放音频。我后来在游戏开始界面加了一个“点击开始”按钮点击后才初始化Audiosource这个问题顺手就解决了。3. 最疼的坑IDBFS与存档系统的真相3.1 PlayerPrefs在WebGL下到底存哪了很多Unity开发者有一种错觉以为PlayerPrefs在WebGL下跟PC端一样随便用就行。我第一次跑通游戏后进入关卡、退出、再刷新页面发现存档居然还在以为万事大吉。直到我仔细看浏览器控制台才发现事情没有那么简单。WebGL没有传统意义上的本地文件系统Unity在浏览器里运行的时候内存中的虚拟文件系统每刷新页面就消失一次。为了让PlayerPrefs能持久化Unity官方在底层用IndexedDB做了一层持久化存储靠的正是IDBFS机制。也就是说PlayerPrefs的数据最终会落到浏览器IndexedDB数据库里而不是你服务器上的某个文件。用一句话解释浏览器把Unity的虚拟文件系统定期同步到IndexedDB里每次页面刷新后再从IndexedDB读回内存。这个设计看起来挺优雅但坑也藏在这里。3.2 我遇到的IDBFS写入失败与排查过程我在部署后第一次完整测试存档时做了这样一组操作进入游戏打了两关退出到主菜单关闭标签页重新打开链接点了“继续游戏”发现进度没了。打开浏览器控制台一看一堆红色报错关键词正是那条经典错误Failed to write file to IDBFS: ... IndexedDB request failed我第一反应是服务器配置的问题但显然不对这和网络请求没关系。接着去Chrome DevTools的Application面板找到IndexedDB一栏发现Unity的数据库确实存在但事务请求失败存储状态显示配额受限。我意识到是浏览器存储空间分配的问题。排查了一轮之后我把问题缩小到了两个触发条件。第一Safari和Firefox的严格隐私模式或者“阻止所有Cookie”模式下IndexedDB会被屏蔽Unity当然写不进去。第二浏览器存储配额不足时IDBFS事务也会失败尤其是当游客模式或者沙盒环境把磁盘配额压得很紧的时候。还有一种情况如果这个WebGL应用被嵌入到一个iframe里而iframe没有配置allow-same-origin属性那么IndexedDB一样不可用。这套东西很真实现在随便搜Unity WebGL IDBFS能看到大量开发者在这上面翻车。官方文档说只要你勾选了Publishing Settings里的Data CachingUnity就会尝试用IDBFS保存数据但这不代表它在所有浏览器环境都能成功。3.3 最终方案存档导出/导入 手动IndexedDB封装面对IDBFS的不稳定问题我做了两手准备。第一手是在不改变原游戏PlayerPrefs调用逻辑的前提下勾选Data Caching让Unity默认机制自动同步。第二手是加了一个“存档导出/导入”功能让玩家能拿到一份纯文本把进度备份到任何地方。这个功能实现起来很简单就是用JsonUtility把存档序列化成一个纯文本字符串然后通过剪贴板或者手动复制保存。我当时让AI帮我写了这一段逻辑它快速生成了核心代码我稍微调整了一下UI布局。核心片段大概是这样的public string ExportSave() { SaveData data new SaveData(); data.level PlayerPrefs.GetInt(level, 1); data.gold PlayerPrefs.GetInt(gold, 200); data.stars PlayerPrefs.GetInt(stars, 0); string json JsonUtility.ToJson(data, true); return Convert.ToBase64String(System.Text.Encoding.UTF8.GetBytes(json)); } public void ImportSave(string base64) { try { string json System.Text.Encoding.UTF8.GetString(Convert.FromBase64String(base64)); SaveData data JsonUtility.FromJsonSaveData(json); if (data ! null) { PlayerPrefs.SetInt(level, data.level); PlayerPrefs.SetInt(gold, data.gold); PlayerPrefs.SetInt(stars, data.stars); PlayerPrefs.Save(); } } catch (System.Exception e) { Debug.LogError($导入存档失败: {e.Message}); } }这段代码逻辑很简单却解决了最大的痛点管你是Safari隐私模式也好浏览器配额满了也好玩家只要把Base64字符串存下来换台设备都能恢复进度彻底绕开了IDBFS本身的不确定性。这里还要提醒各位不要直接在WebGL项目里使用System.IO.File去写绝对路径编译能过运行直接抛异常因为浏览器沙箱里根本没有传统文件路径的概念。当年我2018年写这个游戏时用File.WriteAllText存JSON的代码在WebGL里完全行不通这次迁移直接改成了上面的存档方案。4. 构建产物、浏览器兼容与移动端适配4.1 压缩格式选错加载会卡死在进度条Unity WebGL构建产物基本由三个文件组成.wasm是编译后的核心逻辑.framework.js是加载器.data是打包的资源数据。其中.data通常体积最大我这次从54MB降到26MB之后仍然不算小所以压缩格式的选择直接影响首屏加载速度。Unity在Player Settings Publishing Settings里提供了Compression Format选项Disable、Gzip、Brotli三选一。Gzip兼容性最好几乎所有Web服务器和浏览器都支持Brotli压缩率更高但要求服务器能正确返回br编码头。如果选错轻则加载慢重则直接黑屏。这里我的建议是本地调试用Disable线上发布用Gzip只有在你非常确定CDN支持br的情况下才用Brotli。另外加载进度条卡住还有个常见诱因构建时启用了增量构建但生成的缓存与服务器返回的hash不一致浏览器一直等资源没等到。遇到这种情况在构建前把Player Settings里的Strip Engine Code关掉再开一次或者清理一下项目临时缓存基本能解决。4.2 中文乱码与字体瘦身WebGL中文乱码的根因是Unity无法从浏览器操作系统拿到动态字体。在PC编辑器里Arial会自动映射到系统安装的中文字体但在WebGL运行时没有这种系统级回调。解决办法只有一个把字体文件打包进游戏并且显式创建Font Asset。我当时从网上下载了思源黑体的一个子集文件大概只有几MB然后用Unity的TextMeshPro工具重新生成了字体资源。注意不要直接把一个几十MB的完整中文字体打包进去那会让.data体积暴涨。正确做法是做一个字符子集只包含游戏里实际出现过的汉字这样几MB就够用了。另外一个容易忽视的细节是TextMeshPro的字体表里如果有缺字的场景会在运行时不显示或显示方框。我用一个脚本把场景里所有UI文本收集起来确保生成字库时把这些字全部包含进去彻底避免缺字问题。4.3 触摸事件、点击区域和UI适配手机浏览器打开WebGL游戏之后最常见的问题就是触摸不好用。Unity的默认输入系统在移动浏览器里对Input.touches兼容还行但如果你用的是Overlay模式下的UI还要看EventSystem的Input Module设置。我当时遇到的情况是PC上鼠标点击一切正常手机上点按钮时灵时不灵。排查下来发现原因是Canvas的Reference Resolution是按PC屏幕设置的手机分辨率等比缩放后按钮在视觉上缩小了点击区域跟着变小。我的处理方式是在Canvas Scaler中把UI Scale Mode改成Scale With Screen Size并设置一个适合移动端的Reference Resolution同时把比较大的主要按钮比如“开始游戏”“暂停”的点击区域逻辑上放大一圈做法很朴素就是在Button的Image组件外面再包一层透明的Raycast Target区域确保手指按上去容错率高。还有一个非常实用的小技巧如果按钮的图片本身是有透明区域的Unity默认会把透明区域也当成可点击范围这会导致误触。可以给Image组件设置alphaHitTestMinimumThreshold让透明度低于某个值的像素不参与点击检测这个参数在WebGL移动端表现非常稳定解决了不少误触问题。4.4 不同浏览器下的兼容性速查Unity WebGL对现代浏览器的兼容性整体不错但各家的表现还是有差异。我实测下来整理了一个速查表方便大家对照浏览器加载表现主要问题建议Chrome最好极少有问题优先用Chrome做调试Edge与Chrome同内核很好内存占用偏高建议关闭不用的标签页Firefox尚可隐私模式下IndexedDB可能被禁用提示用户关闭隐私模式Safari偏弱IDBFS兼容、存储配额限制、音频Autoplay策略严格尽量用最新版做好存档导出兜底微信内置浏览器一般WebAssembly支持不稳定加载较慢不建议作为主力入口内存占用这块值得多说两句。有的用户反馈浏览器打开游戏后风扇狂转是因为WebGL构建默认会占用较多内存特别是Unity的Il2Cpp内存池会预先分配。如果场景加载后内存不足浏览器可能会直接崩溃白屏常见的报错是Out of memory。我这次把贴图压缩掉、去除多余的运行时Shader变体之后内存占用从1.5GB降到700MB左右崩溃问题就缓解了。在移动端访问时还可以考虑在Player Settings里把Use Incremental GC关掉避免频繁GC造成卡顿。5. AI辅助开发的真实体验省下的时间和造出的新坑5.1 AI帮我省时间的三个场景第一个场景是批量API迁移。前面提到过Console面板报错一百多条我没必要一条条去查官方文档直接把报错copy给AI它生成替换规则和Python脚本我执行一遍剩下的二十多条手改工作效率明显高过纯手工。第二个场景是错误分析。部署后游戏在浏览器里报错我一开始不知道IDBFS是啥把控制台报错日志和Unity生成的Player.log一股脑丢给AI它很快指出这可能是IndexedDB写入失败并列出了隐私模式、存储配额、iframe限制这几个排查方向。省去了我搜论坛的时间。第三个场景是存档导出导入UI。这段功能相对独立AI生成完整代码的能力非常强。我把需求给它它直接给出一份带布局的完整C#代码我稍微改了一下样式就能跑通了。AI在“独立小模块”这个粒度上是最舒服的。5.2 AI的“幻觉”时刻与应对AI不是万能的这次它也在两个地方给过我带坑的建议。第一处是关于中文乱码。它建议我直接改WebGL模板HTML里的加载逻辑尝试在页面加载时注入字体我按照它的思路做了一段时间毫无效果。后来想明白了Unity引擎内部根本不会去读页面注入的字体这个方向从一开始就是错的。真正有效的方案是把中文字体作为Font Asset打进游戏。第二处是它试图给我引入一个第三方插件来管理浏览器存档理由是这个更“现代”。但我的项目是2018年的单例结构引入新插件意味着大范围重构风险极高。最后我坚持用原生的JsonUtility PlayerPrefs导出方案五分钟搞定比任何插件都轻巧。这两次经历让我总结出一个规律AI对具体项目的上下文理解有限它擅长提供通用方案和基础代码但“这个方案适不适合我的老项目”这类问题最终还是要靠人来判断。5.3 两小时工作流复盘AI参与度与人工判断占比那两小时的时间分布其实比想象中更规律我复盘过一遍大致是这样时间段核心任务AI参与度人工判断占比0-30分钟环境准备、工程升级、梳理报错低AI没参与高版本选型和升级决策30-60分钟API迁移、编译修复高生成替换规则和脚本中检查替换的正确性60-90分钟构建、本地起服务、修运行问题中帮忙分析报错高定位黑屏原因和资源优化90-120分钟部署上线、手机端验证低高因为涉及服务器配置和移动适配可以看到AI参与度最高的阶段是代码迁移和报错分析这些任务规则明确、信息密集而环境决策、定位综合问题、性能调优、部署上线的这些环节仍然需要大量人工判断。这个比例我认为在现在这个阶段是正常的以后AI能力再强这种需要行业经验和现场判断力的活儿还是得自己兜底。6. 部署之后还能怎么玩加载页、排行榜、更多平台6.1 给WebGL加一个带进度条的加载页Unity默认的WebGL加载模板是个灰色界面加一个进度百分比丑且没辨识度。如果想让页面看起来专业一点可以自定义模板。在Unity安装目录的WebGLBuildSupport文件夹里有默认模板复制一份出来改里面的index.html。最简单的做法是在原有进度条基础上加一点你自己的样式比如背景图、加载文案、进度条动画然后放到Assets/WebGLTemplates目录在Player Settings里选择这个模板。我这里给一个最小可用的模板片段它通过Unity的loader监听加载进度div idloading div idprogress-bardiv idprogress-fill/div/div div idprogress-text0%/div /div script var progressFill document.getElementById(progress-fill); var progressText document.getElementById(progress-text); var myGameInstance UnityLoader.instantiate(gameContainer, Build/build.json, { onProgress: function (gameInstance, progress) { var percent Math.round(progress * 100); progressFill.style.width percent %; progressText.textContent percent %; } }); /script如果你用的是Unity 2021以上的新版模板UnityLoader已经改名成createUnityInstance但思路是完全一样的就是监听onProgress回调更新页面的进度条DOM。这一段我建议在做完基础部署之后再去美化优先级不要高于游戏本身的稳定性。6.2 后续扩展排行榜、音效开关、小游戏平台这次迁移的主体已经完成了如果想把游戏做得更完整有几个方向可以继续往下走。第一个方向是加一个在线排行榜。Unity WebGL可以发HTTP请求到任意后端API我用一个简单的云函数存分数前端用UnityWebRequest提交和拉取排行榜数据不需要做复杂的服务器云端一个函数就能搞定。第二个方向是解决音效自动播放的限制。前面提过Chrome对AudioContext的自动播放限制很严格。我加了一个开始界面用户点击后调用AudioListener.pause false来唤醒音频系统。这个方案简单有效游戏从“开局没声音”变成“开局一起点开始”。第三个方向是转成微信小游戏。Unity提供了官方的小游戏适配方案可以把WebGL产物转换后运行在微信的运行时环境里但这个过程会引入微信SDK适配问题比如触摸事件、分包加载、小游戏包体积限制。如果以后有移动分发需求这条路值得探索但我建议先在Web端验证玩法不要一上来就双端并行。还有一个容易被忽略的问题WebGL构建很容易被搜索引擎收录但Unity游戏的动态画面搜索引擎抓不到。如果想给这篇博文配一个可玩的演示入口可以专门写一个带截图和简单介绍的落地页而不是直接把index.html丢出去这样对浏览体验和SEO都更友好。最后再分享一个这次操作中最受用的小习惯。如果你手里的老项目跟我这个一样是几年前的工程那么动手迁移之前第一件事是备份第二件事就是先把旧项目的存档导出功能加上哪怕只是能导出/导入都会让你在做浏览器端存档时少掉大把头发。AI可以帮你生成很多代码但它没法帮你在浏览器报错的时候去打开DevTools面板那些排查和判断始终只有你能做。