ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Unity WebGL跨平台自动全屏横屏适配方案:Windows、安卓、iOS全兼容

Unity WebGL跨平台自动全屏横屏适配方案:Windows、安卓、iOS全兼容

简介:这份资源是面向Unity开发者的WebGL多平台适配Demo,重点解决Unity WebGL打包后在Windows浏览器、Android与iOS移动设备上自动全屏并强制横屏显示的问题。内容围绕Screen.fullScreen与Screen.orientation的调用、Application.platform平台检测、WebGL与浏览器JavaScript桥接交互,以及移动端屏幕旋转生命周期处理等关键环节展开,适合已具备Unity基础、正在做跨端发布适配的开发者参考。压缩包共145个文件,约1.35MB,包含65个meta、25个asset、13个shader、10个png以及cs脚本、jslib桥接文件、unity场景、json配置、html页面等,覆盖工程配置、着色器、资源与WebGL交互模板等模块。目前已有1745人学习下载。通过研究其中的示例工程,读者可快速掌握多平台全屏横屏的落地写法,减少自行摸索浏览器安全策略与移动端旋转适配的时间成本。

1. 从一次翻车说起:为什么Unity WebGL的全屏横屏值得单独做一套方案

去年帮一个做教育模拟软件的团队处理过一件事:他们在Unity里开发了一套三维交互课件,PC端跑得好好的,打包成WebGL放到浏览器里也没问题,但一放到Windows的桌面壳、安卓平板和iPad上,画面要么缩在左上角一小块,要么竖屏显示导致操作区域被裁掉一半。用户反馈很直接——“打开就是歪的,点不到按钮”。这不是个例。Unity WebGL打包后在Windows平台、安卓苹果移动设备上自动全屏横屏,看起来只是“显示适配”四个字,实际涉及浏览器全屏API调用时机、移动端方向锁定、Canvas尺寸重算、输入坐标映射四件事,任何一环没对齐都会翻车。这套方案要解决的就是:让同一份WebGL构建产物,在桌面浏览器壳、安卓WebView、iOS WKWebView里都能自动进入全屏并锁定横屏,不需要用户手动点全屏按钮,也不需要为每个平台单独出一套包。适合正在做跨平台交互应用、在线实验、虚拟仿真课设的开发者直接参考。

2. 需求拆解与技术选型:四个平台差异到底卡在哪

2.1 先明确“自动全屏横屏”的边界条件

很多人一上来就写代码,结果发现iOS上死活不生效。问题出在没定义清楚“自动”的触发条件。浏览器安全策略要求全屏必须由用户手势触发,比如click、touchstart,不能在页面加载完成后直接调requestFullscreen。所以“自动”的真实含义是:用户第一次点击或触摸页面任意位置时,立即进入全屏并锁定方向,后续不再需要额外操作。这个边界条件定下来,方案才能往下走。

另一个边界是横屏锁定。桌面浏览器没有方向锁定API,只能靠CSS和Canvas尺寸适配来模拟横屏布局;安卓Chrome和iOS Safari支持screen.orientation.lock,但iOS只在全屏模式下才允许调用。所以方案必须分两层:一层是“进入全屏”,一层是“锁定方向”,两层在不同平台上的可用性不一样,需要做能力检测和降级处理。

2.2 平台差异对照:Windows、安卓、iOS各卡在哪

平台全屏API可用性方向锁定可用性主要卡点
Windows浏览器requestFullscreen可用不支持lock需CSS模拟横屏布局
安卓WebViewrequestFullscreen可用screen.orientation.lock可用需用户手势触发
iOS WKWebViewwebkitRequestFullscreen可用仅全屏后lock可用必须手势触发且全屏后才能锁
桌面壳内嵌取决于壳的权限通常不支持需壳层配合设置窗口尺寸

这张表是方案设计的核心依据。Windows端不要指望方向锁定,老老实实做Canvas等比缩放和CSS旋转;安卓和iOS要利用原生API,但必须把调用顺序排对——先全屏,再锁方向,顺序反了iOS直接报错。

2.3 技术选型:为什么用Unity官方WebGL模板加一层JS桥

Unity WebGL构建产物默认带一个index.html模板,里面已经包含了Canvas创建和Unity加载逻辑。我的做法是不改Unity工程里的C#业务代码,只在模板层面加一个独立的JS文件,通过Unity的jslib机制或者直接在模板里注入脚本,监听用户手势并调用全屏和方向锁定。这样做的原因是:业务逻辑和显示适配解耦,后续换Unity版本或换模板,适配层可以单独维护。

