📱 鸿蒙 HarmonyOS 全栈专栏 · 第 35 篇 / 共 39 篇(第 5 季·收官季)
上一篇:第 34 篇 上架级项目 | 下一篇:PC 端适配:断点、分栏与键鼠交互

目录

一、选型:三条路的判断标准

二、Canvas 基础:从画一个圆到动起来

三、游戏循环:requestAnimationFrame 与 deltaTime

四、动画取舍:animateTo 还是逐帧画

五、渲染管线与掉帧排查

五点五、XComponent:性能上限更高的一条路

六、要上引擎时的三个注意点

七、六个坑与排错表


 

接第 34 篇结尾的投票,这一篇是得票方向之一:游戏与图形渲染。先说结论帮你省时间:大多数人想做的「游戏感」,其实到不了游戏引擎那一层 —— 一张 Canvas、一个循环、三招性能优化就够了。这一篇把这条路走通。

图 1:做图形的三条路 —— 选错路径,后面每一帧都在还债

📖 本篇导读

全文约 16 分钟,代码可直接跑。

  1. 选型:引擎、XComponent、Canvas 三条路怎么挑
  2. Canvas 基础:从静态绘制到能动起来
  3. 游戏循环:requestAnimationFrame 与 deltaTime
  4. 动画取舍:什么时候用 animateTo,什么时候逐帧画
  5. 渲染管线与掉帧排查:16.7ms 都花在哪了
  6. XComponent:性能上限更高的一条路
  7. 接入 Cocos / Unity 的注意点
  8. 六个坑与排错表

一、选型:三条路的判断标准

上一张图说了结论,这里展开一点。判断标准只有一个:你每一帧要画多少东西、容忍多大的性能开销。

场景推荐路径理由
图表、进度环、小游戏元素(几十个)Canvas / 系统动画实现最快,性能完全够
自研 2D 游戏(几百上千个精灵)XComponent + C++绕过 UI 树直渲,性能上限高
重 3D / 复杂玩法Unity / Cocos 鸿蒙版物理、音频、资源管线现成
页面转场、属性动画animateTo / transition声明式动画交给系统做最省心
💡 不要一上来就装引擎。先画一张会动的图,理解渲染的直觉,再决定要不要上引擎 —— 很多时候你会发现需求根本不需要。

二、Canvas 基础:从画一个圆到动起来

ArkUI 的 Canvas 组件用法和 HTML5 Canvas 几乎一致:声明组件,在 onReady 回调里拿 CanvasRenderingContext2D,之后就是熟悉的 API。

Canvas 静态绘制

// 组件里声明画布
Canvas(this.ctx)
  .width('100%')
  .height('100%')
  .onReady(() => {
    const ctx = this.ctx as CanvasRenderingContext2D
    ctx.fillStyle = '#0A59F7'
    ctx.beginPath()
    ctx.arc(100, 100, 40, 0, Math.PI * 2)
    ctx.fill()
  })

几个要点:

① 坐标系以组件为原点,注意用 vp 单位换算;② 绘制发生在 onReady 之后,之前画不上去;③ Canvas 尺寸变化(比如窗口改变)不会保留内容,需要重画 —— 这一点在 PC 适配(下一篇)时会变成坑。

三、游戏循环:requestAnimationFrame 与 deltaTime

能画之后,第二步是「持续地画」。鸿蒙提供了 requestAnimationFrame,语义与 Web 相同:下一帧渲染前回调一次。

游戏循环

private lastTs: number = 0
private x: number = 50

frame(ts: number) {
  // deltaTime:这一帧距上一帧多少 ms
  const dt = this.lastTs ? ts - this.lastTs : 16.7
  this.lastTs = ts

  // 按时间移动而不是按帧移动:60Hz 和 90Hz 设备速度一致
  this.x += 0.2 * dt

  const ctx = this.ctx as CanvasRenderingContext2D
  ctx.clearRect(0, 0, ctx.width, ctx.height)   // 每帧清屏
  ctx.fillStyle = '#0A59F7'
  ctx.fillRect(this.x, 100, 40, 40)

  requestAnimationFrame(() => this.frame(Date.now()))
}

