【OpenHarmony/HarmonyOS】用 DisplaySync 打造稳定游戏循环:固定时间步长、120 FPS 与降级策略
2026/7/22 23:25:28 网站建设 项目流程

【OpenHarmony/HarmonyOS】用 DisplaySync 打造稳定游戏循环:固定时间步长、120 FPS 与降级策略

游戏是否流畅,不只取决于“每秒画多少帧”,还取决于逻辑是否按稳定时间推进。本文讲解 HarmonyOS 中如何使用DisplaySync驱动 Canvas 游戏,并用固定时间步长避免不同设备上的速度漂移。⚙️

一、普通定时器为什么不够?

很多 2D 游戏原型从下面的代码开始:

setInterval(()=>{ update(); render(); },16);

它简单,但存在明显问题:

  • 16ms并不等于严格 60 FPS,实际调度会受主线程任务影响;
  • 屏幕刷新与定时器触发不同步,可能出现重复绘制或错过刷新;
  • 应用短暂卡顿后,游戏逻辑可能突然跳过一大段距离;
  • 高刷新率设备无法充分利用 90Hz、120Hz 屏幕;
  • 前后台切换后累积的时间差可能让物理系统失控。

HarmonyOS 提供@ohos.graphics.displaySync,可以让回调尽量跟随显示刷新节奏。项目优先使用DisplaySync,不可用时再退回setTimeout

二、把更新和渲染从引擎中解耦

游戏循环类只接收两个回调:

exportclassGameLoop{privateupdateCallback:(deltaTime:number) =>void;privaterenderCallback:() =>void;constructor(update: (dt:number) =>void, render: () =>void) {this.updateCallback= update;this.renderCallback= render; } }

它不认识坦克、子弹或迷宫,只负责“什么时候更新”和“什么时候绘制”。GameEngine在构造时注入具体逻辑:

this.gameLoop=newGameLoop((deltaTime:number) =>this.update(deltaTime),() =>this.render() );

这样的好处是可以独立测试时间推进,也可以在暂停、后台或页面销毁时统一停止,而不必让每个实体关心调度器。

三、优先启用 DisplaySync 🖥️

初始化时尝试创建同步对象:

try{this.displaySync = displaySync.create();this.useDisplaySync =true; }catch(error) { console.warn('[GameLoop] DisplaySync not supported');this.useDisplaySync =false; }

启动时设置期望刷新范围并监听帧事件:

this.displaySync.setExpectedFrameRateRange({ expected:120, min:60, max:120});this.displaySync.on('frame', () => {this.runSingleFrame(); });this.displaySync.start();

expected: 120是性能目标,不是绝对保证。设备硬件、系统功耗策略、温度和当前负载都可能让实际刷新率下降。因此游戏逻辑不能假设每次回调都正好间隔8.33ms

四、固定时间步长是什么?

项目把逻辑步长固定为1 / 120秒。每帧先计算真实经过时间,将其加入累加器;只要累加器足够执行一个逻辑步,就调用一次update(dt)

consttargetFPS =120;consttargetFrameTime =1000/ targetFPS;constcurrentTime = Date.now(); let frameTime = currentTime -this.lastTime;this.lastTime = currentTime;this.accumulator += frameTime;constdt = targetFrameTime /1000;while(this.accumulator >= targetFrameTime) {this.updateCallback(dt);this.accumulator -= targetFrameTime; }this.renderCallback();

假设某次刷新实际间隔 17ms,固定步长约为 8.33ms,那么本帧会执行两次逻辑更新,再绘制一次。这样坦克速度仍然等于“像素/秒”,AI 计时器和子弹寿命也不会因为屏幕刷新变化而改变。

固定步长的优势

  • 物理结果更稳定,碰撞边界更容易复现;
  • 不同刷新率设备上游戏速度一致;
  • 网络对战更容易按逻辑 Tick 设计同步;
  • 录制输入后可以按相同步长回放;
  • 测试不需要依赖真实等待时间。

它并不等于固定渲染帧率

逻辑可以一帧更新多次,也可以某次刷新不更新只重绘。渲染频率由显示系统决定,逻辑频率由固定步长决定,二者是解耦的。

五、为什么要限制最大帧间隔?

应用被断点暂停、系统调度或切到后台后,再回来时Date.now() - lastTime可能是几秒甚至几分钟。如果把全部时间补算回来,循环会在一帧内执行成百上千次更新,主线程继续被占满,这就是常说的“死亡螺旋”。

项目先把单帧差值限制到 250ms:

if (frameTime >250) { frameTime=250;}

同时限制单帧最多补算的逻辑步数:

let updateSteps =0;while(this.accumulator >= targetFrameTime) {this.updateCallback(dt);this.accumulator -= targetFrameTime; updateSteps++;if(updateSteps >=240) {this.accumulator =0;break; } }

对于实时游戏,“丢掉过久的历史”通常比“补齐每一个历史步骤”更合理。玩家更在意恢复后立即可操作,而不是让游戏为了追赶十秒前的状态继续卡住。

