【OpenHarmony/HarmonyOS】ArkUI Canvas 2D 游戏引擎实战:世界坐标、摄像机、HUD 与渲染管线

发布时间:2026/7/23 1:58:20
【OpenHarmony/HarmonyOS】ArkUI Canvas 2D 游戏引擎实战:世界坐标、摄像机、HUD 与渲染管线 【OpenHarmony/HarmonyOS】ArkUI Canvas 2D 游戏引擎实战世界坐标、摄像机、HUD 与渲染管线Canvas 不只是“画几条线”的组件。只要建立坐标系、渲染顺序、摄像机和实体管理它完全可以承载一款中小型 2D 游戏。本文结合迷宫坦克项目讲清楚 HarmonyOS 中 Canvas 游戏场景的组织方式。️一、为什么战场选择 Canvas迷宫坦克游戏每一帧都可能发生这些变化坦克位置改变、炮塔旋转、数十颗子弹移动、粒子衰减、晶石浮动、摄像机跟随、局部迷宫进入或离开视口。若把这些对象全部映射为 ArkUI 组件声明式状态同步会带来大量节点属性更新。Canvas 采用立即绘制模式。开发者维护世界数据然后在每一帧按照固定顺序重新绘制。它牺牲了组件级布局和事件能力却换来了更直接的渲染控制适合高频、批量、几何化的游戏内容。项目仍然保留 ArkUI 作为外层容器这样按钮、菜单和弹窗可以继续使用原生组件。最终形成“双层 UI”Stack ├──Canvas迷宫、坦克、子弹、粒子、传送门、小地图 └── ArkUI HUD计时、晶石、摇杆、射击、暂停、结算二、先建立三套坐标概念Canvas 游戏最容易混乱的不是绘图 API而是坐标。1. 网格坐标迷宫使用二维数组保存一个元素代表一个格子static readonly TILE_EMPTY0;static readonly TILE_WALL1;static readonly TILE_BREAKABLE2;访问maze[row][column]时使用网格坐标。它适合迷宫生成、A* 寻路以及墙体类型判断。2. 世界坐标坦克和子弹不能只能停留在整数格子它们的位置使用像素世界坐标。二者通过格子尺寸转换constworldX column * GameConstants.MAZE_CELL_SIZE GameConstants.MAZE_CELL_SIZE /2;constworldY row * GameConstants.MAZE_CELL_SIZE GameConstants.MAZE_CELL_SIZE /2;反向转换使用Math.floor(worldX / cellSize)。这种设计让寻路仍在离散网格上进行而移动和碰撞可以保持连续。3. 屏幕坐标屏幕坐标是用户最终看到的位置。项目不逐个修改实体而是通过 Canvas 变换一次性把世界平移到屏幕this.context.save();constcamX Math.floor(this.cameraPosition.x);constcamY Math.floor(this.cameraPosition.y);this.context.translate(-camX, -camY);this.drawMazeWalls();this.playerTank?.draw(this.context);this.drawBulletsBatched();this.context.restore();任何世界对象仍然以世界坐标绘制摄像机只影响 Canvas 坐标系。HUD 在restore()之后绘制因此不会跟着地图移动。三、摄像机如何平滑跟随玩家 直接把摄像机设置为“玩家坐标减屏幕中心”会产生生硬的锁定感。项目使用一阶平滑逼近this.targetCameraPosition.x this.playerTank.position.x -this.screenWidth /2;this.targetCameraPosition.y this.playerTank.position.y -this.screenHeight /2;constdx this.targetCameraPosition.x -this.cameraPosition.x;constdy this.targetCameraPosition.y -this.cameraPosition.y;constdistance Math.sqrt(dx * dx dy * dy);if(distance 1.0) {this.cameraPosition.copy(this.targetCameraPosition); }else{this.cameraPosition.x dx *5* deltaTime;this.cameraPosition.y dy *5* deltaTime; }乘上deltaTime后跟随速度与实际帧率解耦。距离小于 1 像素时直接吸附目标防止浮点误差让镜头在终点附近持续微抖。绘制时再对相机坐标取整可以让像素风墙体保持清晰。更完善的相机还应加入地图边界约束避免小地图边缘露出大片空白受到爆炸时的短时震屏速度前瞻让玩家看到移动方向前方更多区域折叠屏不同显示区域下的动态中心点。四、渲染管线为什么必须固定顺序立即绘制没有自动层级后画的内容会覆盖先画的内容。项目的render()按如下顺序执行privaterender() {this.drawBackground();this.context.save();this.context.translate(-camX, -camY);this.drawTerrain();this.drawMazeWalls();this.portals.forEach(portal portal.draw(this.context));this.drawCrystalsBatched();this.powerUps.forEach(item item.draw(this.context));this.playerTank?.draw(this.context);this.enemies.forEach(enemy enemy.draw(this.context));this.drawBulletsBatched();this.drawParticlesOptimized();this.context.restore();this.drawMiniMap(); }这个顺序表达了明确的视觉关系背景在最底层墙体覆盖地面传送门和收藏物在角色下方子弹与粒子在角色上方小地图始终处于屏幕 HUD 层。每个会执行translate、rotate、clip、globalAlpha的绘制块都应配对save/restore。尤其小地图使用圆形裁剪如果忘记恢复上下文后续整张画布都可能被裁成一个圆。五、纯几何绘制坦克项目没有在战斗场景依赖外部精灵图而是用矩形、圆和变换组合坦克。关键步骤是先移动到坦克中心再旋转局部坐标系context.save();context.translate(this.position.x, this.position.y);context.rotate(this.rotation Math.PI /2);context.fillStyle #424242;context.fillRect(-halfSize, -trackLength /2, trackWidth, trackLength);context.fillRect(halfSize - trackWidth, -trackLength /2, trackWidth, trackLength);context.fillStyle this.color;context.fillRect(-bodyWidth /2, -bodyLength /2, bodyWidth, bodyLength);context.beginPath();context.arc(0,0, turretRadius,0, Math.PI *2);context.fill();context.restore();这种方式的优点是体积小、缩放自然、换色方便缺点是复杂美术效果的开发成本较高。适合原型、像素风、简洁几何风格也适合作为图片资源加载失败时的降级方案。六、迷宫只绘制可见区域 当关卡随波次扩大后完整遍历整张地图会浪费大量时间。项目先根据摄像机计算可见网格范围constminGridX Math.max(0, Math.floor(this.cameraPosition.x / cellSize) -1);constminGridY Math.max(0, Math.floor(this.cameraPosition.y / cellSize) -1);constmaxGridX Math.min(this.maze[0].length, Math.ceil((this.cameraPosition.x this.screenWidth) / cellSize) 1);constmaxGridY Math.min(this.maze.length, Math.ceil((this.cameraPosition.y this.screenHeight) / cellSize) 1);随后只遍历这个矩形。额外增加 1 格边距是为了避免相机位于半格位置时墙体在边缘闪烁。同类型墙体还被合并到一个 Path 中this.context.fillStyle #37474F;this.context.beginPath();for(let y minGridY; y maxGridY; y) {for(let x minGridX; x maxGridX; x) {if(this.maze[y][x] GameConstants.TILE_WALL) {this.context.rect(x * cellSize, y * cellSize, cellSize 0.5, cellSize 0.5); } } }this.context.fill();相比每个墙格调用一次fillRect并反复切换样式批量路径显著减少状态切换和绘制提交。可破坏墙使用第二个批次绘制整个迷宫主体只需少量fill()。七、子弹和晶石也可以批量绘制子弹按颜色分组再把同色圆形加入一个路径const batches newMapstring, Bullet[]();for(const bullet of this.bullets) {if(!bullet.isDead) { const list batches.get(bullet.color) ?? []; list.push(bullet); batches.set(bullet.color, list); } } batches.forEach((list,color) { this.context.fillStyle color; this.context.beginPath();for(const bullet of list) { this.context.moveTo( bullet.position.x bullet.radius, bullet.position.y ); this.context.arc(bullet.position.x, bullet.position.y, bullet.radius,0, Math.PI *2); } this.context.fill(); });这里在每个圆之前moveTo很重要否则 Canvas 可能把连续子路径之间用直线连接。晶石同样用一条 Path 批量构建菱形。八、小地图其实是第二台摄像机小地图不是把主画面缩小截图而是重新定义一套摄像机在屏幕左上角建立圆形裁剪区域以玩家世界坐标作为焦点按固定可见半径计算缩放比只绘制小地图范围内的墙体用绿色点、红色点表示玩家和敌人对范围外敌人沿方向向量夹到圆周附近。核心变换为this.context.translate(miniMapCenterX, miniMapCenterY); this.context.scale(scale,scale); this.context.translate(-focusX, -focusY);这与主摄像机完全同构只是增加了缩放与裁剪。理解这一点后实现观察者视角、分屏或回放缩略图都会容易很多。九、Canvas 与 ArkUI 的触摸冲突如何处理游戏 HUD 中大量区域是透明的。若不设置命中策略一个全屏透明容器可能吞掉所有触摸。项目对纯视觉层使用.hitTestBehavior(HitTestMode.None)对需要让子组件响应、同时不阻断其他区域的容器使用.hitTestBehavior(HitTestMode.Transparent)虚拟摇杆只占左半屏射击按钮位于右下角。这样可以支持双指同时移动和射击而不会让 Canvas 自身参与事件分发。十、屏幕尺寸变化时应该做什么Canvas 尺寸改变与游戏世界重新生成是两件事。首次进入战斗时项目使用真实宽高计算格子尺寸并生成地图后续尺寸变化只通知引擎更新屏幕尺寸避免每次布局变化都清空当前关卡。if(this.isGameInitialized) {this.gameEngine.updateScreenSize(newWidth,newHeight); }else{this.gameEngine.initGame(newWidth,newHeight, mode, difficulty);this.isGameInitialized true; }工程中用GameConstants.updateScale()让坦克尺寸、速度、子弹半径与格子尺寸同步缩放保证不同分辨率下“每秒跨越多少格”的体验大致一致而不是单纯放大视觉。十一、常见问题与优化清单 ️1. 画面越来越透明或被裁掉检查所有globalAlpha、clip、translate是否在finally或确定路径中恢复。2. 像素墙出现缝隙取整摄像机坐标并让相邻格子的绘制宽高增加约0.5px避免浮点缩放带来的采样缝隙。3. 大地图掉帧先做视口裁剪再做批量绘制不要在循环内部频繁修改fillStyle、shadowBlur和字体。4. 粒子很多时频繁 GC使用对象池并优先原地修改position.x/y避免每帧创建大量临时向量。5. HUD 跟着地图移动确保世界绘制结束后restore()HUD 只能在未应用摄像机变换的屏幕坐标中绘制。十二、总结一个可维护的 HarmonyOS Canvas 游戏引擎关键不在于绘制 API 数量而在于建立稳定的结构网格坐标表达地图规则世界坐标表达连续实体运动屏幕坐标与摄像机负责最终观察固定渲染顺序表达视觉层级可见区域裁剪与批处理控制开销ArkUI 继续承担 HUD 和原生交互。掌握这套坐标和管线后Canvas 就从一块“画布”变成了真正可扩展的 2D 世界。推荐标签HarmonyOSOpenHarmonyArkUICanvas2D游戏ArkTS