// 启动:首次 onReady 里调一次
requestAnimationFrame(() => this.frame(Date.now()))
⚠️ 最经典的错误: this.x += 1 这种「按帧移动」。设备刷新率不同(60 / 90 / 120Hz),游戏速度就不一样 —— 老玩家称之为「高刷赛车更快」。 一切运动按 deltaTime 换算。

再补一个实用例子 —— 用 Canvas 画进度环,这是最常被问到、也最能说明「Canvas 够用论」的场景:

进度环

// 进度环:一个属性 + 一次重绘,完全不需要引擎
private progress: number = 0

drawRing() {
  const ctx = this.ctx as CanvasRenderingContext2D
  ctx.clearRect(0, 0, ctx.width, ctx.height)
  ctx.lineWidth = 10
  ctx.strokeStyle = '#E8F0FF'
  ctx.beginPath()
  ctx.arc(120, 120, 80, 0, Math.PI * 2)      // 背景环
  ctx.stroke()
  ctx.strokeStyle = '#0A59F7'
  ctx.beginPath()
  ctx.arc(120, 120, 80, -Math.PI / 2,
          -Math.PI / 2 + this.progress * Math.PI * 2)   // 进度弧
  ctx.stroke()
}
💡 图标、图表、进度、装饰性动画 —— 这类「一个属性就能表达」的图形场景,Canvas 的性能余量大到可以忽略。真正需要掂量的只有两点:元素数量(几百个以上)和刷新频率(每帧全量重绘)。

四、动画取舍:animateTo 还是逐帧画

不是所有「动」都需要游戏循环。判断方法:这个动画是属性变化还是自由绘制?

情况用哪种原因
弹窗出现、卡片位移、透明度变化animateTo声明式,系统插值,掉帧也平滑
粒子、笔迹、跟手的拖尾效果Canvas 逐帧无法表达成属性变化
列表滚动加载进度环Canvas 或 Progress 组件一个属性就能表达,别上逐帧

属性动画

// 声明式动画:一行搞定,不要为它写循环
@State scale: number = 1

Button('开始')
  .scale({ x: this.scale, y: this.scale })
  .onClick(() => {
    animateTo({ duration: 300, curve: Curve.EaseOut }, () => {
      this.scale = 1.2
    })
  })
💡 经验法则: 能用 animateTo 表达的,绝不用逐帧画。逐帧循环是最后手段,因为它每帧都占主线程 —— 这就是下一节的主题。

五、渲染管线与掉帧排查

说性能之前,先看一帧是怎么画出来的:

图 2:一帧的旅程 —— 掉帧排查沿这条链从左往右找

屏幕一般 60Hz(高刷机 90/120Hz),意味着每一帧只有 16.7ms 的预算(90Hz 是 11.1ms)。你的状态变化、布局、绘制、合成全部挤在这段时间里,超了就是掉帧。

排查顺序(从最常见到最少见):

症状特征大概率原因怎么确认与修
卡顿不规律,滑动列表时明显主线程有长任务(同步 IO、大 JSON 解析)Profiler 看主线程长任务;挪到 TaskPool
每次进入某个页面卡一下首帧布局过深或一次性渲染太多组件拆 LazyForEach、控制嵌套层级
游戏循环持续微卡每帧创建大对象触发 GC循环外预分配数组,复用对象
动画整体发糊过度绘制或未用图层合成减少透明叠加层数

避免每帧分配

// 反面教材:每帧都新建对象
frame() {
  const points: number[][] = []       // 每帧 60 次 × 每次数百个内层数组
  for (let i = 0; i < 300; i++) {
    points.push([Math.random() * 100, Math.random() * 100])
  }
  // ...
}

// 正确:预分配 + 复用,只在数据变化时更新
private points: number[][] = Array.from({ length: 300 }, () => [0, 0])
frame() {
  for (let i = 0; i < 300; i++) {
    this.points[i][0] = Math.random() * 100   // 原地改,零分配
    this.points[i][1] = Math.random() * 100
  }
}
⚠️ Profiler 里看帧耗时的入口: DevEco Studio → Profiler → Frame。它能列出每一帧的耗时分布,超预算的帧直接标红 —— 别靠肉眼数动画卡不卡。

