UE5.8 多平台性能适配

📑 目录

七月,深圳。

小林把包放在工位上,打开测试机,屏幕右下角弹出一条玩家反馈:「战斗到第三波怪物时,手机烫到握不住,帧率从六十掉到二十几。」项目代号「裂空」的动作游戏正在做 UE5.8 的多平台适配,目标平台包括 Windows、iOS 和 Android。同一套资产要在骁龙 7 系、A17 Pro 和 RTX 3060 上同时跑通,性能差异能达到十几倍。团队决定从分辨率、画质、帧率、内存和发热这五个维度建立一套自适应体系。

小林先翻了翻后台数据。过去一周,低端 Android 设备的平均帧率只有 24,崩溃率 3.2%,其中超过六成崩溃集中在战斗开始后的九十秒内。iOS 高端机表现好一些,平均 52 帧,但温度超过 45 度的会话占比也有 18%。PC 端反而是最稳的,RTX 3060 能稳在 60 帧以上,只是部分玩家的笔记本会在插电和电池模式之间切换时出现突然掉帧。问题很明确:不能再用同一套参数跑所有平台。

UE5.8 的渲染管线比 5.4 更重,Lumen、Nanite 和 Virtual Shadow Maps 默认全开时,移动 GPU 根本吃不消。直连控制台的思路在跨平台项目里走不通,必须让引擎根据当前硬件、温度和电量动态决策。下面以小林遇到的具体问题为线索,记录这套方案。

一、分辨率先让步:Dynamic Resolution

玩家反馈里最常见的话是「画面糊了总比卡死强」。Dynamic Resolution 就是按这个思路设计的。每一帧渲染之前,引擎会根据上一帧的 GPU 耗时决定当前帧的渲染比例。预算由目标帧率决定:

Frame Budget=1000Target FPSms\text{Frame Budget} = \frac{1000}{\text{Target FPS}} \,\text{ms}

60FPS60\,\text{FPS} 为例,单帧时间只有 16.67ms16.67\,\text{ms};如果降到 30FPS30\,\text{FPS},预算就变成 33.33ms33.33\,\text{ms}。当 GPU 耗时连续几帧超出预算,渲染比例从 100%100\% 往下掉;负载回落后再缓慢回升。UI 层保持原分辨率,避免整体模糊。

UE5.8 的 r.DynamicRes.OperationMode 支持几种模式:

  • 0 关闭;
  • 1 根据 GPU 时间自适应;
  • 2 固定比例,只由外部逻辑控制。

对小林来说,模式 1 最省心。配置如下:

[/Script/Engine.RendererSettings]
r.DynamicRes.OperationMode=1
r.DynamicRes.MinScreenPercentage=50.0
r.DynamicRes.MaxScreenPercentage=100.0
r.DynamicRes.FrameTimeBudget=16.0

MinScreenPercentage 不能设得太低,否则文字和远处敌人会糊成一片。团队最后定在 50%50\%100%100\% 之间,每次步进 5%5\%

flowchart TD
    A["上一帧 GPU 超时"] --> B{当前缩放是否到最小}
    B -->|"否"| C["降低 5% 渲染比例"]
    B -->|"是"| D["保持最小比例"]
    C --> E["渲染当前帧"]
    D --> E
    E --> F["UI 以全分辨率合成"]
    F --> G["下一帧重新评估"]

这个图里,UI 合成发生在渲染比例调整之后。如果 UI 也一起缩放,玩家会立刻察觉到画面发虚。保留 HUD 和文字清晰是 Dynamic Resolution 能被接受的关键。

小林在测试中还发现,渲染比例的升降速度要分开设置。掉下去可以快,升上来必须慢。如果负载波动一帧就拉回 100%100\%,画面会频繁在清晰和模糊之间抖动,比一直模糊还难受。UE5.8 的 r.DynamicRes.HistorySize 控制历史帧数量,增大这个值能让决策更平滑。项目里把历史帧从默认的 6 帧调到 12 帧,抖动明显减少。

Dynamic Resolution 还有一个好处是可以和 TSR 配合。TSR 本身会把低分辨率图像重建到目标分辨率,Dynamic Resolution 负责决定 TSR 输入端的分辨率。两者一起用,低端机在视觉上损失的清晰度比单纯降分辨率小得多。小林把 TSR 的 ScreenPercentage 和 Dynamic Resolution 的缩放范围绑定,避免两个系统打架。

另一个细节是半透明物体。后处理阶段的 Bloom、Depth of Field 和 Motion Blur 对分辨率变化很敏感。分辨率降低后,Bloom 的阈值和半径要同步调整,否则会出现光斑边缘锯齿。小林在后处理体积里按当前缩放比例动态重算了这些参数。

二、画质分级:Scalability

