Cocos Creator 的 Web 屏幕适配链路:DPR 钳制、安全区域 CSS 变量与旋转帧的源码拆解
2026/9/17 20:35:10 网站建设 项目流程

Cocos Creator 的 Web 屏幕适配链路:DPR 钳制、安全区域 CSS 变量与旋转帧的源码拆解

【免费下载链接】cocos-engineCocos simplifies game creation and distribution with Cocos Creator, a free, open-source, cross-platform game engine. Empowering millions of developers to create high-performance, engaging 2D/3D games and instant web entertainment.项目地址: https://gitcode.com/GitHub_Trending/co/cocos-engine

在 iPhone 13 Pro 上跑一份 720×1280 的画布,把手机横过来 90 度:游戏框换了宽高,window.resize却没有触发;刘海屏的安全内边距只有顶部有,引擎却把上下补齐成对称矩形。Cocos Creator 的屏幕适配链路就埋着这样的取舍,全在 pal/screen-adapter/web/screen-adapter.ts 一个文件里。下面拆开它的 DPR 钳制、安全区域变量和旋转帧三段实现,看每个决定背后放弃了什么。

核心链路在哪

整条 Web 端适配只落在一个类上:ScreenAdapter在 pal/screen-adapter/web/screen-adapter.ts 里接管窗口事件、全屏状态和方向变化,输出是给#GameDiv#Cocos3dGameContainer这两个 DOM 节点写样式。适配容器的核心逻辑本质上就是在算一个"谁先撑满"的比值:

const designedResolution = legacyCC.view.getDesignResolutionSize() as Size; const frameW = frame.clientWidth; const frameH = frame.clientHeight; const designW = designedResolution.width; const designH = designedResolution.height; const scaleX = frameW / designW; const scaleY = frameH / designH; // ... 省略 3 行 if (scaleX < scaleY) { containerW = frameW; containerH = designH * scaleX; } else { containerW = designW * scaleY; containerH = frameH; }

横向或纵向先"撑满"窗口,另一边就按同一比例回缩——这是等比缩放不出变形的全部秘密,后面所有设计决策都是围绕它展开的。

设计决策拆解

DPR 上限为什么卡在 2 而不是直接透传

现象:3x 设备(iPhone 13 Pro 的window.devicePixelRatio是 3)上,screen.devicePixelRatio返回 2,screen.windowSize比真实物理像素小三分之一,UI 文字和系统界面比会显得略微发虚。

源码指向:pal/screen-adapter/web/screen-adapter.ts 第 100~103 行的 getter。