五点五、XComponent:性能上限更高的一条路

在 Canvas 和完整引擎之间,还有一个中间档:XComponent。它提供一个原生渲染表面,你可以用 C/C++ 直接往上面画(OpenGL ES / Vulkan),或者把第三方 C++ 图形库接进来。它的价值在于:绘制完全绕开 ArkUI 的组件树和 JS 桥,性能上限接近原生。

XComponent 声明

// 页面里声明一个原生渲染表面
XComponent({ id: 'game', type: 'surface',
             library: 'game_render' })   // 对应 C++ 侧的 NAPI 模块
  .width('100%')
  .height('100%')
  .onLoad((ctx) => {
    // ctx 传给 C++ 侧,之后每帧由 C++ 直接渲染
    nativeInit(ctx)
  })

什么时候选它?两个信号:① Canvas 已经优化到头(预分配、脏区、降频)仍然掉帧;② 你有现成的 C++ 渲染代码想复用。两者都不满足,就别碰它 —— Native 工具链的成本(见第 32 篇)会吃掉你几周时间。

调性能之前,先让帧率可见。一个 20 行的自制 FPS 计数器,比肉眼判断靠谱得多:

自制 FPS 计数器

private frames: number = 0
private lastMark: number = 0
@State fps: number = 0

frame(ts: number) {
  this.frames++
  if (ts - this.lastMark >= 1000) {          // 每秒结算一次
    this.fps = this.frames
    this.frames = 0
    this.lastMark = ts
  }
  // ...正常绘制...
  requestAnimationFrame(() => this.frame(Date.now()))
}

build() {
  Stack() {
    Canvas(this.ctx).width('100%').height('100%').onReady(...)
    Text(\`FPS: \${this.fps}\`)          // 挂在角落,打包前移除
      .fontSize(12).margin(8)
  }
}

优化动作的顺序建议:先加 FPS 显示拿到基线 → 做一个优化 → 对比数字。没有基线的优化都是玄学,你以为的「应该更快」经常测出来没变化甚至更慢。

六、要上引擎时的三个注意点

如果评估完确实需要引擎(Cocos Creator、Unity 鸿蒙版都有官方方案),从 2D Canvas 迁过去时注意:

① 性能模型完全不同:引擎走 XComponent + C++ 直渲,不再经过 ArkUI 的组件树,之前调 Canvas 的经验大部分用不上;② 打包产物按引擎的鸿蒙导出模板走,签名、上架流程与普通 App 一致(见第 10 篇);③ 引擎版本与 SDK 版本要匹配,升级 SDK 后先跑引擎的最小示例工程再迁业务。具体导出步骤以引擎官方文档为准。

七、六个坑与排错表

坑现象处理
onReady 前绘制画了个寂寞,控制台还不报错所有绘制放 onReady 之后
窗口尺寸变化后空白Canvas 内容没跟随重画监听尺寸变化,主动重画一帧
按帧移动高刷设备游戏变快统一用 deltaTime 换算
每帧 new 对象持续微卡,GC 间隔性停顿预分配、原地更新
画布坐标没换算 vp不同设备位置错位用 ctx.width / ctx.height 做相对坐标
该用 animateTo 的写了循环代码长、费电、掉帧属性动画交给系统

这一篇你该记住:选型只看每帧元素数量与掉帧容忍度;游戏循环必用 deltaTime;能用 animateTo 就不用逐帧;掉帧排查从主线程长任务开始,Profiler 的 Frame 页是依据。

下一篇是第二个投票方向:鸿蒙 PC 端适配 —— 你的应用怎么从「手机上能跑」变成「PC 上好用」。


如果这个专栏帮你把鸿蒙学明白了,点赞 + 收藏 + 关注 支持一下。
你还有哪个场景没跑通?评论区说清楚你的目标 + 现在怎么问的,我帮你改成能用的提示词。

Logo

作为“人工智能6S店”的官方数字引擎,为AI开发者与企业提供一个覆盖软硬件全栈、一站式门户。

更多推荐