六、DisplaySync 不可用时如何降级?

兼容路径使用递归setTimeout,而不是setInterval

privateloopFallback = () => {if(!this.running)return;this.runSingleFrame();constfps = GameConfig.getInstance().targetFPS ||60;constframeTime =1000/ fps;this.timerId = setTimeout(this.loopFallback, Math.max(1, frameTime -5) )asnumber; }

递归setTimeout会在当前帧完成后再安排下一次调用,不会像setInterval那样在主线程繁忙时不断积累待执行任务。减去少量时间是为了补偿调度误差,但真正的逻辑速度仍由累加器决定。

需要注意,当前工程中 DisplaySync 路径固定期望 120 FPS,而降级路径读取设置页中的目标帧率。若要让设置完全一致,应把统一的目标帧率传入runSingleFrame(),避免配置只影响降级分支。

七、开始与停止必须幂等

页面切换、Canvas 重建和重新开始游戏都可能重复调用start()。如果没有保护,就会出现两个循环同时更新同一个引擎,表现为速度翻倍、音效重复和子弹异常。

start() {if(this.running)return;this.running =true;this.lastTime = Date.now();this.accumulator =0;// 启动 DisplaySync 或 fallback} stop() {this.running =false;this.displaySync?.stop();if(this.timerId !== -1) { clearTimeout(this.timerId);this.timerId = -1; } }

停止时除了更新标志,还必须停止系统同步对象并清理定时器。页面的aboutToDisappear、游戏退出和重新初始化都应该走同一个停止入口。

八、暂停游戏应该暂停什么? ⏸️

暂停有两种常见实现:

方案 A:停止整个 GameLoop

优点是省电,暂停时不会继续绘制。恢复时必须重置lastTime和累加器,否则会把暂停时间当作游戏时间。

方案 B:循环继续,但跳过世界更新

优点是暂停菜单背景仍可保持粒子动画;缺点是仍有 CPU/GPU 开销。项目的引擎在非playing状态下保留部分粒子更新,然后提前返回,这属于折中设计。

无论选择哪一种,都要明确这些计时来源:

  • 固定步长累计的逻辑时间;
  • Date.now()计算的真实时间;
  • ArkUI 定时器更新的 HUD 时间。

如果限时模式直接用Date.now() - startTime,暂停期间也会继续倒计时。若产品要求真正暂停,应累计有效运行时间,或在恢复时补偿暂停时长。

九、120 FPS 下的性能预算

120 FPS 意味着每帧总预算约8.33ms,其中还包括 ArkUI、系统合成和其他任务。仅仅设置期望帧率并不能获得流畅体验,逻辑和绘制都必须控制开销。

项目采用了几类配套措施:

  • 子弹和粒子使用对象池,减少 GC;
  • 死亡对象使用尾部交换删除,避免splice搬移数组;
  • 迷宫只遍历可见区域;
  • 同色墙体、子弹和晶石批量绘制;
  • AI 每 1 秒重新计算路径,而不是每帧执行 A*;
  • 网络状态约每 3 帧广播一次,而非每个显示帧发送;
  • 高频路径原地修改坐标,减少临时Vector2

建议增加的监控指标

  • 一秒内实际回调次数;
  • 每帧逻辑更新次数;
  • updaterender各自耗时;
  • 当前子弹、粒子和 AI 数量;
  • 被丢弃的累计时间;
  • GC 或长任务造成的峰值帧间隔。

只有测量这些指标,才能判断瓶颈来自绘制、AI、碰撞还是系统调度。

十、时间戳与精度的改进建议

项目的帧事件虽然提供时间戳,但为了统一逻辑使用了Date.now()。这是可行的原型方案,不过高精度游戏可以进一步改进:

  1. 使用单调时钟或帧回调时间戳,避免系统时间调整影响;
  2. 明确时间戳单位,纳秒应转换为毫秒;
  3. 为渲染增加插值因子alpha = accumulator / step
  4. 保存上一逻辑位置和当前逻辑位置,渲染时插值得到平滑坐标;
  5. 后台恢复时显式重置计时,而非只依赖 250ms 截断。

插值的基本形式如下:

constalpha= accumulator / targetFrameTime;constrenderX = previousX * (1-alpha) + currentX *alpha;

这样即便逻辑固定 60Hz,也可以在 120Hz 屏幕上获得更顺滑的视觉运动。

十一、总结 ✨

一个可靠的 HarmonyOS 游戏循环至少要做到:

  • 优先跟随DisplaySync,并准备定时器降级路径;
  • 使用真实经过时间驱动累加器;
  • 用固定deltaTime更新世界;
  • 对超长帧和补算次数设置上限;
  • 明确前后台、暂停、重启时的计时重置;
  • 让更新和渲染保持解耦;
  • 结合对象池、批绘制和降频 AI 才能真正冲击高帧率。

高刷新率只是显示能力,稳定的时间模型才是游戏手感和可复现性的基础。🚀


推荐标签:HarmonyOSOpenHarmonyDisplaySync游戏循环Canvas性能优化

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

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

立即咨询