【OpenHarmony/HarmonyOS】不用图片也能画坦克:ArkUI 与 Canvas 几何造型实战
【OpenHarmony/HarmonyOS】不用图片也能画坦克:ArkUI 与 Canvas 几何造型实战
游戏坦克不一定需要 PNG 精灵图。履带可以是圆角矩形,轮子可以是圆,车体可以是矩形,炮塔和炮管通过 Stack、位置与旋转就能组合出来。项目中实际上有两套“代码画坦克”:启动页用 ArkUI 声明式几何组件展示装饰坦克,战斗中用 Canvas 即时绘制可旋转、可后坐的实体。本篇对比两条路径,说明坐标空间、层级、镜像、旋转中心与高频性能如何取舍。🛡️
一、为什么同一个项目需要两种绘制方式
| 场景 | 技术 | 更新频率 | 主要要求 |
|---|---|---|---|
| 启动页两辆装饰坦克 | ArkUI Rect/Circle/Stack |
低 | 声明式布局、易组合动画 |
| 战斗中的玩家与 AI | Canvas 2D | 60~120 FPS | 高频旋转、平移、批量实体 |
启动页调用:
TankShape({
color: '#00E5FF',
scaleSize: 1.2,
facing: 'right'
});
TankShape({
color: '#FF4081',
scaleSize: 1.2,
facing: 'left'
});
战斗引擎则遍历 Tank/EnemyTank 并调用 draw(context)。两者外观语言接近,但不是同一渲染实现。不能修改 TankShape 后期待战斗坦克同步变化。
二、ArkUI TankShape 的坐标骨架
组件对外只有三项属性:
@Component
export struct TankShape {
@Prop color: string = '#00E5FF';
@Prop scaleSize: number = 1.0;
@Prop facing: string = 'right';
}
内部建立 100×80 的局部坐标空间,再放进 120×120 外容器:
外容器 120 × 120
┌────────────────────────┐
│ 局部坦克 100 × 80 │
│ 炮塔/炮管 z=2..4 │
│ 车体 z=3 │
│ 前履带 z=5 │
└────────────────────────┘
固定局部尺寸的好处是所有零件都能用明确坐标定位;对外通过整体 scale 调整大小,不需要为每个零件重复乘倍率。
三、用 zIndex 建立前后关系
坦克不是一组平铺图形,而是有遮挡关系:远侧履带在最底层,炮管部分位于车体后,车体覆盖履带,炮塔覆盖车体,近侧履带位于最前。
| 部件 | 主要图形 | 大致位置 | zIndex |
|---|---|---|---|
| 后履带 | 70×20 圆角 Rect | x=25, y=35 | 默认底层 |
| 炮管 | Row + 两个 Rect | x=60, y=15 | 2 |
| 车体 | 65×35 Rect | x=15, y=25 | 3 |
| 炮塔 | 圆角 Rect + Hatch | x=25, y=5 | 4 |
| 前履带 | Rect + 4 组轮子 | x=5, y=45 | 5 |
声明式片段示例:
Rect()
.width(65)
.height(35)
.fill(this.color)
.radius(6)
.stroke('#000000')
.strokeWidth(2)
.position({ x: 15, y: 25 })
.zIndex(3);
zIndex 让结构不依赖纯粹的声明顺序,读代码时也能看出层级意图。层级值不需要连续,只需相对关系明确。
四、履带如何用基本图形建立机械感
前履带先画外轮廓和内层,再用 ForEach 画四个轮组:
Stack() {
Rect()
.width(85)
.height(28)
.fill('#1a1a1a')
.radius(14)
.stroke('#000000');
Rect()
.width(75)
.height(18)
.fill('#111111')
.radius(9)
.position({ x: 5, y: 5 });
Row({ space: 3 }) {
ForEach([1, 2, 3, 4], () => {
Stack() {
Circle({ width: 14, height: 14 })
.fill('#333333');
Circle({ width: 8, height: 8 })
.fill(this.color);
}
});
}
.position({ x: 10, y: 7 });
}
外层、内槽、轮圈和同色轮毂形成四个明度层级,不需要纹理图片也能表达履带。圆角半径设为高度一半,使外轮廓近似胶囊形。
ForEach 的输入固定,item 没有参与绘制,只用于重复四次。更语义化的方式可以把轮子坐标算出来,但四项静态图形当前足够清楚。
五、炮管的旋转不是坦克朝向
ArkUI 装饰坦克的炮管固定上扬 -15°:
Row() {
Rect().width(6).height(14); // 炮口
Rect().width(40).height(12); // 炮管
}
.rotate({ angle: -15 })
.position({ x: 60, y: 15 })
.zIndex(2);
这是造型角度,不对应战斗瞄准方向。整个坦克的左右朝向通过镜像实现:
.scale({
x: this.facing === 'left' ? -1 : 1,
y: 1
})
镜像作用于整个 100×80 局部 Stack,所以车体、履带、炮口和上扬角一起翻转。相比逐项计算左右坐标,整体负 scale 更简单。
facing 当前是普通 string,任何拼写错误都会默认按右侧。更安全的是:
@Prop facing: 'left' | 'right' = 'right';
这样错误值在编译期就能发现。
六、整体缩放和布局占位不是一回事
外容器设置:
.width(120)
.height(120)
.scale({
x: this.scaleSize,
y: this.scaleSize
})
视觉 scale 通常不会同步改变父布局分配的占位尺寸。scaleSize=1.2 时图形可能绘制到 120×120 之外,但父布局仍按原尺寸排布,旁边元素距离过近时可能重叠。
如果尺寸参与布局,应通过显式宽高或外层约束计算;如果只是装饰浮动且周围留有空间,transform scale 更方便。启动页是装饰场景,所以当前策略可行,但需要在不同屏幕和字体缩放下截图检查。
七、颜色派生目前是有限映射
炮管使用较深颜色:
getDarkColor(hex: string): string {
if (hex === '#00E5FF') return '#00B8D4';
if (hex === '#FF4081') return '#C51162';
return hex;
}
它只对启动页两种已知颜色做映射,其他颜色直接返回原值。优点是结果可控,不需要颜色解析库;缺点是组件看似接受任意 color,实际明暗层次只对两种输入完善。
可以把 darkColor 作为显式 Prop,或建立主题配色对象:
interface TankPalette {
body: string;
bodyDark: string;
track: string;
outline: string;
highlight: string;
}
这比在运行时随意降低 RGB 更可控,也方便高对比度主题。
八、Canvas 坦克从中心原点开始
战斗坦克使用不同坐标思想:先把 Canvas 原点平移到坦克中心,再旋转整个局部坐标系。
draw(context: CanvasRenderingContext2D): void {
context.save();
context.translate(
this.position.x,
this.position.y
);
context.rotate(this.rotation + Math.PI / 2);
const size = this.size;
const halfSize = size / 2;
// 所有零件都围绕 (0,0) 绘制
context.restore();
}
ArkUI TankShape 使用左上角绝对位置,Canvas Tank 使用中心相对坐标。中心原点非常适合旋转,因为坦克不会绕左上角画大圆。
额外的 Math.PI / 2 用于协调“逻辑 rotation 的 0 指向右侧”和“造型默认炮管向上”之间的差异。坐标约定必须写清,否则移动方向、炮口方向和子弹方向容易相差 90°。
九、Canvas 中的履带、车体和炮塔
履带由左右矩形与循环条纹组成:
const trackWidth = size * 0.25;
const trackLength = size;
context.fillStyle = '#424242';
context.fillRect(
-halfSize,
-trackLength / 2,
trackWidth,
trackLength
);
context.fillRect(
halfSize - trackWidth,
-trackLength / 2,
trackWidth,
trackLength
);
context.fillStyle = '#212121';
for (let y = -trackLength / 2 + 4;
y < trackLength / 2;
y += 6) {
context.fillRect(-halfSize, y, trackWidth, 2);
context.fillRect(
halfSize - trackWidth, y, trackWidth, 2
);
}
车体宽度和长度都由 size 比例计算,因此更换 TANK_SIZE 后整体保持比例。炮塔用圆,炮管用矩形。相比 ArkUI 版本,Canvas 造型更简洁,因为同屏可能出现几十辆坦克,每帧都要重画。
十、后坐力:逻辑状态驱动局部变换 💥
开火时 Tank.fire() 把 recoilOffset 设为 5,update 再逐步恢复:
fire(): void {
this.recoilOffset = 5;
}
if (this.recoilOffset > 0) {
this.recoilOffset -= 20 * deltaTime;
if (this.recoilOffset < 0) {
this.recoilOffset = 0;
}
}
绘制炮管时使用嵌套 save/restore:
context.save();
context.translate(0, this.recoilOffset);
context.scale(
this.recoilOffset > 2 ? 1.2 : 1.0,
1.0
);
context.fillRect(
-barrelWidth / 2,
-barrelLength,
barrelWidth,
barrelLength
);
context.restore();
只有炮管坐标被移动和横向放大,车体与炮塔不受影响。嵌套 Canvas 状态能把局部动画限制在最小范围。
十一、HP 条为什么要在旋转恢复后绘制
坦克主体绘制完先 restore,再重新平移到坦克上方绘制 HP:
context.restore(); // 退出坦克旋转
context.save();
context.translate(
this.position.x,
this.position.y - this.size * 0.8
);
context.fillRect(-15, -10, 30, 4);
context.restore();
这样 HP 条始终水平,不会随坦克旋转。如果它属于前一个局部坐标,坦克朝左时血条也会竖起来或倒置。
这体现了渲染分层:实体本体使用世界平移 + 朝向旋转;头顶信息使用世界平移但不旋转;HUD 则完全退出摄像机变换。
十二、几何外观与碰撞体必须协调
坦克画成矩形与炮管组合,碰撞却主要用半径和九点采样近似。炮管伸出车体,但不参与墙体碰撞;否则炮管转动时会频繁卡墙。
| 部分 | 是否参与移动碰撞 | 原因 |
|---|---|---|
| 主车体/履带 | 是,用 radius/采样近似 | 决定能否通过走廊 |
| 炮管 | 否 | 避免旋转时卡墙,规则更易理解 |
| HP 条 | 否 | 纯 UI 信息 |
| 阴影/光效 | 否 | 纯视觉反馈 |
视觉体积与碰撞体不必完全相同,但不能差异大到让玩家感到“明明没碰却撞墙”或“半个车身穿墙”。调试时应叠加显示碰撞半径与采样点。
十三、什么时候选 ArkUI,什么时候选 Canvas
选择 ArkUI 几何组件
- 实体数量少;
- 需要参与布局、点击和无障碍;
- 动画频率不高;
- 希望用声明式条件组合;
- 用在主页、卡片、角色选择和装饰。
选择 Canvas
- 同屏实体多;
- 每帧平移、旋转;
- 需要摄像机统一变换;
- 绘制顺序由游戏引擎掌握;
- 希望减少声明式节点数量。
不要为了代码复用强行让战斗坦克变成几十个 ArkUI 组件,也不要为了“性能”把主页所有按钮和文本画进 Canvas。交互语义和更新频率才是选择依据。
十四、可维护性改进方向 ⚠️
- 把
facing从 string 收紧为联合类型; - 为 TankShape 增加稳定的
TankPalette,避免颜色暗化只支持两种值; - 明确 transform scale 不改变布局占位;
- 抽取共同的比例常量,但不要强求 ArkUI/Canvas 完全同构;
- 为 Canvas 的 save/restore 写配对测试或代码审查清单;
- 统一坦克朝向约定:0 弧度、局部前方和炮口方向;
- 性能测试使用真机帧时间,不以“基本图形肯定快”代替数据。
十五、测试与视觉检查 🧪
| 场景 | 检查点 |
|---|---|
| facing=left/right | 整车镜像,炮口方向正确 |
| scale=0.8/1.2 | 图形不被裁剪、不与旁边文本重叠 |
| 青/粉两色 | 车体与暗色炮管层级清晰 |
| 未知颜色 | 仍可读,不与履带混成一片 |
| rotation=0/π/2/π | Canvas 炮管与逻辑射击方向一致 |
| 连续开火 | 后坐力恢复,不出现负偏移 |
| Canvas 状态 | 画完坦克后其他实体不继承旋转 |
| 大量 AI | 记录绘制耗时和帧时间 |
| HP 条 | 始终水平且位于坦克上方 |
| 碰撞调试层 | 视觉车体与半径差异可接受 |
十六、总结 ✨
代码绘制坦克的价值不只是省下一张图片,而是让颜色、比例、朝向和动画都成为可计算参数。项目在启动页用 ArkUI Rect、Circle、Stack、zIndex 和负 scale 组合出细节丰富的装饰坦克;战斗中则把原点移到实体中心,用 Canvas 绘制更轻量的履带、车体、炮塔,并通过局部变换实现后坐力。
两套实现服务不同更新频率,不必强行合并。更重要的是理解坐标与状态边界:装饰造型使用布局坐标,战斗实体使用中心局部坐标;本体跟随旋转,HP 条退出旋转;视觉炮管不进入碰撞;整体 scale 不等于布局尺寸。掌握这些原则后,即使只使用最基础的几何图形,也能在 HarmonyOS 应用中构建清晰、可动画、可主题化的游戏角色。🚀
推荐标签: OpenHarmony HarmonyOS ArkTS ArkUI Canvas 几何绘制 游戏开发 组件设计

更多推荐


所有评论(0)