简介:这份资源是面向Unity开发者的WebGL多平台全屏横屏适配Demo,针对Unity项目打包为WebGL后在Windows浏览器、Android与iOS移动设备上无法自动全屏、屏幕方向不受控的常见痛点,提供可运行的参考实现。包内共145个文件,以65个meta、25个asset、13个shader及若干cginc、mat等渲染与工程配置资源为主,另含2个cs脚本、2个jslib桥接文件、html与json配置,压缩包约1.35MB,结构完整便于直接导入研究。Demo围绕Screen.fullScreen与Screen.orientation控制、Application.platform平台检测、UnityLoader与浏览器JavaScript交互、移动端旋转生命周期处理等关键环节展开,展示了WebGL与原生平台差异化的适配思路。目前已有1745人学习下载,适合需要快速落地全屏横屏方案、减少重复踩坑的Unity开发者参考借鉴。
1. 从一次翻车说起:为什么 WebGL 打包后全屏横屏这么难搞
去年帮一个做互动展示的团队调项目,Unity WebGL 构建出来的东西在 Windows 浏览器里跑得好好的,一放到安卓平板和 iPad 上就出问题——要么画面只占屏幕左上角一小块,要么横屏游戏被强制竖屏显示,两边留出巨大的黑边。更离谱的是 iOS Safari 上点击全屏按钮后地址栏还在,整个 Canvas 被压扁。这不是个例,是 Unity WebGL 在移动端浏览器上的经典玄学问题。
这个 Demo 要解决的就是这件事:让 Unity WebGL 构建产物在 Windows 桌面浏览器、安卓移动设备、苹果移动设备上都能自动全屏并锁定横屏。它适合正在做 H5 小游戏、互动课件、展厅大屏项目的 Unity 开发者,尤其是那些不想依赖第三方商业插件、希望用原生 JavaScript 桥接方案搞定全屏横屏的人。核心思路不复杂,但浏览器兼容性差异会让第一次做的人踩不少坑。
2. 技术选型与整体架构:为什么不用插件自己写桥接
2.1 三层架构设计
这个方案的整体结构分三层。最底层是 Unity 的 WebGL 模板(Template),负责生成最终的 HTML 壳子和 Canvas 容器;中间层是一段注入到页面里的 JavaScript 桥接脚本,处理全屏 API 调用、屏幕方向锁定和尺寸自适应;最上层是 Unity C# 侧的调用入口,通过Application.ExternalCall或jslib插件机制触发浏览器行为。
选这个架构而不是直接买个 Asset Store 插件,原因有三个。第一,全屏和横屏本质上是浏览器 API 的事,Unity 管不着,插件也只是帮你封装了 JS 调用,多一层黑盒不如自己写清楚。第二,移动端浏览器对 Fullscreen API 和 Screen Orientation API 的支持差异很大,自己写桥接可以针对不同平台做降级处理。第三,WebGL 模板本身就可以定制,把 JS 逻辑直接写进模板里,构建出来就能用,不需要额外配置。
常见做法是在Assets/Plugins/WebGL/目录下放一个.jslib文件,Unity 构建时会把它合并进生成的 JS 里。但更直接的方式是修改 WebGL 模板中的index.html,把全屏逻辑写死在模板里,这样连 C# 调用都省了,页面加载完自动执行。
2.2 浏览器 API 能力对比
不同平台对关键 API 的支持情况直接决定了代码怎么写:
| API | Windows Chrome/Edge | 安卓 Chrome | iOS Safari |
|---|---|---|---|
| requestFullscreen | 支持 | 支持 | 不支持,需 webkit 前缀 |
| screen.orientation.lock | 支持 | 支持 | 不支持 |
| webkitRequestFullscreen | 支持 | 支持 | 支持 |
| 自动全屏(无用户手势) | 不允许 | 不允许 | 不允许 |
这张表说明两个关键问题:iOS 上必须用webkit前缀的 API,而且所有浏览器都要求全屏操作必须由用户手势触发,不能在页面加载时自动调用。所以“自动全屏”的真实含义是:在用户第一次点击/触摸页面时立即触发全屏,而不是页面一打开就全屏。
2.3 目录结构
Demo 的目录结构大致如下:
WebGLTemplates/ FullscreenTemplate/ index.html # 定制模板,包含全屏桥接逻辑 TemplateData/ style.css # Canvas 样式,处理居中与尺寸 fullscreen.js # 核心全屏横屏脚本 Assets/ Plugins/ WebGL/ FullscreenBridge.jslib # C# 调用 JS 的桥接 Scripts/ FullscreenTrigger.cs # 游戏内触发全屏的入口 ProjectSettings/ ProjectSettings.asset # 默认模板设置为 FullscreenTemplate把模板放在Assets/WebGLTemplates/下,然后在 Player Settings 里选择对应模板,构建时 Unity 会自动用它替换默认的index.html。这样整个方案就是自包含的,换项目也能直接复制过去。
3. 核心模块拆解:全屏、横屏、自适应三段代码
3.1 全屏请求与兼容性封装
全屏逻辑的核心是处理不同浏览器的前缀差异,同时要保证在用户手势回调里执行。下面这段代码放在fullscreen.js里:
// 封装跨浏览器的全屏请求 function requestFullscreen(element) { // 标准 API if (element.requestFullscreen) { return element.requestFullscreen(); } // WebKit 内核(iOS Safari、旧版 Chrome) if (element.webkitRequestFullscreen) { return element.webkitRequestFullscreen(); } // 旧版 Firefox if (element.mozRequestFullScreen) { return element.mozRequestFullScreen(); } // IE11 if (element.msRequestFullscreen) { return element.msRequestFullscreen(); } console.warn('当前浏览器不支持全屏 API'); return Promise.reject('Fullscreen not supported'); } // 退出全屏的兼容封装 function exitFullscreen() { if (document.exitFullscreen) { document.exitFullscreen(); } else if (document.webkitExitFullscreen) { document.webkitExitFullscreen(); } else if (document.mozCancelFullScreen) { document.mozCancelFullScreen(); } else if (document.msExitFullscreen) { document.msExitFullscreen(); } }这段代码的关键点在于:requestFullscreen返回的是 Promise,在 iOS 上webkitRequestFullscreen不返回 Promise,所以调用方不能依赖.then()做后续操作。参数element通常传document.documentElement或 Canvas 的父容器,传 Canvas 本身在某些浏览器上会导致全屏后画面比例异常。我一般传document.documentElement,让整个页面全屏,Canvas 再通过 CSS 撑满。
3.2 横屏锁定与降级方案
横屏锁定依赖 Screen Orientation API,但 iOS 完全不支持,所以必须准备降级方案:
// 尝试锁定横屏 function lockLandscape() { // 标准 API,安卓 Chrome 支持 if (screen.orientation && screen.orientation.lock) { screen.orientation.lock('landscape').catch(function(err) { console.warn('横屏锁定失败,走降级方案:', err); fallbackRotate(); }); } else { // iOS 等不支持 lock 的平台 fallbackRotate(); } } // 降级方案:通过 CSS 旋转 Canvas 模拟横屏 function fallbackRotate() { var canvas = document.querySelector('#unity-canvas'); if (!canvas) return; var w = window.innerWidth; var h = window.innerHeight; // 竖屏状态下旋转 90 度 if (h > w) { canvas.style.transform = 'rotate(90deg)'; canvas.style.transformOrigin = 'left top'; canvas.style.width = h + 'px'; canvas.style.height = w + 'px'; canvas.style.position = 'absolute'; canvas.style.left = w + 'px'; canvas.style.top = '0'; } else { // 已经是横屏,清除旋转 canvas.style.transform = ''; canvas.style.width = '100%'; canvas.style.height = '100%'; canvas.style.left = '0'; } }screen.orientation.lock('landscape')在安卓 Chrome 上需要页面已经处于全屏状态才能生效,所以调用顺序必须是先全屏再锁定。iOS 上的降级方案是通过 CSStransform把 Canvas 旋转 90 度,同时交换宽高,再用left和top把旋转后的画面推回可视区域。这个方案在 iPad 上实测可用,但要注意触摸事件的坐标需要同步旋转,否则点击位置会偏移。Demo 里通过 Unity 的Input系统自动处理了坐标映射,不需要额外改 C# 代码。
3.3 尺寸自适应与 resize 监听
移动端浏览器旋转屏幕或地址栏收起时会触发 resize,Canvas 尺寸必须跟着变:
// 自适应 Canvas 尺寸 function resizeCanvas() { var canvas = document.querySelector('#unity-canvas'); if (!canvas) return; var w = window.innerWidth; var h = window.innerHeight; // 保持宽高比,避免拉伸变形 var targetRatio = 16 / 9; var currentRatio = w / h; if (currentRatio > targetRatio) { // 屏幕太宽,以高度为准 canvas.style.height = h + 'px'; canvas.style.width = (h * targetRatio) + 'px'; } else { // 屏幕太高,以宽度为准 canvas.style.width = w + 'px'; canvas.style.height = (w / targetRatio) + 'px'; } canvas.style.position = 'absolute'; canvas.style.left = '50%'; canvas.style.top = '50%'; canvas.style.transform = 'translate(-50%, -50%)'; } // 监听尺寸变化 window.addEventListener('resize', resizeCanvas); window.addEventListener('orientationchange', function() { // 旋转后延迟执行,等浏览器更新尺寸 setTimeout(resizeCanvas, 300); });orientationchange事件触发时浏览器还没更新innerWidth和innerHeight,所以必须加setTimeout延迟。300ms 是个经验值,iOS 上有时需要 500ms。宽高比targetRatio根据项目实际设计分辨率调整,Demo 里用的是 16:9,如果是 4:3 的课件项目就改成4/3。
3.4 用户手势触发全屏的绑定
所有浏览器都要求全屏必须由用户手势触发,所以需要在页面加载后绑定一次性的点击事件:
// 首次用户交互时触发全屏 function bindFirstInteraction() { var triggered = false; function onFirstTouch() { if (triggered) return; triggered = true; requestFullscreen(document.documentElement).then(function() { lockLandscape(); resizeCanvas(); }).catch(function(err) { console.warn('全屏请求被拒绝:', err); }); // 移除监听,只触发一次 document.removeEventListener('click', onFirstTouch); document.removeEventListener('touchstart', onFirstTouch); } document.addEventListener('click', onFirstTouch); document.addEventListener('touchstart', onFirstTouch); } // 页面加载完成后绑定 window.addEventListener('load', function() { bindFirstInteraction(); resizeCanvas(); });用triggered标志位防止重复触发,因为click和touchstart在移动端可能同时触发。移除监听用removeEventListener,不要用once: true,因为部分旧版浏览器不支持这个选项。全屏成功后立即调用lockLandscape和resizeCanvas,保证画面状态正确。
4. 避坑与常见问题排查
4.1 全屏后画面模糊或拉伸
现象:全屏后 Canvas 分辨率没变,画面被拉伸模糊。原因是 Unity WebGL 的 Canvas 尺寸在构建时就固定了,全屏只是 CSS 放大。解决办法是在 Unity 的 Player Settings 里把 Resolution and Presentation 的默认 Canvas 宽度高度设大一些,比如 1920x1080,然后在 JS 里通过unityInstance.Module.setCanvasSize动态调整。Demo 里在resizeCanvas之后调用了这个接口。
4.2 iOS 上全屏按钮点击无反应
现象:iPhone 上点击页面没有任何全屏效果。原因是 iOS Safari 不支持requestFullscreen,只支持webkitRequestFullscreen,而且必须绑定在touchend事件上,touchstart有时会被忽略。解决办法是把监听事件改成touchend,并且确保webkitRequestFullscreen是在事件回调的同步代码里调用,不能放在setTimeout或 Promise 里。
4.3 安卓上横屏锁定报错
现象:控制台报NotSupportedError: screen.orientation.lock() is not available。原因是页面还没进入全屏状态就调用了 lock。解决办法是确保requestFullscreen的 Promise resolve 之后再调用lockLandscape,Demo 里已经用.then()保证了顺序。如果仍然报错,检查是否在 iframe 里运行,iframe 需要加allowfullscreen属性。
4.4 旋转屏幕后触摸坐标偏移
现象:降级旋转方案下,点击位置和实际按钮位置对不上。原因是 CSStransform旋转后,触摸事件的坐标还是原始坐标系。解决办法是在 Unity 的 WebGL 输入模块里做坐标变换,或者更简单的方式:不用 CSS 旋转,而是让 Unity 自己处理横屏,通过Screen.orientation在 C# 侧判断。Demo 里两种方案都提供了,CSS 旋转方案适合快速验证,C# 方案适合正式项目。
4.5 构建后模板没生效
现象:改了index.html但构建出来的页面还是默认样式。原因是 WebGL 模板必须放在Assets/WebGLTemplates/下,并且在 Player Settings 的 Resolution and Presentation 里选中对应模板名称。另外模板文件夹名不能有空格,index.html必须直接放在模板文件夹根目录,不能嵌套子文件夹。
5. 二次开发与改造思路:从 Demo 到生产项目
拿到这个 Demo 后,最直接的改造是把全屏逻辑从模板里抽出来做成可配置的。我一般会在fullscreen.js顶部加一个配置对象:
var FullscreenConfig = { autoFullscreen: true, // 是否首次交互自动全屏 lockOrientation: 'landscape', // 锁定方向,可选 landscape/portrait targetRatio: 16 / 9, // 设计分辨率宽高比 resizeDelay: 300 // 旋转后延迟毫秒 };这样不同项目只需要改配置,不用动核心逻辑。如果项目需要竖屏,把lockOrientation改成portrait,targetRatio改成9/16,降级旋转逻辑也要相应调整。
第二个改造点是和 Unity C# 侧的通信。Demo 里用的是页面加载自动触发,但有些项目需要在游戏内特定场景才全屏,比如点击“开始游戏”按钮后。这时候需要在.jslib里暴露一个函数:
// FullscreenBridge.jslib mergeInto(LibraryManager.library, { RequestFullscreen: function() { requestFullscreen(document.documentElement).then(function() { lockLandscape(); }); }, ExitFullscreen: function() { exitFullscreen(); } });然后在 C# 里调用:
using System.Runtime.InteropServices; using UnityEngine; public class FullscreenTrigger : MonoBehaviour { [DllImport("__Internal")] private static extern void RequestFullscreen(); [DllImport("__Internal")] private static extern void ExitFullscreen(); public void EnterFullscreen() { #if UNITY_WEBGL && !UNITY_EDITOR RequestFullscreen(); #endif } public void QuitFullscreen() { #if UNITY_WEBGL && !UNITY_EDITOR ExitFullscreen(); #endif } }注意#if UNITY_WEBGL && !UNITY_EDITOR这个条件编译,因为DllImport("__Internal")在编辑器里会报错,必须排除编辑器环境。
第三个改造方向是处理多平台差异。Windows 桌面浏览器上全屏后按 ESC 会退出,需要监听fullscreenchange事件同步 Unity 内的状态:
document.addEventListener('fullscreenchange', function() { var isFullscreen = !!document.fullscreenElement; // 通知 Unity 当前全屏状态 if (unityInstance) { unityInstance.SendMessage('FullscreenManager', 'OnFullscreenChanged', isFullscreen ? '1' : '0'); } });unityInstance是 Unity 2020 之后构建产物暴露的全局对象,旧版本用gameInstance。SendMessage的第一个参数是场景中 GameObject 的名字,第二个是方法名,第三个是字符串参数。C# 侧对应的方法签名是public void OnFullscreenChanged(string state)。
验证改造是否成功,我习惯用三个检查点:Windows Chrome 上按 F11 和 ESC 切换全屏,画面不闪烁;安卓 Chrome 上旋转屏幕,Canvas 自动适配且触摸准确;iOS Safari 上点击页面,全屏后横屏显示且地址栏隐藏。三个都过了才算真正可用。
从那以后我每次做 WebGL 项目,都会先把全屏横屏这段逻辑单独抽成一个模板,构建一次就在三个平台上各跑一遍,确认没问题再往下写业务代码。这个习惯帮我省掉了至少三次返工。希望帮到你。
本文还有配套的精品资源,点击获取