ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

Unity WebGL移动端屏幕方向适配:从原理到实战解决方案

Unity WebGL移动端屏幕方向适配:从原理到实战解决方案 1. 项目概述当Unity WebGL遇上移动端屏幕如果你做过Unity WebGL项目并且尝试过在手机浏览器里打开大概率会遇到一个让人头疼的问题屏幕方向不对。你精心设计的横屏游戏在用户手机上可能被强制拉伸成竖屏UI错位、视野变形体验极差或者你的竖屏应用在某些安卓机型上死活不肯“躺下”用户必须手动旋转手机并开启屏幕旋转锁定才能正常游玩这无疑是在劝退用户。这个问题的根源在于WebGL构建运行在浏览器环境中而浏览器对屏幕方向的控制权与原生移动应用通过Unity打包的APK或IPA有着本质的不同。原生应用可以通过清单文件AndroidManifest.xml或Info.plist直接声明支持的屏幕方向系统会强制执行。但WebGL内容作为网页的一部分其显示方式受到宿主网页、浏览器策略以及设备系统多方面的制约。简单来说浏览器说“这个页面可以旋转”设备才会旋转Unity WebGL Player本身并没有直接命令设备旋转的能力。因此“Unity WebGL适配移动端的横屏竖屏显示解决方案”这个标题指向的是一个非常具体且高频的开发痛点。它不是一个简单的API调用而是一套需要从前端HTML/JS、Unity项目设置到CSS样式进行协同处理的系统工程。目标很明确让我们的Unity WebGL内容在移动端浏览器上能够按照我们预期的方向横屏或竖屏正确显示并提供良好的旋转响应或锁定体验。2. 核心挑战与解决思路拆解要解决这个问题我们首先得理解横在面前的几座大山。2.1 挑战一浏览器与系统的权限隔阂最大的障碍是纯粹的WebGL上下文即Unity导出的game.js和game.wasm运行的Canvas无法直接访问或控制设备的屏幕方向传感器也无法强制系统旋转屏幕。这个权限被浏览器牢牢把控。浏览器通过Screen Orientation API提供了一个接口允许网页查询和锁定屏幕方向但这个“锁定”是在浏览器层面生效的并且需要用户手势触发如点击事件后才能调用这是为了防止恶意网站锁定用户屏幕。2.2 挑战二Canvas渲染与CSS显示的分离Unity WebGL的渲染输出到一个canvas元素上。这个Canvas的尺寸width和height属性在Unity的Player Settings中设定决定了渲染缓冲区的分辨率。然而这个Canvas在网页中如何被显示则由外层的HTML容器通常是div的CSS样式如width: 100%; height: 100%;控制。当设备旋转时网页视口viewport的宽高会互换如果我们只简单地将Canvas的CSS设为全屏就会出现渲染内容被拉伸或压缩的情况。2.3 挑战三全屏模式与内联模式的行为差异你可能会在网上搜索到一些方案提到“全屏模式下才能旋转”。这有一定道理。因为许多浏览器对Screen Orientation API的锁定功能在全屏模式下有更宽松的策略或不同的行为。但我们的应用不一定需要也不应该强制用户全屏。我们需要一个在普通网页内嵌模式下也能稳定工作的方案。2.4 解决思路总览基于以上挑战一个完整的解决方案必须多管齐下前端驱动方向检测与锁定使用JavaScript检测设备方向变化并尝试通过Screen Orientation API锁定网页方向。这是控制显示的“总开关”。Unity内容自适应在Unity内部我们需要编写C#脚本监听来自前端的屏幕方向变化事件并动态调整游戏视图的Camera视口、Canvas Scaler或UI布局确保内容始终正确填充不发生形变。HTML/CSS容器响应式适配设计外层HTML容器的CSS使其能根据当前方向横屏或竖屏智能地调整Canvas的显示尺寸和位置处理可能的黑边Letterboxing问题。优雅降级与提示对于不支持方向锁定的浏览器或用户拒绝授权的情况需要准备降级方案比如显示一个友好的提示界面引导用户手动旋转设备。接下来我们将深入每个环节给出具体的实操代码和配置。3. 前端JavaScript层检测、锁定与通信这是整个解决方案的“大脑”负责与浏览器和设备打交道。3.1 修改发布模板index.htmlUnity WebGL发布时会生成一个index.html。我们通常需要修改这个模板文件或者创建一个自定义模板。更推荐后者在Assets目录下创建WebGLTemplates文件夹再在里面创建一个新的模板文件夹如CustomTemplate复制默认模板文件进去进行修改。这样每次构建都会使用你的定制模板。3.2 核心JavaScript代码实现我们需要在index.html的script标签内或引入的外部JS文件中编写以下核心功能。1. 检测屏幕方向变化// 监听设备方向变化注意这个是设备物理方向不一定等于屏幕方向 window.addEventListener(orientationchange, handleOrientationChange); // 更推荐使用 Screen Orientation API 来监听 if (screen.orientation screen.orientation.addEventListener) { screen.orientation.addEventListener(change, handleOrientationChange); } // 初始检测一次 handleOrientationChange(); function handleOrientationChange() { // 获取当前屏幕方向类型如 landscape-primary, portrait-primary 等 const type screen.orientation ? screen.orientation.type : (window.orientation 0 || window.orientation 180 ? portrait : landscape); // 获取当前视口网页可视区域的宽高 const viewportWidth window.innerWidth; const viewportHeight window.innerHeight; // 判断当前是横屏还是竖屏状态 const isLandscape viewportWidth viewportHeight; // 将方向信息和视口尺寸发送给Unity sendOrientationToUnity(type, isLandscape, viewportWidth, viewportHeight); }2. 尝试锁定屏幕方向锁定操作通常需要在用户交互如点击按钮后触发这是浏览器的安全策略。function lockScreenOrientation(targetOrientation) { // targetOrientation: landscape, portrait, landscape-primary 等 if (screen.orientation screen.orientation.lock) { // 注意lock() 返回一个Promise screen.orientation.lock(targetOrientation) .then(() { console.log(屏幕方向已锁定为: ${targetOrientation}); }) .catch((error) { console.error(锁定屏幕方向失败:, error); // 锁定失败后的降级处理例如显示提示 showOrientationLockFailedMessage(); }); } else { // 浏览器不支持 Screen Orientation API console.warn(当前浏览器不支持锁定屏幕方向API); showOrientationNotSupportedMessage(); } } // 示例在用户点击“开始游戏”按钮时锁定为横屏 document.getElementById(startButton).addEventListener(click, function() { lockScreenOrientation(landscape); });重要提示screen.orientation.lock()并非在所有浏览器和所有模式下都可用。特别是在iOS的Safari中限制非常严格。通常只有在全屏模式下或者某些特定的元标签配置下锁定请求才更可能成功。这就是为什么很多方案会建议先进入全屏再锁定方向。3. 与Unity实例通信我们需要将方向信息从JavaScript传递到Unity的C#脚本中。这通过SendMessage或直接调用Unity实例上的方法实现。function sendOrientationToUnity(type, isLandscape, width, height) { // 假设你的Unity实例全局变量是 unityInstance (Unity 2020 默认) // 或者 gameInstance (旧版本) if (typeof unityInstance ! undefined) { // 调用Unity中GameObject上的方法 unityInstance.SendMessage(ScreenOrientationManager, OnScreenOrientationChanged, ${type}|${isLandscape}|${width}|${height}); } // 同时我们也可以根据方向调整HTML容器的CSS类用于样式控制 const container document.querySelector(#unity-container); if (isLandscape) { container.classList.remove(portrait); container.classList.add(landscape); } else { container.classList.remove(landscape); container.classList.add(portrait); } }3.3 HTML/CSS 响应式样式对应的CSS需要处理两种状态#unity-container { position: absolute; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; background: #000; /* 黑边背景色 */ } /* 横屏样式容器横向填充 */ #unity-container.landscape { flex-direction: row; } /* 竖屏样式容器纵向填充 */ #unity-container.portrait { flex-direction: column; } #unity-canvas { /* 初始尺寸由Unity设定这里设为自动 */ width: auto; height: auto; /* 关键保持Canvas原始宽高比避免拉伸 */ object-fit: contain; } /* 针对横屏游戏的设计当处于竖屏状态时显示提示层 */ .orientation-prompt { display: none; position: absolute; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.9); color: white; justify-content: center; align-items: center; flex-direction: column; z-index: 1000; } #unity-container.portrait .orientation-prompt { display: flex; /* 竖屏时显示旋转提示 */ }这段CSS确保了无论方向如何Canvas都会居中显示并保持其原始比例周围用黑边填充Letterboxing这是最通用且视觉上可接受的方式。4. Unity C#层动态适配与内容布局前端告诉了Unity当前的状态Unity内部需要做出响应。我们需要在Unity中创建一个管理器例如ScreenOrientationManager.cs来接收消息并调整内容。4.1 接收JavaScript消息using UnityEngine; using System; // 用于String.Split public class ScreenOrientationManager : MonoBehaviour { // 用于接收来自JS的调用 public void OnScreenOrientationChanged(string data) { // data 格式: landscape-primary|true|1920|1080 string[] parts data.Split(|); if (parts.Length 4) { string orientationType parts[0]; bool isLandscape bool.Parse(parts[1]); int viewportWidth int.Parse(parts[2]); int viewportHeight int.Parse(parts[3]); Debug.Log($Orientation: {orientationType}, Landscape: {isLandscape}, Viewport: {viewportWidth}x{viewportHeight}); // 处理方向变化 HandleOrientationChange(isLandscape, viewportWidth, viewportHeight); } } private void HandleOrientationChange(bool isLandscape, int screenWidth, int screenHeight) { // 核心逻辑根据横竖屏调整你的游戏内容 AdjustCameraViewport(isLandscape, screenWidth, screenHeight); AdjustUIForOrientation(isLandscape); // ... 其他需要调整的系统 } }将这个脚本挂载到一个场景中永不销毁的GameObject上比如叫“ScreenOrientationManager”。4.2 调整摄像机视口这是防止画面拉伸的关键。我们需要根据当前屏幕的宽高比动态计算并设置摄像机的rect。public Camera mainCamera; // 在Inspector中赋值 private void AdjustCameraViewport(bool isLandscape, int screenWidth, int screenHeight) { if (mainCamera null) return; // 计算屏幕宽高比 float screenAspect (float)screenWidth / screenHeight; // 你的游戏设计分辨率宽高比 (例如 16:9 1.777...) float designAspect 16f / 9f; // 根据你的Game视图设置修改 if (screenAspect designAspect) { // 屏幕比设计更“宽”例如在超宽屏或某些横屏状态下 // 左右会出现黑边 float widthRatio designAspect / screenAspect; mainCamera.rect new Rect((1f - widthRatio) / 2f, 0f, widthRatio, 1f); } else if (screenAspect designAspect) { // 屏幕比设计更“窄”例如在竖屏状态下 // 上下会出现黑边 float heightRatio screenAspect / designAspect; mainCamera.rect new Rect(0f, (1f - heightRatio) / 2f, 1f, heightRatio); } else { // 比例完美匹配全屏显示 mainCamera.rect new Rect(0f, 0f, 1f, 1f); } }这种方法保证了游戏核心内容始终按设计比例渲染多余的空间显示为黑边是最保真、最不容易出错的方案。4.3 调整UI布局使用Unity UI如果你的游戏有大量UI可能需要为横屏和竖屏设计两套不同的布局或者使用一个自适应的布局系统。方案A动态切换Canvas Scaler如果你的UI Canvas使用Canvas Scaler并且设置为Scale With Screen Size你可以根据方向动态调整Reference Resolution。public CanvasScaler canvasScaler; private void AdjustUIForOrientation(bool isLandscape) { if (canvasScaler null) return; // 假设横屏参考分辨率是 1920x1080竖屏是 1080x1920 if (isLandscape) { canvasScaler.referenceResolution new Vector2(1920, 1080); // 可能还需要调整UI元素的锚点或位置 } else { canvasScaler.referenceResolution new Vector2(1080, 1920); } }方案B启用/禁用不同的UI根对象更直接的方法是为横屏和竖屏准备两套完整的UI层级然后根据方向切换它们的激活状态。public GameObject landscapeUI; public GameObject portraitUI; private void AdjustUIForOrientation(bool isLandscape) { if (landscapeUI ! null) landscapeUI.SetActive(isLandscape); if (portraitUI ! null) portraitUI.SetActive(!isLandscape); }这种方法逻辑清晰美术和策划可以完全独立地设计两套界面但资源量会翻倍。5. 构建与发布配置要点Unity编辑器中的设置同样重要它们决定了WebGL构建的初始状态和基础能力。5.1 Player Settings 关键配置Resolution and Presentation (分辨率与呈现):Default Screen Width/Height: 这里设置的是Canvas的渲染分辨率。建议设置为你的设计分辨率。例如对于横屏16:9游戏设为1920x1080。这个值影响渲染缓冲区的尺寸与最终显示尺寸无关。WebGL Template: 选择你自定义的模板例如CustomTemplate确保你的HTML/JS/CSS修改能被使用。Publishing Settings (发布设置):Compression Format (压缩格式): 推荐使用Brotli它比Gzip有更好的压缩比能减少加载时间。但需要确保你的服务器支持Brotli解码。Data Caching (数据缓存): 勾选上允许浏览器缓存资源文件提升重复访问的加载速度。5.2 项目设置中的注意事项Quality Settings (质量设置): 针对移动端WebGL务必调低图形质量。关闭或降低抗锯齿MSAA、阴影分辨率、纹理过滤等。WebGL在移动端的性能开销很大优化是必须的。Physics Settings (物理设置): 如果项目用到物理检查固定时间步长Fixed Timestep。在性能较弱的设备上可以考虑适当调大这个值例如从0.02调到0.04以减少每帧的物理计算负担。6. 平台特定问题与进阶策略不同浏览器和设备尤其是iOS和Android之间行为差异巨大。6.1 iOS Safari 的特殊性iOS上的Safari对Screen Orientation API的支持最为保守。screen.orientation.lock()在非全屏模式下基本无效。因此对于iOS我们的策略需要调整依赖orientationchange事件和CSS适配放弃在iOS上强制锁定方向转而专注于通过orientationchange事件检测变化并利用前面提到的CSS和Camera视口适配方案让游戏内容自动适应任何方向。虽然屏幕UI会旋转但游戏画面能保持正确比例。全屏模式作为备选提供一个“进入全屏”按钮。在iOS全屏模式下方向控制的可能性会稍大一些但依然不保证。可以使用requestFullscreen()API。元标签Meta Tags: 在index.html的head里添加以下标签能提供一些基础提示但无法强制锁定。meta nameviewport contentwidthdevice-width, initial-scale1.0, maximum-scale1.0, user-scalableno !-- 下面这行提示浏览器该页面偏好横屏但Safari可能忽略 -- meta nameapple-mobile-web-app-orientations contentlandscape6.2 Android Chrome 的兼容性Android Chrome对Screen Orientation API的支持相对较好尤其是在较新版本中。lock()方法在用户交互后成功概率较高。但仍然要做好降级处理。6.3 处理不支持API的降级方案始终要检测API的可用性。function checkOrientationSupport() { const supportsOrientation !!(screen.orientation screen.orientation.lock); const supportsFullscreen !!(document.documentElement.requestFullscreen); if (!supportsOrientation) { // 显示一个常驻的提示UI告诉用户“建议开启屏幕旋转并横屏体验游戏” createFallbackOrientationUI(); } return supportsOrientation; }降级UI可以包含一个模拟的“旋转图标”动画和文字说明引导用户进行物理旋转。6.4 性能优化考量方向改变时浏览器会触发resize和orientationchange事件Unity端会随之调整视口和UI。这个过程可能引起一帧的卡顿。防抖处理在JavaScript的handleOrientationChange函数中可以添加一个简单的防抖逻辑避免在设备旋转过程中过于频繁地向Unity发送消息。let orientationChangeTimeout; function handleOrientationChange() { clearTimeout(orientationChangeTimeout); orientationChangeTimeout setTimeout(() { // 真正的处理逻辑... }, 100); // 延迟100毫秒执行确保旋转稳定 }Unity端合并操作在AdjustCameraViewport和AdjustUIForOrientation中避免在单帧内进行大量GameObject的激活/禁用或布局重建。可以考虑使用Canvas.ForceUpdateCanvases()来一次性完成UI更新。7. 完整工作流与测试清单将以上所有步骤串联起来一个稳健的适配工作流如下规划阶段确定你的WebGL项目是强制横屏、强制竖屏还是支持旋转。大多数游戏是强制横屏。Unity设置在Player Settings中设置好设计分辨率。在场景中创建ScreenOrientationManager并配置好Camera和UI引用。创建WebGL模板在Assets/WebGLTemplates/下创建自定义模板编写集成方向检测、锁定、通信和响应式样式的index.html。编写C#脚本实现接收消息、调整摄像机视口、切换UI的逻辑。构建与本地测试使用开发服务器如Python的http.server在本地启动作用手机浏览器访问测试。务必使用真机测试模拟器无法完全模拟浏览器行为。迭代与调试使用console.log和Unity的Debug.Log输出方向信息。在Chrome DevTools的移动设备模拟器中可以模拟旋转但同样需要真机验证。重点测试iOS Safari, Android Chrome 以及微信内置浏览器X5内核行为可能更特殊。部署将构建好的文件上传到服务器。确保服务器正确配置了.wasm和.br文件的MIME类型。上线前测试清单[ ] 横屏游戏在竖屏手机打开时是否显示“请旋转设备”的提示[ ] 用户旋转设备到横屏后游戏画面是否能正确填充比例是否正确无拉伸[ ] 点击“锁定横屏/开始游戏”按钮后屏幕方向是否被锁定在支持的浏览器上[ ] 在iOS Safari上不锁定方向仅靠CSS和视口适配旋转设备时游戏内容表现是否正常[ ] UI布局按钮、文字在方向变化后位置和大小是否正确[ ] 进入/退出全屏模式方向逻辑是否依然正确[ ] 在低端安卓机上方向切换过程是否流畅有无明显卡顿或闪烁8. 我踩过的坑与心得最后分享几个在实际项目中积累的经验这些可能不会在官方文档里明确写出。坑1window.orientation的废弃与兼容性早期我们常用window.orientation返回0, 90, -90等角度值但这个API已被废弃。虽然很多浏览器仍支持但未来有风险。务必优先使用screen.orientation.type进行判断并做好回退兼容。坑2Unity WebGL的输入坐标问题当摄像机视口camera.rect不是全屏时即出现了黑边鼠标或触摸点击的坐标需要转换。Unity的Input.mousePosition仍然是基于整个屏幕的。如果你需要点击到游戏区域需要自己写一个坐标转换函数将输入坐标从“屏幕空间”转换到“视口空间”。否则点击黑边区域可能会被误判为点击了游戏内的某个位置。坑3移动端浏览器工具栏的影响移动端浏览器的地址栏、工具栏在滚动时会显示/隐藏这会动态改变window.innerHeight从而触发resize事件。这可能会干扰我们的方向判断逻辑。一个技巧是在方向判断时可以结合screen.orientation和window.innerWidth/innerHeight进行综合判断并设置一个阈值避免因工具栏微小的尺寸变化而误触发。坑4微信内置浏览器的“顽固”微信浏览器特别是安卓版有时会覆盖系统的方向锁定。即使你成功锁定了横屏当用户点击输入框弹出软键盘时屏幕可能又会强制跳回竖屏。对于这种情况没有完美的解决方案。通常的应对策略是避免在横屏游戏中使用原生输入框或者设计一个自定义的、不会触发软键盘的输入组件。心得渐进增强与优雅降级不要追求在所有设备、所有浏览器上实现完美的程序化方向锁定。将“前端JS锁定”视为一种增强体验而把“CSS/Unity视口适配”作为基础能力。只要能做到无论屏幕物理方向如何游戏内容都能正确、无拉伸地显示核心体验就得到了保障。在此基础上再为支持高级功能的浏览器提供更佳的锁定体验这才是最务实的开发思路。
RELATED READING

延伸阅读

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