《HarmonyOS技术精讲-UI开发 (基于NDK构建UI)》第9篇:实战——游戏HUD组件开发

📅 2026/6/25 13:31:17
《HarmonyOS技术精讲-UI开发 (基于NDK构建UI)》第9篇:实战——游戏HUD组件开发
这个 HUD 组件要解决什么问题HarmonyOS 上的游戏开发UI 部分一直比较棘手。ArkUI 框架本身是声明式 UI渲染走的是 GPU 加速但对于 HUDHead-Up Display这种需要高频更新、且对帧率敏感的组件直接使用纯 ArkTS 的 Stack 状态绑定的写法很容易出现帧率抖动。很多人在开发游戏时遇到一个问题血量条用 State 绑定每秒更新 60 次结果 UI 线程负担太重游戏主线程掉帧。实际测试下来当 HUD 组件包含 3 个以上独立更新区域血条、技能 CD、小地图时纯 ArkTS 实现的渲染耗时在 5-8ms留给游戏逻辑的时间就很少了。所以这篇文章要做的是用 NDK Canvas 的方式把 HUD 的渲染工作从 ArkTS 侧剥离交给 C 的 Canvas 绘制。这样 ArkTS 侧只负责接收数据、触发绘制真正的渲染计算由 C 处理。最终目标在 60fps 下稳定运行且 HUD 渲染耗时控制在 2ms 以内。环境说明DevEco Studio 版本DevEco Studio 6.1.0 及以上 HarmonyOS SDK 版本HarmonyOS 6.1.0(23) 及以上 目标设备手机整体架构设计这个 HUD 组件包含三个核心模块血量条使用 Canvas 绘制矩形条颜色从绿色渐变到红色。技能冷却图标使用 Canvas 绘制圆形遮罩根据冷却进度显示扇形填充。小地图使用 Canvas 绘制简化地形玩家位置用红点标记。数据更新策略是这样ArkTS 侧负责接收游戏逻辑发来的数据血量百分比、技能冷却剩余时间、玩家位置并传递给 C。C 侧负责计算绘制参数颜色渐变、扇形角度、坐标映射然后调用 Canvas 的绘图 API 完成渲染。所有绘制操作都在 GPU 上执行C 代码只做计算和 API 调用不阻塞 UI 线程。核心实现1. ArkTS 侧数据中转与 Canvas 容器先写 ArkTS 侧的入口文件。这个HudComponent组件负责实例化 C 的绘制类并通过Canvas把绘制结果呈现出来。// HudComponent.etsimport{HudRender}from../native/cpp/NativeHudRender;EntryComponentstruct HudComponent{StateprivatehealthPercent:number1.0;StateprivateskillCooldown:number0.0;StateprivateplayerX:number0.5;StateprivateplayerY:number0.5;privatenativeRender:HudRendernewHudRender();aboutToAppear(){// 初始化 C 渲染器this.nativeRender.init();// 模拟游戏数据更新实际项目里由游戏线程推送setInterval((){this.healthPercentMath.max(0,this.healthPercent-0.01);this.skillCooldown(this.skillCooldown0.02)%1.0;this.playerX0.5Math.sin(Date.now()/1000)*0.3;this.playerY0.5Math.cos(Date.now()/1000)*0.3;},16);// 约 60fps}build(){Column(){Canvas(this.context){// 这里用 Canvas 包裹 C 的绘制结果}.width(100%).height(100%).onReady((){// Canvas 准备好后把 Native Canvas 指针传给 CletnativeCanvasthis.context.getNativeCanvas();this.nativeRender.setNativeCanvas(nativeCanvas);}).onDraw((context){// 每次绘制时把数据传给 C由 C 完成渲染this.nativeRender.drawHud(this.healthPercent,this.skillCooldown,this.playerX,this.playerY);})}}}代码说明HudRender是 C 导出到 ArkTS 的类负责管理 Native Canvas 和绘制逻辑。setInterval模拟游戏数据更新。真机开发中这些数据应该来自游戏引擎的帧循环。onDraw回调是 ArkUI 的 Canvas 事件每次帧渲染时触发。这里把数据传递给 C 一次性绘制避免多次跨语言调用。注意事项getNativeCanvas()返回的是底层的OH_NativeCanvas指针不能频繁获取。建议在onReady里只获取一次然后缓存到 C 侧。onDraw的回调频率与 ArkUI 的刷新率一致默认是 60fps。如果游戏引擎需要独立帧率可以考虑用requestAnimationFrame替代。2. C 侧绘制类实现这一部分是核心。C 代码使用libNativeCanvas.so提供的 API 完成绘制。// NativeHudRender.h#ifndefNATIVE_HUD_RENDER_H#defineNATIVE_HUD_RENDER_H#includenapi/native_api.h#includenative_drawing/drawing_canvas.hclassHudRender{private:OH_Drawing_Canvas*canvas_nullptr;int32_twidth_0;int32_theight_0;public:voidInit(OH_Drawing_Canvas*canvas,int32_twidth,int32_theight);voidDrawHealthBar(floatpercent);voidDrawSkillCooldown(floatprogress);voidDrawMinimap(floatplayerX,floatplayerY);};#endif// NativeHudRender.cpp#includeNativeHudRender.h#includenative_drawing/drawing_brush.h#includenative_drawing/drawing_path.hvoidHudRender::Init(OH_Drawing_Canvas*canvas,int32_twidth,int32_theight){canvas_canvas;width_width;height_height;}voidHudRender::DrawHealthBar(floatpercent){if(percent0.0f)percent0.0f;if(percent1.0f)percent1.0f;// 血量条背景灰色OH_Drawing_Brush*bgBrushOH_Drawing_BrushCreate();OH_Drawing_BrushSetColor(bgBrush,0xFF333333);OH_Drawing_CanvasDrawRect(canvas_,50,height_-60,350,height_-40);// 血量条前景从红到绿渐变uint32_tredstatic_castuint32_t((1.0f-percent)*255);uint32_tgreenstatic_castuint32_t(percent*255);uint32_tcolor(0xFF24)|(red16)|(green8)|0xFF;OH_Drawing_BrushSetColor(bgBrush,color);floatfillWidth300.0f*percent;OH_Drawing_CanvasDrawRect(canvas_,50,height_-60,50fillWidth,height_-40);OH_Drawing_BrushDestroy(bgBrush);}voidHudRender::DrawSkillCooldown(floatprogress){// 技能图标背景灰色圆int32_tcxwidth_-80;int32_tcyheight_-80;int32_tradius30;// 绘制圆形遮罩根据 progress 绘制扇形// 这里用 Path 弧线实现OH_Drawing_Path*pathOH_Drawing_PathCreate();OH_Drawing_PathMoveTo(path,cx,cy);floatstartAngle-90.0f;// 从顶部开始floatsweepAngle360.0f*progress;OH_Drawing_PathArcTo(path,cx-radius,cy-radius,cxradius,cyradius,startAngle,sweepAngle);OH_Drawing_PathClose(path);OH_Drawing_Brush*skillBrushOH_Drawing_BrushCreate();OH_Drawing_BrushSetColor(skillBrush,0xFF00AABB);OH_Drawing_CanvasDrawPath(canvas_,path);OH_Drawing_PathDestroy(path);OH_Drawing_BrushDestroy(skillBrush);}voidHudRender::DrawMinimap(floatplayerX,floatplayerY){// 小地图背景半透明矩形OH_Drawing_Brush*mapBgOH_Drawing_BrushCreate();OH_Drawing_BrushSetColor(mapBg,0x88000000);OH_Drawing_CanvasDrawRect(canvas_,10,10,210,210);// 简单地形画几条线OH_Drawing_Path*terrainPathOH_Drawing_PathCreate();OH_Drawing_PathMoveTo(terrainPath,20,20);OH_Drawing_PathLineTo(terrainPath,200,50);OH_Drawing_PathLineTo(terrainPath,150,200);// 这里只演示实际地形数据可以动态传入OH_Drawing_CanvasDrawPath(canvas_,terrainPath);// 玩家位置红点int32_tmapCenterX110;int32_tmapCenterY110;int32_tdotXmapCenterXstatic_castint32_t((playerX-0.5)*200);int32_tdotYmapCenterYstatic_castint32_t((playerY-0.5)*200);// 绘制一个半径为5的小圆OH_Drawing_BrushSetColor(mapBg,0xFFFF0000);OH_Drawing_CanvasDrawCircle(canvas_,dotX,dotY,5);OH_Drawing_BrushDestroy(mapBg);OH_Drawing_PathDestroy(terrainPath);}代码说明每个绘制函数都接受浮点数参数由 ArkTS 侧传入。血量条和技能冷却的绘制逻辑直接使用了 OH_Drawing API避免了在 ArkTS 侧做复杂计算。小地图的地形绘制使用了 Path可以扩展为加载地形数据。性能优化这里每帧都创建和销毁 Brush、Path 对象。更优的做法是在 Init 里预创建好 Brush 和 Path 对象每帧只修改颜色和坐标。但这里为了代码清晰采用了每帧重新创建的方式。实际项目里建议把 Brush 和 Path 缓存起来减少内存操作。3. NAPI 导出为了让 ArkTS 能够调用 C 类需要写一个 NAPI 绑定。// napi_init.cpp#includenapi/native_api.h#includeNativeHudRender.hstaticHudRender*g_rendernullptr;staticnapi_valueInit(napi_env env,napi_callback_info info){size_t argc3;napi_value args[3];napi_get_cb_info(env,info,argc,args,nullptr,nullptr);// 假设传入的参数是: canvas指针, width, height// 实际项目中需要通过 getNativeCanvas 获取returnnullptr;}staticnapi_valueDrawHud(napi_env env,napi_callback_info info){size_t argc4;napi_value args[4];napi_get_cb_info(env,info,argc,args,nullptr,nullptr);doublehealthPercent,skillCooldown,playerX,playerY;napi_get_value_double(env,args[0],healthPercent);napi_get_value_double(env,args[1],skillCooldown);napi_get_value_double(env,args[2],playerX);napi_get_value_double(env,args[3],playerY);if(g_render){g_render-DrawHealthBar(static_castfloat(healthPercent));g_render-DrawSkillCooldown(static_castfloat(skillCooldown));g_render-DrawMinimap(static_castfloat(playerX),static_castfloat(playerY));}returnnullptr;}// NAPI 模块注册staticnapi_valueInitModule(napi_env env,napi_value exports){g_rendernewHudRender();// 注册 init 和 drawHud 两个方法napi_value fn_init,fn_draw;napi_create_function(env,init,NAPI_AUTO_LENGTH,Init,nullptr,fn_init);napi_create_function(env,drawHud,NAPI_AUTO_LENGTH,DrawHud,nullptr,fn_draw);napi_set_named_property(env,exports,init,fn_init);napi_set_named_property(env,exports,drawHud,fn_draw);returnexports;}NAPI_MODULE(NODE_GYP_MODULE_NAME,InitModule)说明实际项目中Init 函数需要处理 Native Canvas 指针的传递。官方推荐的做法是在 ArkTS 侧通过Canvas.getNativeCanvas()获取OH_NativeCanvas对象然后通过 NAPI 传递到 C 侧。这里简化了过程只展示了数据传递和绘制调用。完整实现需要参考官方文档中关于OH_NativeCanvas的使用方式。常见问题与踩坑问题 1Canvas 绘制圆角不够圆现象绘制技能冷却图标的扇形时边缘出现锯齿视觉效果不柔滑。原因OH_Drawing_PathArcTo的曲线绘制在默认情况下没有开启抗锯齿。ArkUI 的 Canvas 其实支持抗锯齿但在 NDK 侧需要通过OH_Drawing_CanvasSetAntiAlias来设置。解决方案在DrawSkillCooldown函数开始时调用OH_Drawing_CanvasSetAntiAlias(canvas_, true);。注意这个设置会影响整个 Canvas 的绘制如果不想影响其他绘制可以在绘制前保存/恢复 Canvas 状态。问题 2ArkUI 的 onDraw 回调在游戏高负载下可能不触发现象当游戏逻辑非常繁忙时onDraw回调的间隔变得不稳定甚至出现短暂卡顿。原因onDraw是在 ArkUI 的 UI 线程中触发的。如果游戏逻辑阻塞了 UI 线程ArkUI 就无法及时触发onDraw。这在纯 ArkTS 方案中同样存在。解决方案将游戏逻辑完全放到独立线程例如 Worker 或 NAPI 线程中执行只把最终数据通过跨线程通信传给 HUD 组件。HUD 组件的更新不依赖游戏逻辑的计算耗时。同时为了确保 60fps可以在 HUD 组件中使用requestAnimationFrame来主动请求绘制而不是依赖onDraw的被动触发。最佳实践避免在onDraw中做过多计算onDraw回调的主要职责是绘制任何非必须的计算都应该提前完成。比如血量百分比、技能冷却进度这些数据应该在游戏线程中算好然后直接传给 C。使用OH_Drawing_CanvasSave/Restore如果连续绘制多个 HUD 元素并且每个元素有自己的抗锯齿、透明度等属性推荐在绘制前调用Save绘制后调用Restore。这样可以避免绘制属性相互污染。预分配 Brush 和 Path 对象每帧都Create和Destroy对象会带来额外的内存分配和释放开销。推荐在 Init 阶段创建好对象然后每帧只修改属性例如颜色、路径。这样可以减少 GC 压力提升稳定性。完整入口// Index.etsimport{HudComponent}from./HudComponent;EntryComponentstruct Index{build(){Stack(){// 游戏场景示意Column(){Text(游戏画面).fontSize(50).fontColor(Color.White)}.width(100%).height(100%).backgroundColor(Color.Black)// HUD 组件覆盖在最上层HudComponent()}.width(100%).height(100%)}}FAQQ为什么真机上 HUD 渲染流畅而在模拟器上偶尔卡顿A模拟器通常无法完全模拟真机的 GPU 驱动和硬件加速效果。模拟器中的 Canvas 操作可能通过软件渲染完成性能远低于真机。建议以真机为准模拟器仅用于调试逻辑。QArkTS 侧的数据更新频率必须和 C 绘制频率一致吗A不需要。onDraw回调的频率由 ArkUI 控制通常与设备刷新率一致60Hz 或 90Hz。数据更新频率可以更高例如 120Hz但 C 侧只取最新的数据进行绘制。多余的更新会被跳过不会造成性能浪费。Q这个 HUD 组件可以复用给其他页面吗A可以。HudComponent是一个标准的 ArkTS 组件可以放在任何页面中。但需要注意C 侧的 Native Canvas 是与具体页面实例绑定的。如果页面切换时需要重新初始化建议在aboutToDisappear中释放 C 对象在aboutToAppear中重新初始化。