选型上对比过三种方案:一是纯CSS媒体查询,只能做布局适配,做不到真全屏和方向锁定;二是用第三方全屏库,但移动端兼容性参差不齐,而且引入额外依赖;三是自己写一个轻量适配层,代码量不到200行,可控性最强。最终选了第三种,下面把关键实现拆开讲。

3. 总体设计与核心模块:适配层怎么搭、代码怎么写

3.1 适配层架构:三个模块各管一件事

整个适配层分成三个模块:手势监听模块、全屏控制模块、方向与尺寸适配模块。手势监听模块负责在页面首次交互时触发后续流程;全屏控制模块封装了不同浏览器的全屏API差异,统一成一个enterFullscreen方法;方向与尺寸适配模块负责调用方向锁定API,并在锁定失败时用CSS和Canvas重算做降级。

模块之间的调用关系是:手势监听触发后,先调全屏控制,全屏成功回调里再调方向锁定,方向锁定失败则走尺寸适配降级。这个顺序不能乱,iOS上尤其严格。

3.2 全屏控制模块:兼容写法和调用时机

// fullscreen-controller.js // 统一封装不同浏览器的全屏API,返回Promise便于链式调用 function enterFullscreen(element) { return new Promise((resolve, reject) => { // 标准API、WebKit前缀、MS前缀依次尝试 const requestMethod = element.requestFullscreen || element.webkitRequestFullscreen || element.msRequestFullscreen; if (!requestMethod) { reject(new Error('当前环境不支持全屏API')); return; } // 必须由用户手势触发,否则浏览器会拒绝 requestMethod.call(element).then(resolve).catch(reject); }); } // 监听首次用户交互,只触发一次 let hasTriggered = false; function bindFirstGesture(element) { const handler = () => { if (hasTriggered) return; hasTriggered = true; enterFullscreen(element) .then(() => { // 全屏成功后再尝试锁定方向 lockOrientation(); }) .catch((err) => { console.warn('全屏失败,走降级方案', err); fallbackResize(); }); // 移除监听,避免重复触发 document.removeEventListener('click', handler); document.removeEventListener('touchstart', handler); }; document.addEventListener('click', handler); document.addEventListener('touchstart', handler); }

这段代码的关键点有三个。第一,requestFullscreen必须由用户手势触发,所以绑在click和touchstart上,且只触发一次。第二,不同浏览器前缀不同,用短路运算依次取,取不到就reject,让上层走降级。第三,全屏成功后才调lockOrientation,这个顺序在iOS上是硬性要求,反了会直接抛异常。

参数说明:element传的是Unity的Canvas容器,通常是#unity-canvas或#gameContainer。如果传document.documentElement,全屏后整个页面包括背景都变黑,视觉上更干净,但要注意Canvas尺寸重算的时机。

3.3 方向锁定与尺寸适配:成功路径和降级路径

// orientation-adapter.js // 尝试锁定横屏,失败则用CSS和Canvas重算模拟 function lockOrientation() { if (screen.orientation && screen.orientation.lock) { screen.orientation.lock('landscape') .then(() => { console.log('方向锁定成功'); resizeCanvasToFit(); }) .catch((err) => { console.warn('方向锁定失败,走尺寸适配', err); fallbackResize(); }); } else { // 桌面浏览器不支持lock,直接走尺寸适配 fallbackResize(); } } // 尺寸适配:按横屏比例重算Canvas大小,保持内容不裁切 function resizeCanvasToFit() { const canvas = document.querySelector('#unity-canvas'); if (!canvas) return; const targetRatio = 16 / 9; // 横屏目标比例,可按项目调整 const winW = window.innerWidth; const winH = window.innerHeight; let newW, newH; if (winW / winH > targetRatio) { // 窗口比目标更宽,以高度为准 newH = winH; newW = winH * targetRatio; } else { // 窗口比目标更窄,以宽度为准 newW = winW; newH = winW / targetRatio; } canvas.style.width = newW + 'px'; canvas.style.height = newH + 'px'; canvas.style.position = 'absolute'; canvas.style.left = (winW - newW) / 2 + 'px'; canvas.style.top = (winH - newH) / 2 + 'px'; } // 降级方案:不锁方向,只做尺寸适配,并提示用户旋转设备 function fallbackResize() { resizeCanvasToFit(); // 可选:显示一个旋转提示浮层,引导用户手动横屏 const tip = document.createElement('div'); tip.innerText = '请将设备旋转至横屏以获得最佳体验'; tip.style.cssText = 'position:fixed;bottom:20px;left:50%;transform:translateX(-50%);' + 'background:rgba(0,0,0,0.7);color:#fff;padding:8px 16px;border-radius:4px;' + 'font-size:14px;z-index:9999;'; document.body.appendChild(tip); // 3秒后自动消失 setTimeout(() => tip.remove(), 3000); }

