1. 项目概述:为什么全屏API值得你花时间研究?
如果你正在用Cocos Creator开发游戏或者互动应用,那么“全屏”这个功能大概率是你绕不开的一个坎。它看起来简单——不就是让画面铺满整个屏幕吗?但实际做起来,你会发现这里面的坑一个接一个:为什么在手机上点了按钮没反应?为什么在PC浏览器里全屏后UI错位了?为什么退出全屏后游戏状态乱了?这些“为什么”背后,是不同浏览器厂商五花八门的实现标准、移动端和桌面端迥异的交互逻辑,以及引擎本身对原生API的封装策略。
我接手过不少从其他引擎迁移过来或者从零开始的项目,几乎每一个涉及到全屏需求时,都会经历一番折腾。网上能找到的代码片段往往只解决了“能不能用”的问题,但距离“好用、稳定、不出错”还差得远。这份指南,就是把我这些年踩过的坑、总结的最佳实践,以及那些官方文档里没明说但至关重要的细节,系统地梳理给你。无论你是想实现一个沉浸式的关卡体验、一个全屏播放的视频广告,还是一个简单的“点击全屏”按钮,这里的内容都能让你少走弯路。
2. 全屏API的核心原理与浏览器差异
全屏功能本质上并不是Cocos引擎“创造”的,而是对浏览器原生Fullscreen API(或称Fullscreen API)的一层封装。理解这层封装,是解决一切奇怪问题的起点。
2.1 原生Fullscreen API的“分裂”现状
你可能听说过requestFullscreen这个方法,但实际写代码时,会发现它前面常常需要加上webkit、moz或ms这样的前缀。这是因为全屏API在成为正式标准前,各大浏览器都有自己的实现。虽然现在主流浏览器都支持标准写法,但为了极致的兼容性(尤其是考虑一些老旧的内嵌浏览器环境),我们通常还是需要做能力检测。
// 一个经典的全屏请求函数,考虑了兼容性 function requestFullscreen(element) { if (element.requestFullscreen) { element.requestFullscreen(); } else if (element.webkitRequestFullscreen) { // Chrome, Safari, Edge element.webkitRequestFullscreen(); } else if (element.mozRequestFullScreen) { // Firefox element.mozRequestFullScreen(); } else if (element.msRequestFullscreen) { // IE/Edge Legacy element.msRequestFullscreen(); } }Cocos Creator引擎内部已经帮你处理了这部分兼容性代码,它通过screen模块提供了一个统一的接口。但知道底层原理非常重要,因为当你需要调试一些深层次问题(比如全屏事件没触发)时,你可能会需要直接和原生API打交道。
2.2 Cocos Creator的封装逻辑
在Cocos Creator 3.x版本中,全屏API主要通过screen模块来提供。这个模块的设计目标是让开发者无需关心底层差异,但同时也带来了一些限制和特定的行为模式。
引擎通常会将全屏请求绑定到canvas元素上,也就是我们游戏渲染的主画布。当你调用screen.requestFullScreen()时,引擎内部会做以下几件事:
- 找到当前的
canvasDOM元素。 - 用上述兼容性方法调用该元素的
requestFullscreen。 - 监听全屏状态变化事件,并同步到引擎的内部状态。
这里有一个至关重要的限制,也是新手最容易栽跟头的地方:绝大多数现代浏览器(包括Chrome、Safari、Firefox)出于安全和用户体验的考虑,都要求全屏请求必须由一个真实的、明确的用户手势(User Gesture)来触发。这意味着你不能在游戏的start()函数里、在一个定时器回调里、或者在一个网络请求成功的回调里直接调用全屏API。它必须发生在例如button节点的click事件、触摸屏的touchstart事件等直接由用户操作引发的事件处理函数中。
注意:这就是为什么你写了一个“游戏加载完成后自动全屏”的代码却无效的原因。解决方案通常是将全屏调用包装在一个按钮点击事件里,或者先引导用户进行一个任意交互(如点击“开始游戏”按钮),再在回调中触发全屏。
2.3 移动端与PC端的核心行为差异
这个差异直接决定了你的全屏策略。
- PC浏览器:通常实现的是“全屏模式”,即网页内容占据整个屏幕,隐藏浏览器地址栏、工具栏等。退出方式一般是按
ESC键或调用exitFullscreen。 - 移动端浏览器(特别是iOS Safari和部分安卓浏览器):情况复杂得多。很多移动浏览器并不支持或严格限制真正的
Fullscreen API。它们更常见的是“横屏锁定”或“沉浸式网页应用”模式。当你旋转设备时,浏览器会尝试让页面横屏显示并可能隐藏一些UI。此外,移动端上常见的“添加到主屏幕”后以全屏模式打开的PWA(渐进式Web应用)又是另一种实现路径。
因此,在Cocos里做全屏,很多时候在移动端实际做的是“横屏适配”和“阻止默认竖屏行为”的工作,而非严格意义上的浏览器全屏。
3. Cocos Creator中实现全屏的完整方案
理解了原理,我们来看具体怎么做。我将分基础实现、增强健壮性、UI适配三个层次来构建一个完整的方案。
3.1 基础实现:让全屏工作起来
首先,你需要在场景中有一个用于触发全屏的节点,比如一个按钮。为这个按钮添加一个事件处理脚本。
1. 创建全屏管理模块(FullScreenManager.ts)一个好的习惯是将全屏逻辑集中管理,而不是散落在各个按钮脚本里。
// FullScreenManager.ts import { _decorator, Component, screen, SystemEvent, macro } from 'cc'; const { ccclass, property } = _decorator; @ccclass('FullScreenManager') export class FullScreenManager extends Component { // 单例模式,方便全局访问 private static _instance: FullScreenManager = null; public static get instance(): FullScreenManager { return FullScreenManager._instance; } // 当前是否处于全屏状态 private _isFullScreen: boolean = false; onLoad() { if (FullScreenManager._instance && FullScreenManager._instance !== this) { this.destroy(); return; } FullScreenManager._instance = this; // 建议不要轻易DontDestroy,除非全局需要 // director.addPersistRootNode(this.node); this.initFullScreenEvent(); } // 初始化全屏事件监听 private initFullScreenEvent() { // 监听引擎的全屏变化事件 screen.on('full-screen-change', this.onFullScreenChange, this); // 监听浏览器原生事件(作为备用) document.addEventListener('fullscreenchange', this.onNativeFullScreenChange.bind(this)); document.addEventListener('webkitfullscreenchange', this.onNativeFullScreenChange.bind(this)); // for Safari } // 请求进入全屏 public requestFullScreen(): void { // 关键:检查是否已有用户交互上下文。这里假设由按钮点击调用,所以是安全的。 // 更安全的做法是传入触发事件的Event对象,但Cocos封装后通常不需要。 if (screen.fullScreen()) { console.warn('Already in full screen mode.'); return; } // 调用引擎API const success = screen.requestFullScreen(); if (!success) { console.error('Failed to request full screen. It may not be called from a user gesture.'); // 可以在这里触发一个失败回调,或者显示提示给用户 // this.onFullScreenRequestFailed?.(); } } // 退出全屏 public exitFullScreen(): void { if (!screen.fullScreen()) { return; } screen.exitFullScreen(); } // 切换全屏状态 public toggleFullScreen(): void { if (screen.fullScreen()) { this.exitFullScreen(); } else { this.requestFullScreen(); } } // 引擎全屏变化回调 private onFullScreenChange(): void { this._isFullScreen = screen.fullScreen(); console.log(`Full screen changed via engine: ${this._isFullScreen}`); this.dispatchFullScreenChangeEvent(this._isFullScreen); } // 原生全屏变化回调(备用) private onNativeFullScreenChange(): void { const fullscreenElement = document.fullscreenElement || document.webkitFullscreenElement; // Safari const isFullScreen = !!fullscreenElement; // 如果引擎事件可能漏掉,可以用原生事件同步状态(但需谨慎处理重复触发) if (isFullScreen !== this._isFullScreen) { console.log(`Full screen changed via native API: ${isFullScreen}`); this._isFullScreen = isFullScreen; this.dispatchFullScreenChangeEvent(isFullScreen); } } private dispatchFullScreenChangeEvent(isFullScreen: boolean): void { // 这里可以触发一个自定义事件,通知游戏其他系统全屏状态变了 // 例如:重新布局UI、调整分辨率、暂停/恢复游戏等 this.node.emit('fullscreen-changed', isFullScreen); } public get isFullScreen(): boolean { return this._isFullScreen; } onDestroy() { screen.off('full-screen-change', this.onFullScreenChange, this); document.removeEventListener('fullscreenchange', this.onNativeFullScreenChange); document.removeEventListener('webkitfullscreenchange', this.onNativeFullScreenChange); if (FullScreenManager._instance === this) { FullScreenManager._instance = null; } } }2. 在按钮上使用创建一个简单的按钮控制脚本。
// FullScreenButton.ts import { _decorator, Component, Button } from 'cc'; import { FullScreenManager } from './FullScreenManager'; const { ccclass, property } = _decorator; @ccclass('FullScreenButton') export class FullScreenButton extends Component { @property(Button) public button: Button = null; onLoad() { if (this.button) { this.button.node.on(Button.EventType.CLICK, this.onButtonClick, this); } } private onButtonClick() { const manager = FullScreenManager.instance; if (manager) { manager.toggleFullScreen(); } else { console.error('FullScreenManager not found in scene.'); } } onDestroy() { if (this.button) { this.button.node.off(Button.EventType.CLICK, this.onButtonClick, this); } } }将这个脚本挂载到你的按钮节点上,并将按钮组件拖拽赋值。点击按钮,理论上就能切换全屏了。
3.2 增强方案:处理边界情况与提升用户体验
基础方案能跑通,但离“健壮”还差得远。下面这些增强点,是区分普通功能和优秀体验的关键。
1. 用户手势验证与优雅降级如前所述,非用户手势触发的全屏会失败。我们可以增加一个验证层。
// 在FullScreenManager中增加 private _pendingFullScreenRequest: boolean = false; public safeRequestFullScreen(userGestureEvent?: any): void { // 如果已经在全屏,或已有 pending 请求,则忽略 if (this._isFullScreen || this._pendingFullScreenRequest) return; // 尝试直接请求 if (screen.requestFullScreen()) { this._pendingFullScreenRequest = true; // 标记请求已发出,等待回调 } else { // 如果失败,可能是非用户手势。提供一个降级方案。 console.warn('Immediate fullscreen request failed. This might be due to lack of user gesture.'); // 方案A:显示一个提示,告诉用户“请点击屏幕任意位置进入全屏” // this.showFullScreenHint(); // 方案B:将请求缓存,在下一个真实的用户事件中触发(复杂,需事件代理) } } // 在全屏变化回调中重置 pending 状态 private onFullScreenChange(): void { const newState = screen.fullScreen(); this._pendingFullScreenRequest = false; // 无论成功失败,请求流程结束 this._isFullScreen = newState; console.log(`Full screen changed: ${newState}`); this.dispatchFullScreenChangeEvent(newState); }2. 处理ESC键退出用户按ESC退出全屏是浏览器默认行为,但我们需要捕获这个事件,以便同步更新游戏状态(比如更新全屏按钮的图标)。
// 在FullScreenManager的initFullScreenEvent中增加键盘监听 private initFullScreenEvent() { // ... 原有事件监听 // 监听键盘事件,主要用于检测ESC退出 systemEvent.on(SystemEvent.EventType.KEY_DOWN, this.onKeyDown, this); } private onKeyDown(event: EventKeyboard) { // macro.KEY.escape 是Cocos定义的键码常量 if (event.keyCode === macro.KEY.escape) { // 注意:按ESC时,浏览器已退出全屏,引擎事件会触发。 // 这里可以做一些额外的逻辑,比如防止游戏暂停等。 // console.log('ESC key pressed, fullscreen might exit.'); } }3. 移动端横屏适配对于移动端,全屏往往意味着横屏。你需要确保你的游戏Canvas适配横屏。
- 项目设置:在Cocos Creator的
项目设置->项目数据中,将适配屏幕宽度/高度策略设置好(通常SHOW_ALL或FIXED_WIDTH是安全选择)。 - 监听屏幕旋转:
import { view, ResolutionPolicy } from 'cc'; // 在游戏启动或全屏时,可以强制或建议横屏 public adjustForMobileLandscape(): void { // 设置设计分辨率策略,更适合横屏 view.setDesignResolutionSize(1920, 1080, ResolutionPolicy.SHOW_ALL); // 监听屏幕旋转事件(注意:浏览器API,非所有环境支持) window.addEventListener('orientationchange', this.onOrientationChange.bind(this)); } private onOrientationChange() { console.log(`Orientation changed: ${screen.orientation}`); // 根据新方向重新调整UI布局 this.dispatchFullScreenChangeEvent(this._isFullScreen); // 重新触发布局事件 } - 阻止竖屏默认行为:在
index.html的<meta>标签中,可以添加以下内容来建议浏览器横屏显示,但效果因浏览器而异。<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"> <!-- 尝试锁定横屏,支持性有限 --> <meta name="screen-orientation" content="landscape">
3.3 UI适配:全屏状态下的布局挑战
进入全屏后,你的游戏分辨率(screen.windowSize)很可能变了,但Canvas的设计分辨率可能没变。这会导致UI拉伸、错位或模糊。
解决方案:使用Widget组件和动态调整
- 对于静态UI(如背景):使用
Widget(对齐挂件)组件,将其对齐到父节点的各边,并勾选绝对位置或相对位置,使其能随父节点(通常是Canvas)大小变化而自动锚定。 - 对于需要动态调整的UI:监听我们
FullScreenManager发出的fullscreen-changed事件。// 在某个UI管理脚本中 onLoad() { const manager = FullScreenManager.instance; if (manager) { manager.node.on('fullscreen-changed', this.onFullScreenChanged, this); } // 初始调整一次 this.adjustUILayout(screen.fullScreen()); } private onFullScreenChanged(isFullScreen: boolean) { this.adjustUILayout(isFullScreen); } private adjustUILayout(isFullScreen: boolean) { const canvas = find('Canvas'); // 获取Canvas节点 const winSize = screen.windowSize; // 获取窗口实际大小 const designSize = view.getDesignResolutionSize(); // 获取设计分辨率 // 计算缩放比例和偏移 const scaleX = winSize.width / designSize.width; const scaleY = winSize.height / designSize.height; const scale = Math.min(scaleX, scaleY); // 通常取最小缩放以保证内容全部显示 // 调整UI根节点的缩放 canvas.setScale(scale, scale); // 如果你的UI需要根据宽高比进行不同的布局(如横屏竖屏) if (winSize.width > winSize.height) { // 横屏布局 this.arrangeUIForLandscape(); } else { // 竖屏布局 this.arrangeUIForPortrait(); } // 特别处理:全屏下可能需要隐藏或移动某些UI元素(如网页端的浏览器地址栏提示) const someUINode = this.node.getChildByName('AddressBarHint'); if (someUINode) { someUINode.active = !isFullScreen; // 非全屏时显示提示 } } - 字体和图像清晰度:如果缩放导致模糊,考虑为高清屏幕(如Retina)准备
@2x、@3x的图集,并在代码中根据devicePixelRatio动态设置canvas的width和height属性(这属于更高级的优化,Cocos引擎有相关设置)。
4. 常见问题排查与实战技巧
即使按照上面的方案做了,你依然可能会遇到一些诡异的问题。下面是我总结的“排坑指南”。
4.1 问题速查表
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 点击按钮,全屏无任何反应 | 1. 代码未被正确调用。 2. 调用时机非用户手势。 3. 浏览器安全策略阻止(如iframe中)。 | 1. 在按钮回调函数第一行加console.log,确认函数执行。2. 确保调用发生在 click、touchstart等事件的同步回调中。3. 尝试在浏览器控制台直接执行 document.querySelector('canvas').requestFullscreen(),看是否有错误。 |
| 全屏后,游戏画面只占中间一部分,有黑边 | UI适配未做好。Canvas缩放或Widget设置不正确。 | 1. 检查Canvas节点的Widget组件,确保四边对齐并勾选。2. 监听 fullscreen-changed事件,打印screen.windowSize和设计分辨率,计算并手动调整根节点缩放。 |
| 按ESC退出全屏后,游戏UI布局混乱 | 退出全屏时,未触发UI重新布局。 | 确保FullScreenManager在onFullScreenChange(无论是进入还是退出)时,都正确派发了状态变化事件,并且UI管理器监听了该事件。 |
| 在iOS Safari上无效或行为异常 | iOS对全屏API支持有限,或处于低电量模式等限制。 | 1. 考虑使用webkitEnterFullscreen等特定前缀API(Cocos已封装)。2. 引导用户手动点击屏幕上的“全屏”图标(浏览器自带)。 3. 将重点放在横屏适配和PWA上,而非强求API全屏。 |
| 在微信小游戏或其它平台无效 | 该平台可能不支持或修改了全屏API。 | 1. 使用条件编译(CC_WECHATGAME)。2. 调用平台提供的特定API,如微信小游戏的 wx.setKeepScreenOn或wx.createFullScreenAd(后者是激励视频广告全屏)。3. 查阅该平台的专属文档。 |
| 全屏切换时有明显闪烁或延迟 | 浏览器重绘、CSS过渡或引擎视图尺寸同步导致。 | 1. 为Canvas的容器div添加CSS样式{ transition: none; }。2. 尝试在请求全屏前,先隐藏一些复杂UI,完成后再显示。 |
4.2 实战技巧与心得
- “双保险”事件监听:如上面代码所示,同时监听Cocos引擎的
screen.on('full-screen-change')和原生的fullscreenchange事件。因为极少数情况下,引擎的事件可能因为封装层问题未能触发,原生事件可以作为兜底。 - 状态同步是核心:全屏不是一个动作,而是一个状态。你的游戏逻辑(如UI布局、摄像机视野、输入处理)都应该依赖于这个状态,而不是假设某个动作一定成功。
FullScreenManager维护一个_isFullScreen状态并对外提供事件,就是这个目的。 - 移动端优先考虑“横屏”而非“全屏”:在移动端项目规划初期,就按横屏设计UI和布局。把“全屏API”当作一个在支持它的浏览器上能获得更好体验的“增强功能”,而不是核心功能。这样在不支持的平台上,应用依然能正常以横屏模式运行。
- 测试,测试,再测试:全屏功能必须在你的目标平台和浏览器上逐一测试。Chrome、Firefox、Safari(Mac/iOS)、各手机厂商浏览器、微信内置浏览器、各种小游戏平台……它们的行为都可能不同。建立一个简单的测试用例,快速验证核心功能。
- 提供明确的用户反馈:当全屏请求失败时,不要静默失败。可以给用户一个友好的提示,例如:“您的浏览器不支持自动全屏,请尝试点击浏览器右上角的图标。”或者“请先点击游戏画面,再尝试全屏。”良好的用户体验来自于对失败情况的妥善处理。
全屏功能就像游戏开发中的许多其他特性一样,概念简单,细节魔鬼。它要求开发者不仅了解引擎API,还要对Web平台的特性和差异有深入的了解。希望这份指南能帮你构建一个稳定、可靠的全屏体验,让你能把更多精力集中在游戏内容本身,而不是和浏览器特性搏斗。