public get devicePixelRatio (): number { // TODO: remove the down sampling operation in DPR after supporting resolutionScale return Math.min(window.devicePixelRatio ?? 1, 2); }

?? 1是给老浏览器的兜底,真正的决定是Math.min这个上限:canvas 缓冲区的物理像素数等于 CSS 像素乘 DPR,3x 全屏渲染意味着过绘制面积接近翻倍,中端机直接掉帧,所以引擎统一降采样到 2。代价是写死在 getter 里没有出口——cocos/core/platform/screen.ts 里resolutionScale的 setter 整段被注释掉(第 103~108 行),项目侧目前无法调回 3x,高 DPI 设备永远是"用清晰度换帧率"的方案 A。

安全区域为什么读 CSS 变量而不是 JS API

现象:刘海屏上safeAreaEdge能拿到顶部 inset;但一旦构建模板没配置viewport-fit=cover,四个方向静默全部为 0,SafeArea组件形同虚设,且没有任何报错。

源码指向:读取端在 pal/screen-adapter/web/screen-adapter.ts 第 152~165 行,变量定义在 templates/web-mobile/style.css 第 56~61 行的:root

:root { --safe-top: env(safe-area-inset-top); --safe-right: env(safe-area-inset-right); --safe-bottom: env(safe-area-inset-bottom); --safe-left: env(safe-area-inset-left); }
public get safeAreaEdge (): SafeAreaEdge { const dpr = this.devicePixelRatio; const _top = parseInt(getComputedStyle(document.documentElement).getPropertyValue('--safe-top') || '0') * dpr; // ... 省略 3 行(bottom/left/right 同 top 的处理) return { top: _top, bottom: _bottom, left: _left, right: _right }; }

走 CSS 变量中转是因为env(safe-area-inset-*)是浏览器在样式层计算的,JS 在非 CSS 上下文里根本没有 API 能读到它——CSS 变量是当前唯一跨浏览器的通道。乘dpr是把 CSS 像素换算成物理像素,与windowSize的单位对齐;|| '0'防的是变量未定义时parseIntNaN。取舍在于:引擎放弃了一个"标准 JS API"的体面,换取了 iOS/Android/微信容器都能工作的兼容面,代价是整条链路完全寄生在构建模板上——模板被改坏,引擎只会安静地返回 0。

横屏游戏在竖屏手机上怎么"转正"

现象:配置为竖屏的项目被用户横着拿手机,画面不会被拉宽,而是整个游戏框直立着占住屏幕中央两侧留黑边,就像外层 div 被写了rotate(90deg)

源码指向:判定在_updateFrameState(第 595~603 行),执行在_resizeFrame(第 538~545 行),同一文件。

// 判定:配置方向与物理方向冲突,且只针对移动端 this.isFrameRotated = systemInfo.isMobile && ((isBrowserLandscape && orientation === Orientation.PORTRAIT) || (!isBrowserLandscape && orientation === Orientation.LANDSCAPE)); // 执行:frame 宽高互换、旋转 90 度,margin 把转出去的框挪回视口 // ... 省略 2 行(-webkit- 前缀的同款 transform) if (this.isFrameRotated) { this._gameFrame.style.transform = 'rotate(90deg)'; this._gameFrame.style.transformOrigin = '0px 0px 0px'; this._gameFrame.style.margin = `0 0 0 ${winWidth}px`; this._gameFrame.style.width = `${winHeight}px`; this._gameFrame.style.height = `${winWidth}px`; }

方向位定义在 pal/screen-adapter/enum-type/orientation.ts 第 25~32 行,用位运算让AUTO = 15能覆盖所有方向组合,orientation比较天然支持"横屏左右皆可"这类配置。选 DOM transform 而不是重排 canvas,是因为旋转发生在合成层,引擎侧零拷贝成本,2D/3D 场景内容完全无感。但这个技巧在移动端全屏下会失效——_getFullscreenTarget(第 569~571 行)直接返回document.body做全屏目标,注释写明 "the transform of game frame doesn't work when it's on fullscreen",于是旋转帧逻辑被整条绕过,这就是下面第一个坑的来源。

落到项目里

💡 顶部栏/底部栏这种必须避开刘海和 Home 指示条的组件,不要自己读env(),直接消费引擎已经换算好的物理像素数据:

import { _decorator, Component, screen, sys, view, UITransform } from 'cc'; const { ccclass } = _decorator; @ccclass('SafeTopBar') export class SafeTopBar extends Component { onLoad (): void { // 引擎的 window-resize 已在方向变化防抖后才派发,不必自己再监听 matchMedia screen.on('window-resize', this.refresh, this); screen.on('orientation-change', this.refresh, this); this.refresh(); } onDestroy (): void { screen.off('window-resize', this.refresh, this); screen.off('orientation-change', this.refresh, this); } refresh (): void { const visible = view.getVisibleSize(); // symmetric=false 保留真实上下 inset,只算顶部偏移 const safeRect = sys.getSafeAreaRect(false); const topInset = Math.max(visible.height - safeRect.y - safeRect.height, 0); const ui = this.node.getComponent(UITransform)!; ui.setContentSize(ui.width, ui.height - topInset); } }

适用边界:H5 里顶栏、血条、返回键这类贴边组件可以直接用;微信/字节小游戏容器里 CSS 变量不一定被填充(小游戏走 pal/screen-adapter/minigame/screen-adapter.ts 的独立实现),贴边组件建议改用平台自带的安全区 API 双通道取值。

多分辨率适配的另一半在设计分辨率映射上,一行即可声明策略:

import { view, ResolutionPolicy } from 'cc'; // 设计分辨率 720×1280,按最小边缩放保证内容完整显示(SHOW_ALL) view.setDesignResolutionSize(720, 1280, ResolutionPolicy.SHOW_ALL);

适用边界:SHOW_ALL在任何宽高比下不裁切内容,是默认安全选项;但在 18:9 长屏上左右黑边会明显偏宽,想撑满画面应切到FIXED_HEIGHT,再手动处理左右超出区域的 UI 裁切——策略清单和各自实现见 cocos/ui/view.ts 第 94~99 行。

边界与翻车记录

⚠️旋转后 UI 刷新慢半拍,前 200ms 布局用的还是旧尺寸。根因:方向变化回调被EVENT_TIMEOUT防抖(Web 200ms、编辑器内 5ms,第 41 行与第 431~433 行,pal/screen-adapter/web/screen-adapter.ts),因为orientationchange事件触发时window.innerWidth/innerHeight尚未更新。处理:布局逻辑挂在引擎的'orientation-change'/'window-resize'上,别自己再写matchMedia双份监听,否则会用旧尺寸先刷一次错版。

⚠️进全屏后,竖屏游戏在横拿的手机上不再转正,画面被拉宽。根因:移动端全屏时 frame 的 transform 不生效,_getFullscreenTarget回退到document.body(第 569~571 行),旋转帧那套样式写到了不会被全屏命中的节点上。处理:全屏场景下别依赖isFrameRotated,一律以sys.windowSize为准重算布局;产品上要么限制全屏入口只出现在同方向设备,要么接受全屏内画面拉伸。

⚠️刘海只在顶部,布局却把底部也顶下来一截。根因:sys.getSafeAreaRect默认symmetric=true,会把上下(或左右)取大值补齐成对称矩形(cocos/core/platform/sys.ts 第 374~386 行)。这是有意为之的保守策略——横竖切换后刘海位置会变,对称值最不容易把内容顶进裁切区。处理:确实需要真实非对称 inset 时传getSafeAreaRect(false),或直接读screenAdapter.safeAreaEdge原始值。

往哪走

想看设计分辨率到可见区的完整映射(EXACT_FIT/SHOW_ALL/FIXED_HEIGHT五种策略的实现差异),入口在 cocos/ui/view.ts 的ResolutionPolicy族;想看引擎官方怎么把安全区域数据喂给布局系统,SafeArea组件(cocos/ui/safe-area.ts)演示了getSafeAreaRectWidget的对齐联动。如果你手上某个具体机型出现了适配问题,欢迎在项目 issue 里贴复现步骤:机型、浏览器/容器版本、物理朝向,三段信息齐了就能直接定位到上面哪一环。

【免费下载链接】cocos-engineCocos simplifies game creation and distribution with Cocos Creator, a free, open-source, cross-platform game engine. Empowering millions of developers to create high-performance, engaging 2D/3D games and instant web entertainment.项目地址: https://gitcode.com/GitHub_Trending/co/cocos-engine

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询