ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

Unity WebGL全屏显示:从浏览器API到分辨率与状态适配

Unity WebGL全屏显示:从浏览器API到分辨率与状态适配 去年接了个把 Unity 项目搬进浏览器的活美术和策划直接在网页上验收第一句话就是怎么点了全屏没反应。当时我以为加个按钮调一下浏览器的全屏接口就完事结果一头撞进坑里按钮点了没反应、全屏之后画面糊成一片、按 Esc 退出来 UI 还挂着全屏中的状态、嵌在 iframe 里干脆整套接口都摸不到。前前后后折腾了两三天才把 UnityWebGl 网页全屏显示这件事从能点做到能用再到敢交付。这篇东西就是把那几天的过程摊开讲。全屏在 Unity WebGL 里从来不是调一个 API的问题它横跨三层浏览器的视口控制、canvas 的绘制缓冲区、Unity 内部的相机与 UI 适配。任何一层没对齐表现出来都是全屏坏了但坏的地方完全不一样。下面这套内容适合三类人看刚把项目导出成 WebGL、被全屏按钮折磨过的开发者要交付给第三方嵌入、被 iframe 卡住的同学以及画面全屏后糊掉、变形、鼠标点不准找不到原因的人。代码可以直接抄思路可以直接套。1. 先把全屏拆成三层不然后面全是返工1.1 浏览器视口层全屏接口到底给了你什么浏览器提供的全屏能力本质上是让某一个 DOM 元素脱离页面布局独占整个屏幕。它的调用入口是元素上的requestFullscreen()退出入口在document上叫exitFullscreen()。这两个动作都有一个硬前提必须由用户的真实交互触发。你在定时器里调、在数据加载完成后的回调里调浏览器一律拒绝而且拒绝方式很安静多数情况下只在控制台留一条警告。判断当前是否处于全屏不看按钮状态看document.fullscreenElement——它有值就说明有元素正在独占屏幕值是null就说明没有。这个判断比维护一个布尔变量可靠得多因为用户随时可以按 Esc 或用系统手势退出你的变量根本收不到通知。状态变化靠事件监听fullscreenchange在进入和退出时都会触发fullscreenerror只在失败时触发。这两个事件是你做 UI 同步的唯一正确入口后面第 4 节会重点讲。还有一个容易忽略的开关document.fullscreenEnabled。它是false的时候说明当前环境从策略层面就不允许全屏典型场景就是被嵌在一个没有授予全屏权限的 iframe 里。先查这个值能省掉一半的排查时间。注意老版本 WebKit 内核用的是带前缀的写法webkitRequestFullscreen/webkitfullscreenElement移动端部分浏览器至今仍然只认带前缀的版本。稳妥的封装里要把这两套都写上而不是只调标准接口。1.2 画布层canvas 的显示尺寸和绘制尺寸是两回事这一层是绝大多数全屏后画面糊的根源我见过太多人卡在这。canvas 有两个尺寸概念。一个是 CSS 尺寸也就是它在页面上占多大地方由width/height样式或者父容器决定另一个是绘制缓冲区尺寸也就是canvas.width/canvas.height这两个属性它决定了实际有多少像素参与渲染。两者不一致的时候浏览器会把缓冲区拉伸到 CSS 尺寸上显示于是要么糊要么锯齿。Unity 的 WebGL 框架在启动时会根据发布配置给 canvas 设一个初始缓冲区尺寸之后窗口尺寸变化时它会自己重算。但这个重算依赖一个条件它读到的 canvas 的 CSS 尺寸必须是真实铺满容器的。如果你的容器用了transform: scale()、或者给 canvas 加了padding、或者父级有奇怪的定位方式框架读到的clientWidth就不是你以为的那个值重算出来的缓冲区尺寸自然也是错的。所以我在模板里的做法永远是让 canvas 老老实实width: 100%; height: 100%; display: block;父容器负责布局canvas 只负责填满中间不加任何花活。1.3 游戏内部层别指望Screen.fullScreen在 WebGL 上生效Unity 的Screen.fullScreen和Screen.SetResolution这套东西在 WebGL 平台上是失效的。原因很直白浏览器沙箱里的分辨率由 canvas 缓冲区决定Unity 运行时无权直接改操作系统层面的窗口尺寸它能做的只是改自己那块画布的像素数。这意味着你写在桌面端跑得好好的全屏逻辑搬到 WebGL 上会变成什么都不发生而且不报错。我第一版就是这么翻车的——C# 里判断了一下平台以为是编辑器模拟的问题导出来才发现是真不生效。正确的分工是全屏的进入和退出交给浏览器接口画面的清晰度和比例交给 canvas 尺寸与 Unity 相机适配。Unity 侧只负责发指令和收状态不负责真正的全屏动作。2. 方案选型真全屏、伪全屏还是两个都要2.1 三种常见落地方式的横向对比在动手之前先把可选路径摆清楚。我实测过的三种方式如下方式实现手段覆盖范围主要短板适用场景真全屏元素调用requestFullscreen()桌面端浏览器覆盖率很高安卓主流浏览器可用iOS 上的浏览器对任意元素支持极差必须用户手势触发桌面端为主的运营后台、展示型项目伪全屏CSS 把容器铺满视口position: fixedinset: 0 高层级全平台无差别可用浏览器地址栏、标签栏依然存在按 Esc 退不出移动端为主的 H5 小游戏、必须兼容 iOS 的项目混合方案优先真全屏失败或环境不支持时自动降级到伪全屏全平台代码量翻倍两套状态的同步要写清楚我实际交付的项目基本都用这个伪全屏的核心就三行 CSS容器固定定位、四边贴零、层级拉到足够高同时把body的滚动关掉。它看上去没有真正全屏但在移动端浏览器里用户感知上的差别远小于你的预期——尤其是当页面本身就没有其他内容的时候。真全屏和伪全屏最大的行为差异在于退出的方式真全屏下用户按 Esc 或系统返回手势会退出这个动作你拦不住只能监听伪全屏下退出只能靠你自己的按钮所以反而状态更可控。这个差异直接影响第 4 节的状态同步逻辑。2.2 我的选型结论默认走混合判断条件写死我现在的固定做法是先判断环境能力再决定走哪条路判断逻辑在下发指令那一刻执行而不是在页面加载时缓存。判断条件按顺序来document.fullscreenEnabled为true且当前元素上存在requestFullscreen方法走真全屏否则直接走伪全屏。iOS 上的浏览器基本都会落到第二条分支上这不是缺陷是现状提前接受它比事后打补丁省事得多。还有一层要考虑如果项目是嵌在 iframe 里交给第三方用的真全屏这条路还得额外满足两个条件——iframe 标签上要有allowfullscreen属性同时allow属性里要显式写明全屏权限。少任何一个document.fullscreenEnabled直接是false你连尝试的机会都没有。提示混合方案里最容易出问题的是降级后状态没对齐。比如真全屏失败了代码走了伪全屏分支但 Unity 侧收到的信号是进入成功UI 显示成退出全屏按钮用户点了才发现根本没全屏。所以指令的下发结果必须回传不能只发不收。3. 改造发布模板把控制权从 Unity 手里拿回来3.1 自定义模板目录怎么建名字和路径都是硬要求Unity 的 WebGL 发布模板放在工程目录下的Assets/WebGLTemplates/里。这个路径的名字、大小写、层级都不能错错一个字母在发布面板里就看不到你的模板。建目录的步骤很死板但每次新建工程我都得提醒自己一遍在Assets下建WebGLTemplates文件夹注意首字母大写、其余小写、中间不能有空格和下划线然后在这个文件夹里建你的模板文件夹比如叫FullscreenTpl把默认模板里的index.html复制进去再检查一下模板文件夹里有没有thumbnail.png——没有的话发布面板里那块预览图是空白的不影响功能但影响心情。发布的时候在 Player Settings 的 Resolution and Presentation 里把 WebGL Template 选成你的模板名。选完之后建议清一次浏览器缓存在本地跑一遍因为我遇到过发布配置改了但浏览器拿着旧 index.html 的情况排查半天以为是代码问题。模板里的占位符在不同 Unity 版本写法有差异。老版本用的是%UNITY_WIDTH%、%UNITY_HEIGHT%、%UNITY_WEBGL_LOADER_URL%、%UNITY_WEBGL_DATA_URL%这种百分号包裹的形式2020 之后的版本改成了{{{ UNITY_WIDTH }}}这种大括号形式。改模板之前先打开原始文件看一眼用的是哪套别照着一个四年前的教程硬改改完占位符没被替换页面白屏。3.2 index.html 的骨架改造容器、canvas、覆盖层三层结构原始模板的结构一般是一个外层容器包着 canvas 和加载条。我要改的是把容器角色拆清楚外层负责铺满视口中间层负责承载 canvas最上面放我自己的一层 UI 覆盖元素。结构大致长这样div idunity-viewport div idunity-container canvas idunity-canvas tabindex-1/canvas /div div idfs-ui button idfs-btn typebutton全屏/button /div /div几个细节值得说。canvas上的tabindex-1是为了让它能被脚本聚焦——退出全屏后如果不把焦点还给 canvas键盘输入会丢玩家按 WASD 没反应这是我在一个项目里被反馈了三次的 bug。#fs-ui这层要有pointer-events: none只给按钮开pointer-events: auto否则它会挡住 canvas 的鼠标事件全屏进去之后点哪儿都没反应。加载条的改动要谨慎。Unity 的加载进度是挂在原始结构上的直接删掉#unity-loading-bar相关内容会让createUnityInstance的进度回调无处可写。我的做法是保留结构只调整样式把加载条改成覆盖在 canvas 上方居中的形式加载完成后由框架自己隐藏。3.3 样式层撑满、去滚动条、处理移动端视口高度样式这块有三类坑我按顺序写。第一类是滚动条。html, body上要同时设margin: 0; padding: 0; overflow: hidden;。只设overflow: hidden而不清margin页面顶上会留 8 像素的白边全屏之后白边跟着进去看起来像画面没铺满。#unity-viewport上设position: fixed; inset: 0;比width: 100%; height: 100%更省心因为它不受父级定位影响。第二类是高度单位。移动端浏览器用100vh会有经典的地址栏问题——地址栏收起时100vh比可视区小地址栏展开时又比可视区大表现为画面下方被切掉一条或者出现一条空白。新一点的写法是用100dvh动态视口高度能跟着地址栏变化。但dvh在部分老设备上不认识所以要写成兜底叠加的形式#unity-viewport { position: fixed; inset: 0; height: 100vh; height: 100dvh; overflow: hidden; background: #1a1a1a; }后一行覆盖前一行认识的用dvh不认识的自动退回vh。这是我用了两年最稳的写法。第三类是 canvas 的显示尺寸#unity-container { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; } #unity-canvas { display: block; width: 100%; height: 100%; outline: none; background: #000; }display: block是为了干掉行内元素基线带来的底部空隙outline: none是为了去掉聚焦时的虚线框——那个框在游戏里特别扎眼。容器用 flex 居中是为了后面做等比留黑边的时候让 canvas 缩小后仍然居中不用手算偏移。4. 实操一套可复用的全屏控制代码4.1 JS 侧封装能力探测、进入、退出、状态回调先给一个能直接用的封装。几个设计取舍我在注释里写了理由。(function () { const el document.getElementById(unity-viewport); const btn document.getElementById(fs-btn); function canNative() { return !!(document.fullscreenEnabled (el.requestFullscreen || el.webkitRequestFullscreen)); } function isNative() { return !!(document.fullscreenElement || document.webkitFullscreenElement); } function applyPseudo(on) { if (on) { el.classList.add(pseudo-fs); document.body.style.overflow hidden; } else { el.classList.remove(pseudo-fs); document.body.style.overflow ; } notify(on ? enter : exit, pseudo); } function notify(state, mode) { // Unity 侧收到的结果一定要带 mode不然分不清是真全屏还是降级 if (window.__unityInstance) { window.__unityInstance.SendMessage(WebBridge, OnFullscreenChanged, JSON.stringify({ state: state, mode: mode })); } } function enter() { if (!canNative()) { applyPseudo(true); return; } const p el.requestFullscreen ? el.requestFullscreen({ navigationUI: hide }) : el.webkitRequestFullscreen(); if (p p.catch) { p.catch(function () { applyPseudo(true); }); } } function exit() { if (isNative()) { const p document.exitFullscreen ? document.exitFullscreen() : document.webkitExitFullscreen(); if (p p.catch) { p.catch(function () {}); } } else { applyPseudo(false); } } function bind() { // 关键直接挂 DOM 点击不要经过 Unity 的输入链路转一圈再调 btn.addEventListener(click, function () { if (isNative() || el.classList.contains(pseudo-fs)) { exit(); } else { enter(); } }); [fullscreenchange, webkitfullscreenchange].forEach(function (ev) { document.addEventListener(ev, function () { const on isNative(); btn.textContent on ? 退出全屏 : 全屏; notify(on ? enter : exit, native); // 布局稳定后再触发重算见 5.3 setTimeout(function () { window.dispatchEvent(new Event(resize)); if (window.__canvas) { window.__canvas.focus(); } }, 120); }); }); } window.__fsApi { enter: enter, exit: exit, toggle: function () { if (isNative() || el.classList.contains(pseudo-fs)) exit(); else enter(); } }; window.addEventListener(DOMContentLoaded, bind); if (document.readyState ! loading) bind(); })();这里最值得单独拎出来讲的是按钮挂载的位置。很多教程给的方案是Unity 里做一个 UI 按钮点击后走 C# → jslib → 调requestFullscreen()。这条路在 Chromium 内核上通常能跑通因为用户激活状态有大约五秒的有效期但在 WebKit 内核上全屏调用必须严格发生在事件处理函数的调用栈里经过 Unity 的输入系统转一圈之后激活状态可能已经被判定失效表现就是点了完全没反应控制台报一个 attempt to use fullscreen API failure。所以我的做法是把全屏按钮做成 DOM 元素浮在 canvas 上面。这样做还有个额外好处全屏按钮在 Unity 加载完成之前就能用用户不会遇到点了没反应因为游戏还在加载的情况。代价是按钮样式要自己写不能复用游戏内的 UI 组件——但说实话一个全屏按钮而已两种状态、一个图标五分钟的事。4.2 Unity 侧C# 与 jslib 的桥接怎么写才不翻车Unity 调 JS 走 jslib文件放Assets/Plugins/WebGL/下扩展名是.jslib。内容是标准的mergeInto结构mergeInto(LibraryManager.library, { FS_Enter: function () { if (window.__fsApi) window.__fsApi.enter(); }, FS_Exit: function () { if (window.__fsApi) window.__fsApi.exit(); }, FS_Toggle: function () { if (window.__fsApi) window.__fsApi.toggle(); }, FS_IsActive: function () { var on !!(document.fullscreenElement || document.webkitFullscreenElement); var pseudo document.getElementById(unity-viewport); if (!on pseudo) on pseudo.classList.contains(pseudo-fs); return on ? 1 : 0; } });C# 这头包一层重点是条件编译不能少using System.Runtime.InteropServices; using UnityEngine; public static class FullscreenBridge { #if UNITY_WEBGL !UNITY_EDITOR [DllImport(__Internal)] private static extern void FS_Enter(); [DllImport(__Internal)] private static extern void FS_Exit(); [DllImport(__Internal)] private static extern void FS_Toggle(); [DllImport(__Internal)] private static extern int FS_IsActive(); #endif public static void Enter() { #if UNITY_WEBGL !UNITY_EDITOR FS_Enter(); #endif } public static void Exit() { #if UNITY_WEBGL !UNITY_EDITOR FS_Exit(); #endif } public static void Toggle() { #if UNITY_WEBGL !UNITY_EDITOR FS_Toggle(); #endif } public static bool IsActive { get { #if UNITY_WEBGL !UNITY_EDITOR return FS_IsActive() 1; #else return Screen.fullScreen; #endif } } }DllImport(__Internal)里的双下划线加 Internal 是固定写法写成别的名字在浏览器里会直接报找不到函数。另外编辑器下必须走#else分支否则 Play 模式一跑就抛异常一堆人第一次搭这套东西的时候就被这个卡住以为是 jslib 没生效。回过头来接收 JS 的回调需要一个挂在场景里的 GameObject名字要和 JS 里SendMessage的第一个参数完全一致。我一般叫它WebBridge挂在启动场景最上面的一个空物体上里面就这么点东西using UnityEngine; using System; public class WebBridge : MonoBehaviour { public static event Actionbool, string OnFullscreenChanged; // 由 JS 调用参数是 JSON 字符串 public void OnFullscreenChanged(string json) { var state json.Contains(\enter\); var mode json.Contains(native) ? native : pseudo; OnFullscreenChanged?.Invoke(state, mode); } }解析 JSON 那里我用字符串包含判断看起来很土。原因是这个回调调用频率极低用户点一次全屏才触发一次引入一个 JSON 库不值当而且 WebGL 下 IL2CPP 裁剪有可能把反射用到的字段裁掉我遇到过一次序列化出来全是默认值的情况从那以后这种极简场景我就手写了。4.3 状态双向同步别让 UI 和真实状态打架真全屏有个绕不过去的特性用户按 Esc 就能退出你拦不住也不该拦。所以游戏里显示全屏中实际早就退出来了这种状态错位只能靠事件监听解决靠按钮点击是解决不了的。我的同步规则是三条第一UI 状态只由事件驱动不由按钮点击驱动。点了按钮不改 UI等fullscreenchange回来再改。这样用户按 Esc 退出的时候UI 也会跟着变回全屏。第二Unity 侧的响应要区分模式。真全屏和伪全屏在游戏内的处理不一样真全屏下浏览器 UI 消失可视区变大相机需要重新算比例伪全屏下视口尺寸其实没变position: fixed的元素本来就是这个尺寸只是被提到了最上层相机不需要调整。如果不区分模式伪全屏下会出现画面被二次拉伸的诡异效果。第三进入和退出都要能被打断。全屏请求是异步的返回一个 Promise可能成功也可能失败。失败之后必须走降级而不是停在那里。我在enter()里挂的.catch就是干这个的。注意不要在fullscreenchange里立刻读尺寸。这个事件触发的时候布局往往还没稳定读到的clientWidth可能是旧值重算出来的缓冲区尺寸就是错的。加一个 100 到 150 毫秒的延迟或者用requestAnimationFrame套两层实测都行。4.4 iframe 嵌入这两个属性少一个就白干交付给第三方嵌入的时候问题基本都出在 iframe 上。宿主页面那边iframe标签必须同时带上allowfullscreen和allowfullscreen。前一个是老式写法后一个是新式的权限策略写法两个都写是为了覆盖不同版本的浏览器。只写一个的话在某些组合下全屏请求会被静默拒绝。嵌入方这边你的index.html里没法自己给自己授权。所以能力探测这一步特别重要document.fullscreenEnabled返回false的时候立刻走伪全屏别去尝试真全屏。尝试失败的后果是用户点了按钮画面完全没变化连降级都不会触发——因为 Promise 被拒绝了而你如果没写 catch那就什么都不会发生。还有一个更麻烦的情况如果宿主和 iframe 不同源你连fullscreenEnabled都可能读不到真实值。这种场景我一般直接默认走伪全屏同时给宿主方提供一段说明告诉他们如果要真全屏需要在 iframe 上加哪些属性。把选择权交出去比自己猜要靠谱。如果游戏里有需要鼠标锁定的操作比如第一人称视角requestPointerLock也有类似的权限要求iframe 上要额外加allowfullscreen; pointer-lock。这个细节我在一个项目里漏了导致嵌入版里视角拖不动查了两天才发现问题在宿主页面的 iframe 标签上。5. 全屏之后才是真正的难点分辨率与画面比例5.1 DPR 与渲染分辨率清晰度和显存的拉扯进全屏之后画面变糊八成是 DPR 的问题。window.devicePixelRatio在普通笔记本上一般是 1 或 1.25在高端显示器上是 2在手机上普遍是 2 到 3。Unity 的 WebGL 框架会读取这个值把 canvas 的缓冲区尺寸按 DPR 放大这样在高分屏上画面才清晰。问题在于这个放大是平方级的。假设你的可视区是 1920×1080DPR 是 2缓冲区就是 3840×2160像素数是原来的四倍。WebGL 的填充率、显存占用、后期处理的采样成本全部翻四倍。我一个项目在开发机上跑得挺流畅到了配置一般的机器上开了全屏直接掉到二十帧原因就是这个。我的处理原则是给 DPR 设一个上限并且允许玩家自己调。具体做法是在createUnityInstance之前先覆写一次像素比(function () { const MAX_DPR 1.5; const raw window.devicePixelRatio || 1; const capped Math.min(raw, MAX_DPR); if (capped raw) return; Object.defineProperty(window, devicePixelRatio, { get: function () { return capped; }, configurable: true }); })();这段必须在 Unity 的加载脚本执行之前跑放在index.html里加载脚本的上方就行。1.5 这个值是我反复试出来的折中从 1 提到 1.5清晰度提升肉眼可见从 1.5 提到 2多数场景下几乎看不出差别但性能开销是实打实的。另外提一句Application.targetFrameRate在 WebGL 里是生效的设成 60 配合浏览器的刷新节奏比较稳。但要注意标签页切到后台时浏览器会主动降频这时候测出来的帧率没有参考价值测性能必须保证标签页在前台。5.2 宽高比变化拉伸、裁切还是留黑边从普通页面进全屏可视区的宽高比一定变了。原来是浏览器里嵌着一块 16:9 的区域全屏之后可能变成 16:10 甚至 21:9。这时候画面怎么处理只有三个选项。拉伸铺满canvas 直接 100% 撑满不改相机。画面会被横向或纵向拉伸圆形变椭圆角色变胖变瘦。除非你的游戏是纯 2D 抽象的否则别选这个。留黑边canvas 保持原始比例居中显示两边或上下留黑。做法是在容器的 flex 布局基础上给 canvas 设aspect-ratio: 16 / 9;同时max-width: 100%; max-height: 100%;。这是最省事、最不破坏画面构图的方式代价是浪费一部分屏幕面积。动态适配canvas 撑满然后 Unity 侧根据新的宽高比调整相机。正交相机调orthographicSize透视相机调fieldOfView或者用Camera.rect做视野扩展。这种方式观感最好但要处理多出来的视野里露出场景边界的问题——我们的做法是在场景四周预留一圈装饰性元素视野扩大时露出来也不突兀。实际项目里我用得最多的是留黑边 关键 UI 动态适配的组合。渲染层保持严格的比例保证画面不变形UI 层用CanvasScaler的Match Width Or Height模式match值设成 0.5让 UI 在宽高比变化时做等比缩放两边的锚点该贴边的贴边。这样一个 16:9 的工程在 21:9 的屏幕上画面居中留黑边UI 该在右上角的还在右上角不会跑到屏幕外面去。5.3 重算尺寸的时机与顺序顺序错了白算全屏切换之后尺寸重算涉及三个动作顺序有讲究第一等 CSS 布局落地。全屏切换会引起容器尺寸变化浏览器需要一帧来重新布局。这就是为什么fullscreenchange里不能立刻读尺寸。第二让 canvas 的 CSS 尺寸先正确。框架读的是clientWidth/clientHeight这两个值由 CSS 决定必须先对。第三触发重算。多数版本的做法是派发一个窗口resize事件让 Unity 的框架按它自己的逻辑去更新缓冲区。我实测下来的写法是function refreshCanvas() { requestAnimationFrame(function () { requestAnimationFrame(function () { window.dispatchEvent(new Event(resize)); }); }); }双requestAnimationFrame是为了保证布局一定已经完成。用setTimeout100 毫秒也行但rAF更精确而且不会在后台标签页里乱跑。如果你的版本框架重算不灵还有一个兜底方案手动改canvas.width/canvas.height然后等下一次框架自己校准。但我不推荐长期用这个因为手动改和框架内部的尺寸记录会打架容易出现改完一帧之后又被改回去的现象表现是画面抖一下。还有一点重算之后要主动把焦点还给 canvas。前面提过tabindex-1的用法配合canvas.focus()一起用。不做这一步的用户体验是全屏之后打字打不了、按方向键页面在滚动尤其在有聊天框或者输入功能的项目里必现。6. 踩坑记录与排查手册6.1 高频问题速查表下面这张表是我这几年攒下来的遇到现象先查这张表能覆盖八成情况。现象最可能的原因处理方式点按钮完全无反应调用不在用户手势的调用栈里或 iframe 缺全屏权限按钮挂 DOM 层直接监听检查allowfullscreen与allow全屏后画面模糊DPR 过高或 canvas 缓冲区未重算限制 DPR 上限全屏后延迟派发resize全屏后画面拉伸变形canvas 撑满但相机未适配用aspect-ratio留黑边或按宽高比重算相机按 Esc 退出后 UI 状态错乱UI 只由点击事件驱动没监听fullscreenchange改为事件驱动点击后不改状态全屏后鼠标点击位置偏移canvas 上有 padding、border 或transform: scale去掉所有影响clientRect的样式全屏后键盘输入失效焦点没回到 canvascanvas.focus()并加tabindex-1页面底部有一条空白用了100vh但移动端地址栏变化改用100dvh并保留vh兜底全屏按钮被画面挡住UI 层层级或pointer-events设置错误覆盖层pointer-events: none按钮单独开auto全屏进入后立刻自动退出同时调了两次进入/退出或和页面其他全屏逻辑冲突调用前先查fullscreenElement判断当前状态移动端全屏后无法横屏屏幕方向锁定未申请或不被支持先进入全屏再调screen.orientation.lock失败则改用 UI 提示6.2 几个不好查的疑难 case说三个我印象最深的。第一个是全屏后鼠标点不准。表现是角色能跑但点击 UI 按钮的位置偏了大概二十来个像素。查了半天发现是覆盖层的按钮用了padding而 Unity 计算鼠标位置的时候用的是 canvas 的包围盒两者坐标系看似一样实际因为覆盖层居中对齐多算了偏移。最后的解决方式是给 canvas 加一个明确的getBoundingClientRect打印在浏览器里对一遍一眼就看出来 canvas 的实际位置比视觉位置高了一截。这种问题的排查思路是不要猜把getBoundingClientRect()的四个值打出来和视觉位置对比。第二个是伪全屏下画面被放大了一倍。原因是伪全屏的 CSS 里写了transform: scale(1)之类的变换而 Unity 框架在算缓冲区尺寸的时候用的是offsetWidthtransform不改变offsetWidth但它会改变视觉呈现。视觉上放大了缓冲区没跟上于是糊。教训是布局不要用transform要用尺寸本身去控制。第三个是退出全屏后黑屏一瞬。原因是退出全屏时容器尺寸从大变小框架重算缓冲区还没完成浏览器已经用旧的缓冲区做了拉伸显示。视觉上就是一闪。处理方式是在容器上加一个短暂的背景色过渡或者干脆接受这一帧——如果追求极致可以在退出前先把 canvas 缩放到目标尺寸再退出全屏但这样代码复杂度会上去不少。6.3 我总结的几条硬规矩最后这几条是长期踩坑总结出来的写在这里当备忘。全屏按钮永远是 DOM 元素不做成 Unity 内 UI。这条前面说过再强调一次因为它能一次性解决点了没反应和加载期间不能用两个问题。能力探测在调用那一刻做不在加载时缓存。iframe 的权限、用户手势状态、浏览器策略这些都可能在运行期间变化。缓存一个布尔值看起来省事实际会埋雷。状态只认事件不认操作。UI 显示什么完全由fullscreenchange决定。用户按 Esc 退出这件事你管不了能管的只有自己的状态。降级路径必须是默认路径的一部分不是异常处理。我在enter()里把降级写在.catch里但更重要的是canNative()返回false的时候直接走伪全屏那条分支。这两条路在我这里地位是对等的。尺寸重算永远延迟一帧。任何一个进入/退出全屏之后立即读尺寸的写法都有概率读到旧值。加rAF不加成本不加就是玄学 bug。别在 iframe 里赌权限。拿不到fullscreenEnabled的时候直接告诉嵌入方需要加什么属性比自己尝试要快。这套代码现在是我们几个 WebGL 项目的标准模板新建工程直接拷过去改一下按钮位置和配色就能用。中间那层混合方案的设计其实是被逼出来的——刚开始只想做真全屏结果第一个交付的客户就用 iPad 验收当场翻车回来才把伪全屏补上。现在回头看早一点接受全屏不是一个按钮的事而是一套状态机能省下的时间远超写这套代码的时间。
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表