这段代码把成功路径和降级路径都覆盖了。lockOrientation先检测screen.orientation.lock是否存在,存在就尝试锁定,失败或不存在都走fallbackResize。resizeCanvasToFit按16:9比例重算Canvas尺寸并居中,保证内容不被裁切。fallbackResize额外加了一个旋转提示浮层,提升用户体验。

参数说明:targetRatio可以根据项目实际设计分辨率调整,比如4:3或21:9。canvas选择器要跟Unity模板里的id一致,默认是#unity-canvas,如果改过模板要同步改。

3.4 在Unity模板里挂载适配层

<!-- index.html 模板片段 --> <canvas id="unity-canvas"></canvas> <script src="fullscreen-controller.js"></script> <script src="orientation-adapter.js"></script> <script> // Unity加载完成后绑定手势监听 createUnityInstance(canvas, config).then((unityInstance) => { // 绑定首次手势,触发全屏和方向锁定 bindFirstGesture(document.documentElement); }); </script>

挂载顺序很重要:先引入两个适配脚本,再在Unity实例创建完成后的then回调里绑定手势。不要在DOMContentLoaded里绑,因为那时Canvas可能还没创建完。另外,如果项目用了自定义模板,要确保这段代码在Unity加载逻辑之后执行。

4. 避坑与常见问题:五个最容易翻车的点

4.1 iOS上全屏后方向锁不住

现象:安卓上一切正常,iOS上全屏成功了但画面还是竖屏。原因:iOS的screen.orientation.lock必须在全屏模式下调用,而且全屏和锁定之间不能有异步延迟,否则手势上下文丢失。解决:把lockOrientation直接放在enterFullscreen的then回调里同步调用,不要用setTimeout包一层。

4.2 桌面浏览器全屏后Canvas模糊

现象:Windows浏览器全屏后画面变糊,文字边缘有锯齿。原因:Canvas的CSS尺寸变了但绘图缓冲区尺寸没变,浏览器做了拉伸。解决:在resizeCanvasToFit里同时设置canvas.width和canvas.height为CSS尺寸乘以devicePixelRatio,然后调Unity的resize事件通知引擎重绘。

4.3 安卓WebView里全屏API不生效

现象:在安卓原生壳的WebView里,requestFullscreen返回reject。原因:WebView默认没开启全屏权限,需要在壳层设置WebChromeClient并实现onShowCustomView。解决:如果壳层可控,让壳层配合;如果不可控,走fallbackResize降级,至少保证布局正确。

4.4 用户手势触发后页面滚动被锁死

现象:全屏后页面无法滚动,退出全屏也恢复不了。原因:全屏时给body加了overflow:hidden,退出时没移除。解决:监听fullscreenchange事件,在退出全屏时移除overflow:hidden和旋转提示浮层,恢复页面原始状态。

4.5 多次点击导致重复全屏和报错

现象:用户快速点两下,控制台报“全屏请求被拒绝”。原因:第一次全屏还没完成,第二次请求就来了,浏览器拒绝。解决:用一个isFullscreening标志位做防抖,请求发出后置true,fullscreenchange事件里置false,期间忽略后续请求。

5. 验收与答辩:怎么证明这套方案真的跑通了

验收这套方案,不能只看“我这边能全屏”就完事。我一般会按下面这张表逐项过,每项都要在真实设备或模拟器上跑一遍,截图留档。

验收项测试方法通过标准
Windows全屏Chrome/Edge打开,点击页面进入全屏,Canvas居中无裁切
Windows退出全屏按Esc恢复窗口,页面可滚动
安卓全屏+锁方向安卓Chrome打开,点击页面全屏且横屏,旋转设备画面不转
iOS全屏+锁方向Safari打开,点击页面全屏且横屏,无报错
降级路径禁用全屏API的环境布局正确,有旋转提示
重复点击快速点两下无控制台报错,状态正常

答辩时,评委最常问的是“为什么iOS上顺序不能反”和“降级方案怎么保证体验”。第一个问题用第4.1节的解释回答,第二个问题把fallbackResize的代码逻辑讲清楚——核心是保证内容不裁切、有引导提示。另外,建议准备一个对比演示:同一份构建产物,在没加适配层和加了适配层的页面上分别打开,让评委直观看到差异。

从那以后我每次做WebGL跨平台适配,都强制走一遍这张验收表,尤其是iOS的真机测试,模拟器上跑通不代表真机没问题。希望帮到你。

本文还有配套的精品资源,点击获取

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进