
1. 项目概述为什么我们需要自动化每次在Unity里把项目发布成WebGL你是不是都得重复那几件“小事”打开Player Settings勾上“Run In Background”把分辨率调成1920x1080再到Publishing Settings里把压缩格式从Disabled改成Brotli或者Gzip。项目小还好要是手头同时维护着三五个不同需求的WebGL Demo每次发布前都得手动核对一遍不仅繁琐还容易出错。万一忘了开压缩一个几兆的小游戏加载起来慢如蜗牛忘了设全屏玩家一点开发现是个小窗口体验直接打折。这个流程的核心痛点在于重复和易遗漏。Unity的构建设置Player Settings本质上是项目资产的一部分但针对不同发布平台尤其是WebGL的一些关键配置却需要我们像校对员一样每次手动确保。这不符合现代开发中“一次设置处处生效”的自动化理念。更头疼的是有些设置比如WebGL模板的修改、启动全屏的JavaScript注入往往需要在构建完成后再次手动干预流程被割裂了。所以这个项目的目标非常明确将Unity 2022同样适用于其他相近版本发布WebGL时所有需要手动操作的、与全屏和压缩相关的配置全部通过脚本自动化完成。我们要实现的是从点击“Build”按钮开始到最终得到一个开箱即用、具备正确全屏行为和优化压缩的WebGL构建包中间无需任何人工点击。这不仅仅是省了几次鼠标点击更是将发布流程标准化、可靠化为持续集成/持续部署CI/CD铺平道路。2. 核心需求与方案设计拆解要实现“自动搞定”我们首先得弄清楚到底要“搞定”什么。基于标题和常见的WebGL发布经验我们可以将需求拆解为三个层次并为之设计对应的自动化方案。2.1 需求一构建时自动应用Player Settings这是最基础的一层。我们需要在Unity构建流程开始前通过脚本自动设置好那些关键的Player Settings。对于WebGL全屏体验最重要的两个设置是Run In Background: 必须勾选。WebGL应用在浏览器标签页失去焦点时默认会暂停执行。勾选此项可以保证即使用户切换了标签页你的游戏逻辑比如网络重连、后台音乐也能继续运行这对于需要保持连接或状态的应用至关重要。Default Screen Width/Height: 设置默认的显示分辨率。虽然WebGL最终会撑满浏览器窗口或容器但这里设置一个合理的初始值如1920x1080可以作为回退方案并影响一些初始化渲染逻辑。方案设计我们将创建一个PreBuildProcessor脚本利用Unity的IPreprocessBuildWithReport接口。这个接口允许我们在构建开始前执行自定义代码。在这里我们将直接修改PlayerSettings类的相关静态属性。2.2 需求二构建时自动配置压缩格式WebGL构建的输出是大量的.data、.framework.js、.wasm等文件。对这些文件进行压缩能显著减少网络传输体积提升加载速度。Unity WebGL主要支持两种压缩Gzip: 兼容性极好所有现代浏览器都支持。Brotli: 压缩率通常比Gzip更高但需要较新版本的浏览器支持现代浏览器基本都已支持。如果不在Unity中明确设置压缩选项默认可能是Disabled这意味着你的构建包会以原始大小传输浪费带宽和用户时间。方案设计同样在PreBuildProcessor脚本中我们将修改PlayerSettings.WebGL.compressionFormat属性。为了获得最佳兼容性和压缩率平衡我们通常选择WebGLCompressionFormat.Brotli。如果构建服务器或目标环境不支持Brotli再回退到Gzip。2.3 需求三构建后自动注入全屏控制逻辑这是最关键也最容易忽略的一层。Unity Player Settings里的“Fullscreen Mode”选项在WebGL平台上的作用有限。它通常只是设置Unity内部渲染视图的“全屏”状态而非控制浏览器层面的全屏。要让WebGL应用能够响应按键比如F11或代码调用实现真正的浏览器全屏我们需要在生成的HTML页面通常是index.html中加入特定的JavaScript代码。方案设计我们需要介入构建完成后的环节。这里有两种主流方法修改WebGL模板Unity允许自定义WebGL模板。我们可以创建一个自定义模板在里面直接写好全屏控制的JS代码和按钮。这是一劳永逸的方法但需要维护模板文件。构建后处理脚本创建一个PostBuildProcessor脚本利用IPostprocessBuildWithReport接口。在构建完成后脚本自动定位生成的index.html文件读取其内容在body标签结束前或script标签内插入我们准备好的全屏JS代码片段。这种方法更灵活无需管理模板直接对输出产物进行修改。考虑到项目的标题是“自动搞定全屏与压缩设置”为了流程的彻底自动化并且不依赖预先准备的自定义模板我们选择方案二构建后处理脚本注入。这样无论使用哪个基础模板甚至是默认模板我们的脚本都能在最后为其“赋能”添加上全屏功能。3. 核心脚本实现详解接下来我们将把上述方案转化为具体的C#脚本。我们会在Unity项目中创建一个Editor文件夹因为构建相关的脚本必须放在Editor下并在其中放置我们的处理器脚本。3.1 构建前处理器WebGLBuildPreprocessor.cs这个脚本负责在构建开始前统一设置Player Settings和压缩格式。using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; public class WebGLBuildPreprocessor : IPreprocessBuildWithReport { // 定义回调顺序数字越小越先执行。通常设为默认值即可。 public int callbackOrder { get { return 0; } } // 构建开始前自动调用的方法 public void OnPreprocessBuild(BuildReport report) { // 检查当前构建平台是否为WebGL不是则直接返回 if (report.summary.platform ! BuildTarget.WebGL) { Debug.Log($[WebGLAutoConfig] 当前构建平台为 {report.summary.platform}非WebGL跳过自动配置。); return; } Debug.Log([WebGLAutoConfig] 开始自动配置WebGL构建参数...); // 1. 设置“Run In Background” - 这对WebGL后台运行至关重要 if (!PlayerSettings.runInBackground) { PlayerSettings.runInBackground true; Debug.Log(已设置: PlayerSettings.runInBackground true); } // 2. 设置默认屏幕分辨率可选但建议设置 // 这里设置为1920x1080你也可以从配置文件中读取 int defaultWidth 1920; int defaultHeight 1080; if (PlayerSettings.defaultWebScreenWidth ! defaultWidth || PlayerSettings.defaultWebScreenHeight ! defaultHeight) { PlayerSettings.defaultWebScreenWidth defaultWidth; PlayerSettings.defaultWebScreenHeight defaultHeight; Debug.Log($已设置: 默认分辨率 {defaultWidth}x{defaultHeight}); } // 3. 设置压缩格式为Brotli推荐压缩率更高 // 如果目标环境明确不支持Brotli可切换为WebGLCompressionFormat.Gzip var targetCompression PlayerSettings.WebGL.compressionFormat; var desiredCompression WebGLCompressionFormat.Brotli; if (targetCompression ! desiredCompression) { PlayerSettings.WebGL.compressionFormat desiredCompression; Debug.Log($已设置: 压缩格式 {desiredCompression}); } // 4. 可选但推荐禁用异常堆栈跟踪以减小.wasm文件体积 // PlayerSettings.WebGL.exceptionSupport WebGLExceptionSupport.None; // Debug.Log(已设置: 异常支持 None (减小构建体积)); Debug.Log([WebGLAutoConfig] WebGL构建参数自动配置完成。); } }关键点解析与注意事项IPreprocessBuildWithReport接口这是Unity提供的标准构建管线扩展点。实现该接口的脚本会在构建开始时被自动调用。平台判断首先检查BuildReport.summary.platform非常重要。避免在打包Android或iOS时也执行了WebGL的配置导致意外修改。PlayerSettings.runInBackground这个属性是全局的对所有平台生效。将其设为true后即使你之后打包其他平台它也会保持为真。这在大多数情况下是好事比如PC独立游戏也希望后台运行但如果你有特殊需求可能需要更精细的控制。压缩格式选择我们首选Brotli。但请注意要使Brotli压缩生效你的Web服务器如Nginx, Apache必须正确配置能对.br后缀的文件提供正确的Content-Encoding: br响应头。如果服务器环境不可控使用Gzip是更安全的选择。异常支持代码中被注释掉的WebGLExceptionSupport.None是一个激进的优化选项。设置后C#代码中的异常将不会生成详细的堆栈信息能显著减少发布后代码体积但会让调试变得极其困难你只能看到“发生了异常”而不知道在哪一行。建议在最终发布版本Release Build中启用在开发阶段保持默认的Full或ExplicitlyThrownExceptions。3.2 构建后处理器WebGLPostBuildProcessor.cs这个脚本负责在构建完成后修改生成的index.html注入全屏控制的JavaScript代码。using System.IO; using UnityEditor; using UnityEditor.Build; using UnityEditor.Build.Reporting; using UnityEngine; public class WebGLPostBuildProcessor : IPostprocessBuildWithReport { public int callbackOrder { get { return 0; } } public void OnPostprocessBuild(BuildReport report) { if (report.summary.platform ! BuildTarget.WebGL) { return; } string buildOutputPath report.summary.outputPath; Debug.Log($[WebGLAutoConfig] 构建完成输出路径: {buildOutputPath}); // 寻找index.html文件 string indexPath Path.Combine(buildOutputPath, index.html); if (!File.Exists(indexPath)) { // 有时模板可能生成不同的文件名如template.html这里需要根据实际情况调整 Debug.LogWarning($[WebGLAutoConfig] 未在{buildOutputPath}找到index.html全屏脚本注入失败。); return; } string htmlContent File.ReadAllText(indexPath); string fullscreenScript GetFullscreenJavaScriptSnippet(); // 检查是否已经注入过避免重复注入 if (htmlContent.Contains(toggleFullscreen)) { Debug.Log([WebGLAutoConfig] 检测到已存在全屏脚本跳过注入。); return; } // 寻找/body标签将脚本插入到其之前 // 这是一种常见且稳定的插入点 int bodyEndTagIndex htmlContent.LastIndexOf(/body); if (bodyEndTagIndex -1) { // 如果没有/body则在文件末尾插入 Debug.LogWarning([WebGLAutoConfig] 未找到/body标签将在文件末尾注入脚本。); htmlContent \n fullscreenScript; } else { htmlContent htmlContent.Insert(bodyEndTagIndex, fullscreenScript \n); } File.WriteAllText(indexPath, htmlContent); Debug.Log([WebGLAutoConfig] 全屏控制JavaScript脚本已成功注入index.html。); } private string GetFullscreenJavaScriptSnippet() { // 这里返回一个完整的全屏控制JS脚本。 // 它包含一个切换全屏的函数并尝试在页面加载后自动添加一个全屏按钮。 return !-- 由WebGL自动配置脚本注入的全屏控制功能 -- script typetext/javascript var unityInstance null; // 将由Unity加载器赋值 // 全屏切换函数 function toggleFullscreen() { if (!document.fullscreenElement) { // 进入全屏 var docElm document.documentElement; if (docElm.requestFullscreen) { docElm.requestFullscreen(); } else if (docElm.mozRequestFullScreen) { /* Firefox */ docElm.mozRequestFullScreen(); } else if (docElm.webkitRequestFullscreen) { /* Chrome, Safari Opera */ docElm.webkitRequestFullscreen(); } else if (docElm.msRequestFullscreen) { /* IE/Edge */ docElm.msRequestFullscreen(); } } else { // 退出全屏 if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } } } // 监听全屏变化事件可以用于更新按钮文字等UI状态可选 document.addEventListener(fullscreenchange, handleFullscreenChange); document.addEventListener(mozfullscreenchange, handleFullscreenChange); document.addEventListener(webkitfullscreenchange, handleFullscreenChange); document.addEventListener(msfullscreenchange, handleFullscreenChange); function handleFullscreenChange() { var fsButton document.getElementById(fullscreenButton); if (fsButton) { fsButton.textContent document.fullscreenElement ? 退出全屏 : 进入全屏; } } // 页面加载后尝试在Unity容器旁添加一个全屏按钮 function addFullscreenButton() { // 寻找Unity容器通常类名是unity-container或webgl-content var container document.querySelector(.webgl-content) || document.querySelector(.unity-container) || document.querySelector(#unityContainer); if (!container) { console.warn(未找到Unity容器无法自动添加全屏按钮。); return; } // 创建按钮 var button document.createElement(button); button.id fullscreenButton; button.textContent 进入全屏; button.style.cssText position: absolute; top: 10px; right: 10px; z-index: 1000; padding: 8px 15px; background: rgba(0,0,0,0.6); color: white; border: 1px solid #ccc; border-radius: 4px; cursor: pointer; font-size: 14px; ; button.onclick toggleFullscreen; // 将按钮插入到容器中 container.style.position relative; // 确保容器是相对定位按钮才能绝对定位在其中 container.appendChild(button); console.log(全屏按钮已自动添加。); } // 当Unity实例创建后调用此函数来关联实例并添加按钮 function registerUnityInstance(instance) { unityInstance instance; // 可以在这里将全屏功能与Unity实例关联例如通过JSLib调用C#函数 // unityInstance.Module.fullscreenHandler toggleFullscreen; addFullscreenButton(); } // 如果Unity加载器调用了特定的回调可以在这里挂钩 // 例如对于2022 LTS的模板可能会触发unityGame实例化 // 这是一个备用的初始化钩子 document.addEventListener(DOMContentLoaded, function() { // 延迟执行确保Unity加载器可能已经运行 setTimeout(addFullscreenButton, 1000); }); /script ; } }关键点解析与注意事项IPostprocessBuildWithReport接口与预处理对应在构建完成后执行。文件路径定位构建输出路径outputPath由BuildReport提供。我们假设主入口文件是index.html这是Unity WebGL模板的默认名称。如果你使用了高度自定义的模板或重命名了文件需要修改这里的查找逻辑。脚本注入策略我们选择在/body标签前插入。这确保了DOM元素已加载我们的脚本可以安全地操作它们如添加按钮。同时将JS代码放在body末尾是常见的性能优化手段避免阻塞页面渲染。防重复注入通过检查HTML内容是否包含我们函数的关键字如toggleFullscreen可以避免在多次构建时重复注入代码导致HTML文件臃肿。全屏API的浏览器兼容性JavaScript全屏API有带前缀的版本webkit,moz,ms。我们的代码包含了所有主流浏览器的前缀确保了最大兼容性。自动添加按钮addFullscreenButton函数尝试智能地找到Unity画布容器并动态添加一个全屏按钮。这是为了提供“开箱即用”的体验。但是这依赖于容器有特定的CSS类名.webgl-content,.unity-container或ID#unityContainer。你需要根据你实际使用的WebGL模板结构调整这里的查询选择器。最稳妥的方法是在你的自定义模板中预留一个按钮位置然后通过脚本控制其显示和功能。与Unity实例的交互registerUnityInstance函数预留了接口。如果你需要在C#中控制全屏例如游戏内按ESC键退出全屏你需要创建.jslib文件来桥接JS和C#并在这里进行绑定。对于大多数“自动搞定”的需求一个外部的JS按钮已经足够。4. 完整工作流与集成测试将上述两个脚本放入项目的Assets/Editor目录下后整个自动化流程就已经就绪了。下面我们来走一遍完整的发布和测试流程确保每一步都按预期工作。4.1 一键构建验证准备一个简单的Unity场景随便创建一个立方体或一个简单UI用于验证构建结果。打开Build SettingsFile - Build Settings确保平台切换到WebGL。执行构建点击Build或Build And Run。选择输出文件夹例如Builds/WebGL。观察Console日志在构建过程中Console窗口应该会依次出现来自我们两个脚本的Debug Log[WebGLAutoConfig] 开始自动配置WebGL构建参数...已设置: PlayerSettings.runInBackground true...[WebGLAutoConfig] 构建完成输出路径: ...[WebGLAutoConfig] 全屏控制JavaScript脚本已成功注入index.html。这些日志确认了我们的自动化脚本被成功触发并执行。4.2 构建结果检查构建完成后打开输出文件夹如Builds/WebGL。检查文件压缩查看生成的.data.br或.data.gz文件取决于你设置的压缩格式。如果看到.br或.gz后缀说明压缩设置已生效。如果只有.data文件则说明压缩未启用需要检查脚本中的PlayerSettings.WebGL.compressionFormat设置是否正确以及构建日志是否有错误。检查index.html用文本编辑器打开index.html滚动到文件底部在/body标签之前你应该能看到我们注入的一大段script代码里面包含了toggleFullscreen等函数。这证明后处理脚本成功运行。4.3 本地服务器测试WebGL构建不能直接通过浏览器打开file://协议运行需要启动一个本地HTTP服务器。使用Python快速启动服务器需安装Python# 在构建输出目录Builds/WebGL下打开终端/命令行 python -m http.server 8000使用Node.js的http-server需安装Node.js# 全局安装 npm install -g http-server # 在构建目录运行 http-server -p 8000浏览器访问打开浏览器输入http://localhost:8000。功能验证全屏按钮页面加载后你应该能在Unity游戏画面的右上角或根据脚本中CSS定位的位置看到一个“进入全屏”按钮。点击它页面应该能切换到浏览器全屏模式按钮文字变为“退出全屏”。后台运行切换到另一个浏览器标签页等待几秒再切回来。如果你的游戏有连续的动作比如一个旋转的立方体它应该没有暂停这表明runInBackground设置生效了。压缩验证打开浏览器的开发者工具F12切换到Network网络标签页刷新页面。查看加载的.data.br或.js.br等文件在响应头Response Headers中应该能看到content-encoding: br或gzip。这表示服务器正确提供了压缩后的文件传输体积会小很多。5. 进阶配置与疑难排错在实际项目中你可能会遇到更复杂的需求或环境问题。下面是一些进阶技巧和常见问题的解决方案。5.1 如何根据构建类型开发/发布应用不同配置你可能希望在开发构建时保留完整的异常堆栈以便调试而在发布构建时启用最高压缩并移除调试信息。我们可以通过判断BuildOptions来实现。修改WebGLBuildPreprocessor.cs中的OnPreprocessBuild方法public void OnPreprocessBuild(BuildReport report) { if (report.summary.platform ! BuildTarget.WebGL) return; bool isDevelopmentBuild (report.summary.options BuildOptions.Development) ! 0; Debug.Log($[WebGLAutoConfig] 构建类型: {(isDevelopmentBuild ? 开发版 : 发布版)}); // 基础设置始终应用 PlayerSettings.runInBackground true; PlayerSettings.defaultWebScreenWidth 1920; PlayerSettings.defaultWebScreenHeight 1080; // 根据构建类型差异化设置 if (isDevelopmentBuild) { // 开发构建使用Gzip兼容性更好保留异常信息 PlayerSettings.WebGL.compressionFormat WebGLCompressionFormat.Gzip; PlayerSettings.WebGL.exceptionSupport WebGLExceptionSupport.FullWithStacktrace; // 或 ExplicitlyThrownExceptions Debug.Log(开发构建配置Gzip压缩完整异常支持。); } else { // 发布构建使用Brotli压缩率更高禁用异常堆栈以减小体积 PlayerSettings.WebGL.compressionFormat WebGLCompressionFormat.Brotli; PlayerSettings.WebGL.exceptionSupport WebGLExceptionSupport.None; Debug.Log(发布构建配置Brotli压缩无异常堆栈支持。); } }5.2 全屏按钮样式冲突或位置不对我们的后处理脚本尝试自动定位Unity容器并添加按钮。如果按钮没出现或位置错乱大概率是CSS选择器没匹配到正确的DOM元素。排查步骤用浏览器打开构建后的页面按F12打开开发者工具。使用元素检查器Inspector查看Unity游戏画面周围的HTML结构。找到最外层包裹Canvas的那个div。记下它的id或class。回到WebGLPostBuildProcessor.cs的addFullscreenButton函数中修改document.querySelector的选择器字符串。例如如果你发现容器是div idgameContainer就改成document.querySelector(#gameContainer)。如果你希望按钮放在一个绝对固定的位置比如相对于整个浏览器窗口可以将container.appendChild(button)改为document.body.appendChild(button)并调整button.style.cssText中的position为fixed。更稳健的方案直接修改或创建自定义WebGL模板。在模板的HTML文件中预留一个button idfullscreenBtn然后在注入的JS脚本中直接获取这个按钮并绑定事件而不是动态创建。这样可以完全控制按钮的样式和位置。5.3 构建后处理器没有运行如果构建完成后没有看到注入脚本的日志或者index.html没有被修改请检查脚本位置确保WebGLPostBuildProcessor.cs文件放在Assets/Editor或任何名为Editor的文件夹下。编译错误检查Unity Console是否有任何编译错误。即使脚本有语法错误Unity也可能不会主动提示构建处理器失效但会导致其不被执行。接口实现确保类实现了IPostprocessBuildWithReport接口并且callbackOrder属性有正确的getter。输出路径权限确认Unity进程有权限写入构建输出目录。在一些受限制的目录如系统盘根目录可能会写入失败。5.4 服务器不支持Brotli压缩怎么办如果你将构建包部署到服务器后发现.br文件被直接下载而不是被解压说明服务器没有配置Brotli支持。解决方案回退到Gzip在构建预处理脚本中将压缩格式固定设置为WebGLCompressionFormat.Gzip。Gzip的支持几乎是100%的。配置服务器对于Nginx需要安装ngx_brotli模块并配置。这是一个进阶操作。对于Apache需要安装brotli模块。对于静态托管服务如GitHub Pages, Netlify, Vercel这些平台通常自动支持Brotli无需额外配置。对于IIS需要安装IIS Brotli扩展。最佳实践在自动化构建脚本中可以添加一个配置选项允许通过编辑器菜单或配置文件来切换压缩格式以适应不同的部署环境。5.5 如何与CI/CD流水线集成这套自动化脚本天生适合集成到CI/CD如Jenkins, GitLab CI, GitHub Actions中。关键在于CI机器上的Unity批处理模式Batch Mode构建同样会触发这些预处理和后处理脚本。在CI中的典型命令/path/to/Unity -quit -batchmode -projectPath /path/to/your/project -executeMethod UnityEditor.BuildPipeline.BuildPlayer -buildTarget WebGL -buildPath ./WebGLBuild当这个命令执行时我们的WebGLBuildPreprocessor和WebGLPostBuildProcessor会自动运行确保每次CI构建产出的WebGL包都具备一致的、优化过的配置。你甚至可以在CI脚本中根据分支如main分支用Brotli发布配置develop分支用Gzip开发配置来传递参数进一步动态化构建过程。整个流程下来你会发现原本需要多次点击、反复检查的发布工作现在只需要点击一次构建或者由CI系统自动触发就能得到一个配置完善、功能完整的WebGL版本。这不仅提升了效率更重要的是消除了人为失误的风险让发布过程变得可靠且可重复。