分辨率只能解决 GPU 压力的一部分。阴影、后期处理、反射和粒子数量同样吃资源。UE5.8 的 Scalability 系统把画质拆成多个组:AntiAliasing、View Distance、Shadow、Post Process、Texture、Effect、Foliage、Shading。每个组 0 到 3 级,对应 Low、Medium、High、Epic。

flowchart TD
    preset["画质预设"]
    aa["抗锯齿"]
    shadow["阴影"]
    post["后处理"]
    tex["纹理"]
    preset --> aa
    preset --> shadow
    preset --> post
    preset --> tex

项目里,小林根据设备型号映射到预设:

FScalability::FQualityLevels UMyGameUserSettings::GetPresetByDevice()
{
    FString Name = FPlatformMisc::GetDeviceProfileName();
    if (Name.Contains("Adreno7")) return LowPreset;
    if (Name.Contains("AppleA17")) return HighPreset;
    return MediumPreset;
}

代码不到二十行,核心逻辑是读取设备型号再返回对应的 FQualityLevels。真正的工作在设备配置文件里完成,用 BaseDeviceProfiles.ini 给不同 GPU 指定默认 Scalability 等级。

需要注意,画质分级不是越细越好。如果等级太多,QA 测不过来,玩家也看不出差别。团队最终保留了 Low、Medium、High 三档,Epic 只在 PC 上开放。

具体到低档 Android,小林关掉了 Lumen 和 Virtual Shadow Maps,把阴影距离从 10000 压到 3000,后处理品质降到 Low,粒子同屏数砍掉一半。中档机型保留 Lumen Lite,阴影用中等分辨率。高档 iOS 才允许开启完整 Lumen 和 Nanite。每一档的切换不是简单改数字,而是要重新跑一遍性能基准,确认没有某个隐藏开关把帧时间拉到预算外。

Scalability 还涉及视野距离。低端机上把 View Distance Scale 从 1.0 降到 0.6,远处的小物件和植被不再绘制,CPU 的剔除压力也小了很多。这个改动对开放世界场景尤其有效,但室内关卡影响不大,所以团队按关卡类型单独配置了默认值。

粒子系统的分级最容易被忽略。Niagara 的 Quality Level 可以按 Scalability 等级切换不同的 Emitter。Low 档直接关闭远景尘埃、火星和血迹溅射,只保留近身的命中特效。Medium 保留半数粒子,High 才全开。这个调整对 GPU 的压力下降明显,美术起初担心表现会打折扣,但实际玩下来发现战斗可读性反而提高了。

三、帧率锁定与自适应

手机上常见的帧率选项是 30、40、60,部分高刷机型支持 90 和 120。目标帧率一旦选定,Dynamic Resolution 的预算就跟着定了。玩家切出设置界面后,引擎要立即生效,不能等下次重启。

锁定帧率有两层含义。一是固定 t.MaxFPS 或项目设置里的 Frame Rate Limit;二是让 VSync 与屏幕刷新率对齐。如果目标 30 而屏幕刷新 60,开 VSync 会稳定在两倍刷新间隔,画面反而平滑。如果目标 40 而屏幕刷新 60,VSync 会造成抖动,此时需要关闭 VSync 或者改成 30。

sequenceDiagram
    participant P as "玩家"
    participant S as "设置界面"
    participant G as "游戏循环"
    participant R as RHI
    participant OS as "操作系统"
    P->>S: 选择 30 FPS
    S->>G: 写入 TargetFPS
    G->>G: 重新计算 Frame Budget
    G->>R: 设置交换链间隔
    R->>OS: 调用 Platform API
    OS-->>R: 确认刷新同步
    R-->>G: 返回同步状态
    G-->>P: 画面稳定输出

小林在 iPhone 15 Pro 上发现一个细节:目标帧率从 60 降到 30 后,GPU 负载并没有减半,因为 Lumen 和反射还在按 30 帧的预算跑。真正让功耗下来的是同时下调画质等级和分辨率缩放上限。

高刷屏幕的选择也有讲究。90 FPS 和 120 FPS 虽然顺滑,但对发热和续航的冲击很大。团队在设置里把 90 和 120 标为「高性能模式」,默认只在电量充足且设备温度正常时显示。低电量或过热时,这些选项会变灰,避免玩家误选后体验更差。

这里有一个常见的坑:菜单界面、暂停画面和过场动画不需要 60 FPS。小林给这些场景单独设置了帧率上限。打开背包时降到 30,播放剧情动画时锁 24。别看这几秒钟的切换,累积起来对发热和续航的影响很明显。

四、内存与发热控制

移动设备的散热天花板很低。CPU 和 GPU 共享同一套热管,温度上来后系统会降频。项目初期,团队用 Perfetto 抓取 Android 的 Systrace,发现温度超过 42C42^\circ\text{C} 后,大核频率会被系统压低到原来的 60%60\%。这个降频不是游戏能控制的,但游戏可以预先降低负载,让温度升得慢一些。

控制发热的手段有几种:

  1. 降低阴影贴图分辨率,减少带宽;
  2. 限制粒子同屏数量;
  3. 缩短视野距离,少渲染远处物体;
  4. 降低后处理品质,尤其是 Bloom 和 Motion Blur。

发热控制还要关注内存。移动端的内存超分配会被系统直接杀进程。UE5.8 的 s.Memory.MaxAsyncLoadingBudget 和纹理流送池大小需要根据设备 RAM 分级。2GB 内存机型把纹理池压到 256MB,4GB 机型放宽到 512MB,8GB 以上机型才使用 1GB。

小林的做法是每 5 秒读取一次设备温度,再按阈值触发降质:

void UMyGameInstance::UpdateThermalState()
{
    float Temp = FAndroidMisc::GetDeviceTemperatureLevel();
    if (Temp > 45.0f) ApplyThermalPreset(EThermalLevel::Critical);
    else if (Temp > 42.0f) ApplyThermalPreset(EThermalLevel::Warm);
    else ApplyThermalPreset(EThermalLevel::Normal);
}

这个函数在 Game Instance 的 Tick 里调用,温度正常时什么事情都不做。升到 Warm 就降一档画质,升到 Critical 再降一档并锁 30 FPS。温度回落后不会立刻恢复,而是等连续 60 秒低于阈值才升回去,防止在边界来回横跳。

内存方面,小林还关闭了默认开启的 HDR 渲染管线。移动端很多屏幕亮度范围有限,HDR 带来的收益不大,却需要额外的 Render Target 和 Tone Mapping 开销。改成 LDR 后,内存占用减少了约 80MB,发热也有所下降。

另一个容易爆内存的地方是 Texture Streaming。UE5.8 默认会根据纹理池大小动态流送,但如果关卡里贴图密度过高,低端机会频繁出现贴图模糊闪烁。小林把 r.Streaming.MipBias 在 Low 档提到 1,让远处贴图用更低的 Mip,虽然清晰度有所下降,但不会出现加载跟不上镜头转动的情况。

五、平台专属优化

Windows、iOS、Android 三端的优化点完全不同。

PC 端主要防 CPU 瓶颈。桌面 CPU 核心数多但主频差异大,如果逻辑线程和渲染线程抢核心,帧时间会不稳定。UE5.8 的 TaskGraph 已经做了核心隔离,项目里把渲染线程绑在物理核心上,避免被后台任务挤占。

iOS 端要利用 Metal 的 TBDR 架构。尽量减少 Render Target 切换,把多个后处理合并到同一个 Render Pass。UE5.8 的移动延迟渲染管线对 MSAA 的支持有所改善,但小林还是建议关闭 MSAA,改用 Temporal AA,省下的带宽可以留给纹理采样。

Android 端的问题最杂。不同厂商的 GPU 驱动对同一 API 的行为可能不同。团队维护了一份黑名单:某些 Mali GPU 开启 Virtual Shadow Maps 会闪退,某些 Adreno 在 Vulkan 下会出现花屏。项目用 Device Profile 做特性开关,而不是在运行时检测 GPU 名称再 if-else。

graph LR
    A["低端 Android"] --> B["关闭 Lumen"]
    B --> C["关闭 Virtual Shadow Maps"]
    C --> D["降低纹理池"]
    D --> E["目标 30 FPS"]
    F["高端 iOS"] --> G["开启 Lumen"]
    G --> H["保留 Nanite"]
    H --> I["目标 60 FPS"]

PC 上小林还做了一件小事:默认关闭 Motion Blur 和 Chromatic Aberration。这两个效果在高帧率下对体验贡献有限,但会增加 GPU 开销。主机和高端 PC 玩家如果想开,可以在设置里手动打开。

Android 的 Shader 变体编译也是个大头。低端机第一次进游戏会卡很久,因为 Pipeline State Object 还没缓存。UE5.8 的 Shader Pipeline Cache 可以预编译常用 PSO,小林在打包时把缓存文件一起打进去,冷启动时间从 38 秒降到 12 秒。

六、电池与续航

续航是移动游戏留存的重要指标。玩家在连续游玩时,电量下降速度直接影响次日打开率。UE5.8 没有一键省电模式,但可以通过组合策略实现类似效果。

电池电量低于 20%20\% 时,系统会提示低电量模式。游戏应该监听这个事件,把目标帧率降到 30,关闭屏幕空间反射,并把分辨率缩放上限从 100%100\% 锁到 75%75\%。充满电或接入电源后,再自动恢复。

计算功耗的基本关系可以写成:

PdrawkV2fCP_{\text{draw}} \approx k \cdot V^2 \cdot f \cdot C

其中 VV 是工作电压,ff 是 GPU/CPU 频率,CC 是等效负载电容,kk 是与制程和散热相关的系数。降压降频的节能效果是非线性的,频率降到一半,功耗往往不到一半。这也是 30 FPS 比 60 FPS 续航长得多的原因之一。

小林在测试时发现,屏幕亮度对续航的影响比想象中大。很多游戏在省电模式里只顾着降画质,忘了提醒玩家调低亮度。团队在低电量提示里加了一句话:建议关闭自动亮度并调低屏幕亮度。这个文案看起来不起眼,实际能把每小时耗电减少 8% 到 12%。

七、性能测试与基准

性能适配不能靠感觉,必须建立可重复的基准。团队每天早上跑一轮自动化测试:同一台测试机从冷机状态启动游戏,进入固定关卡,记录前三十分钟的帧率、温度和电量。

测试数据记录在 InfluxDB 里,Grafana 看板按版本号对比。关键指标包括:

  • 平均帧率和 P1、P99 帧时间;
  • 掉帧次数,即帧时间超过预算 50%50\% 的帧数;
  • 温度曲线;
  • 电量消耗速率,单位 mAh/min。

基准测试最怕的是环境不一致。同一台手机,电量 100%100\%20%20\% 时的峰值频率可能不同;室温 25C25^\circ\text{C}35C35^\circ\text{C} 时的发热曲线也完全不同。所以测试必须在恒温箱里进行,手机全程连接电源但关闭充电优化。

小林还加了一个回归检测脚本。每次构建完成后,脚本自动对比新版本和上一个稳定版的关键指标。如果平均帧率下降超过 5%5\%,或者 P99 帧时间上升超过 10%10\%,CI 就标红并通知相关负责人。这个机制帮团队抓到过两次 Shader 变体爆炸导致的隐性回归。

测试用例的设计也要贴近真实玩法。自动脚本不能只站在原地看风景,要模拟移动、攻击、释放技能和切换镜头。团队用 UE5.8 的 Functional Testing 框架录了一段战斗回放,每天在测试机上重放同一段操作,保证数据可比。

八、把选择权交给玩家

自动适配能覆盖大部分场景,但总有一些玩家愿意牺牲画质换帧率,或者愿意锁 30 帧换续航。设置界面要给玩家留下清晰的选项。

推荐的选项分组:

  • 帧率:30 / 40 / 60 / 90 / 120,只显示当前设备支持的模式;
  • 画质:Low / Medium / High,附带简短说明;
  • 分辨率:自动 / 高 / 中 / 低;
  • 省电模式:开关,开启后强制 30 FPS 并降低特效;
  • 自定义:高级玩家可以单独调整阴影、反射、抗锯齿。

每一项改动都要即时生效,而不是提示「重启后生效」。UE5.8 的 UGameUserSettings 在 ApplySettings 后会重新编译 Shader 变体,这个过程在高端机上不到半秒,低端机上可能要一两秒,需要显示进度条避免玩家以为卡死。

小林在 UI 设计里坚持一个原则:选项名称要让玩家一眼看懂后果。把「Dynamic Resolution」翻译成「动态分辨率」对普通玩家没有意义,改成「战斗掉帧时自动降低画质」就直观多了。同理,「Thermal Throttling」不直译成热节流,而是用「设备过热保护」这种玩家能理解的表达。

设置变更最好有 A/B 恢复机制。玩家如果调乱了画面,可以一键恢复推荐设置。小林在本地存了一份默认预设和一份当前预设,恢复时直接覆盖即可,不需要重新检测设备能力。

九、把适配流程固化到管线

人工调完一次 preset 后,如果没有文档和工具,下个版本很容易回到原点。小林把设备分级、Scalability 映射、温度阈值、帧率选项全部写进一张 CSV 表,打包时由 Python 脚本生成对应的 ini 文件。策划和美术可以直接改表,不需要碰代码。

这张表的结构大致如下:

DeviceProfileScalabilityMaxFPSTexturePoolThermalWarmThermalCritical
Adreno7xxLow302564144
AppleA16Medium605124346
AppleA17High6010244346
MaliG710Low302564043

每次新增机型,只需要在表里加一行,CI 自动生成配置并跑基准。这样适配工作从一次性的调参变成了可持续维护的流程。

结语

多平台性能适配没有终点。新机上市后,旧机的表现会相对变差;引擎版本升级后,旧的 Scalability 预设可能需要重新标定。小林把这套规则写进 CI:每出一次构建包,自动在五种代表机型上跑基准,帧率或温度超过阈值就发邮件告警。

最后回到那封玩家反馈。三天后,团队发布了补丁:Dynamic Resolution 下限从 40%40\% 调到 50%50\%,发热时自动降一档画质,低电量默认开启省电模式。那位玩家回复说:「现在能玩半小时不发烫了。」这句话比任何 benchmark 都更让人安心。