前言
棋盘背景网格画完,接下来要渲猫咪棋子——这是游戏页的主角。在 ArkUI 里,棋子是「Emoji Text + 圆形背景色块 + 绝对定位」三层组合:Emoji 显字形、Circle 背景显等级色、position 摆坐标。这三种技术在前几篇都拆过,本篇要把它们合成一个完整棋子渲染流程。
本篇以「猫猫大作战」游戏页猫咪层为锚点,把 ForEach 遍历猫咪数组、Emoji 字符到等级映射、圆形背景色、绝对定位摆放四大要点讲透。读完本篇你将能独立写出:Emoji 棋子层、等级色圆背景、绝对定位棋盘三种实战套路。
提示:本系列不讲 ArkTS 基础语法与环境搭建,假设你已跟完第 1–14 篇。
一、场景拆解:猫咪层渲染
打开entry/src/main/ets/pages/Index.ets的GameView第 2 层:
// 来源:entry/src/main/ets/pages/Index.ets GameView() 第 2 层 Column() { ForEach(this.cats, (cat: Cat) => { Stack() { Circle({ width: 30, height: 30 }).fillColor(this.getCatColor(cat.level)) Text(this.getCatEmoji(cat.level)).fontSize(20) } .alignContent(Alignment.Center) .position({ x: `${cat.x * 100 / COLS}%`, y: `${cat.y * 100 / ROWS}%` }) }, (cat: Cat) => `cat-${cat.id}`) } .width('100%').height('100%').zIndex(1)猫咪层规格拆解:
| 元素 | 类型 | 作用 |
|---|---|---|
ForEach | 循环渲染 | 遍历this.cats数组 |
Stack | 叠层容器 | 圆背景 + Emoji 叠 |
Circle | 圆形矢量 | 等级色背景(30vp) |
Text | Emoji 字符 | 猫咪字形(20vp) |
position | 绝对定位 | 摆到棋盘坐标 |
| 密钥 | cat-${cat.id} | 唯一追踪 |
关键经验:圆 30 比 Emoji 20 大——圆作背景框,Emoji 不顶满留点边气。
二、Cat 数据结构
打开entry/src/main/ets/components/GameTypes.ets:
export interface Cat { id: number // 唯一标识 x: number // 棋盘列坐标 (0-COLS) y: number // 棋盘行坐标 (0-ROWS) level: CatLevel // 猫咪等级 (1-6) }2.1 interface 类型契约
ArkTS 严格模式禁any,禁对象字面量动态加属性——必须用interface或class显式声明字段:
// ✅ 正确:interface 显式契约 export interface Cat { id: number x: number y: number level: CatLevel } // ❌ 错误:ArkTS 禁动态对象 const cat: any = { id: 1, x: 0, y: 0 } cat.level = CatLevel.LEVEL_1 // 动态加属性,编译报错!2.2 CatLevel enum
export enum CatLevel { LEVEL_1, // 0 LEVEL_2, // 1 LEVEL_3, // 2 LEVEL_4, // 3 LEVEL_5, // 4 LEVEL_6 // 5 }枚举值默认从 0 递增——LEVEL_1= 0、LEVEL_6= 5。本系列第 114 篇会专讲 enum。
提示:枚举名
LEVEL_1实际值是 0,不是 1。如果想值从 1 开始,写LEVEL_1 = 1, LEVEL_2 = 2, ...。本项目用默认 0 起,所以渲染时要cat.level + 1才显示「1 级」。
三、Emoji 字符到等级映射
// 来源:entry/src/main/ets/pages/Index.ets getCatEmoji(level: CatLevel): string { switch (level) { case CatLevel.LEVEL_1: return '🐱' case CatLevel.LEVEL_2: return '😺' case CatLevel.LEVEL_3: return '😸' case CatLevel.LEVEL_4: return '😻' case CatLevel.LEVEL_5: return '🐼' case CatLevel.LEVEL_6: return '🐯' default: return '🐱' } }3.1 Emoji 映射表的两种写法
// 写法 1:switch(本项目) getCatEmoji(level: CatLevel): string { switch (level) { case CatLevel.LEVEL_1: return '🐱' case CatLevel.LEVEL_2: return '😺' // ... default: return '🐱' } } // 写法 2:Record 映射表 const CAT_EMOJI: Record<number, string> = { [CatLevel.LEVEL_1]: '🐱', [CatLevel.LEVEL_2]: '😺', [CatLevel.LEVEL_3]: '😸', [CatLevel.LEVEL_4]: '😻', [CatLevel.LEVEL_5]: '🐼', [CatLevel.LEVEL_6]: '🐯' } getCatEmoji(level: CatLevel): string { return CAT_EMOJI[level] ?? '🐱' }实战经验:Record 映射表 O(1) 查询比 switch O(n) 快,6 级以上必用 Record。本系列第 115 篇会专讲 Record。
3.2 Emoji 渲染踩坑
- 预览器可能缺字形:DevEco 预览器某些 Emoji 不显示,真机才完整。
- fontColor 对彩色 Emoji 无效:Emoji 自带色表,
.fontColor(Color.Red)只影响单色 Emoji(如✨)。 - 字号比汉字大 10–20%:同字号 Emoji 视觉比汉字小一圈,要调大才平衡。本项目 Emoji 20 配 Circle 30,视觉才稳。
四、圆形背景色
getCatColor(level: CatLevel): string { switch (level) { case CatLevel.LEVEL_1: return '#FFC107' // 黄 case CatLevel.LEVEL_2: return '#FF9800' // 橙 case CatLevel.LEVEL_3: return '#E74C3C' // 红 case CatLevel.LEVEL_4: return '#9B59B6' // 紫 case CatLevel.LEVEL_5: return '#2ECC71' // 绿 case CatLevel.LEVEL_6: return '#3498DB' // 蓝 default: return '#FFC107' } }4.1 圆背景 + Emoji 叠层结构
Stack() { Circle({ width: 30, height: 30 }).fillColor(this.getCatColor(cat.level)) Text(this.getCatEmoji(cat.level)).fontSize(20) } .alignContent(Alignment.Center)三层叠:
| 层 | 元素 | 尺寸 | 作用 |
|---|---|---|---|
| 底 | Circle | 30vp | 等级色背景圆 |
| 上 | TextEmoji | 20vp | 猫咪字形 |
| 容器 | Stack | auto | 叠层居中 |
Stack的alignContent(Alignment.Center)让 Emoji 居中叠在 Circle 上。
4.2 圆尺寸 vs Emoji 尺寸比例
| 比例 | 视觉 | 适用 |
|---|---|---|
| 圆 30 + Emoji 20(1.5:1) | 圆作背景框,Emoji 不顶满 | 本项目 |
| 圆 24 + Emoji 24(1:1) | Emoji 顶满圆,无背景感 | 纯 Emoji 棋子 |
| 圆 40 + Emoji 24(1.67:1) | 圆大留白多,Emoji 小 | 低视觉权重 |
实战经验:1.5:1 是甜区——圆显色,Emoji 显形,两者各司其职。
五、ForEach 遍历猫咪数组
ForEach(this.cats, (cat: Cat) => { Stack() { /* ... */ } .position({ x: `${cat.x * 100 / COLS}%`, y: `${cat.y * 100 / ROWS}%` }) }, (cat: Cat) => `cat-${cat.id}`)5.1 keyGenerator 用 cat.id
(cat: Cat) => `cat-${cat.id}` // 密钥 'cat-1'/'cat-2'/...关键:密钥用cat.id而非cat.x或cat.y——因为猫咪会移动(下落、合并),x/y会变,但id不变。用 id 做密钥才能追踪同一只猫的移动,而非每次坐标变都重建。
5.2 数组增删触发 ForEach 重渲
this.cats.push(newCat) // 添加 → ForEach 新增一项渲染 this.cats.splice(idx, 1) // 删除 → ForEach 移除一项渲染 this.cats = [...this.cats] // 整体替换 → ForEach 全 diffArkUI 监视机制:@State cats: Cat[]被重新赋值或增删时,ForEach 用密钥做 diff——新增 id 渲染新项,删除 id 销毁旧项,其余复用。
5.3 数组项内部属性变化
this.cats[0].y += 1 // 移动第 0 只猫注意:直接改cats[0].y不会触发 ForEach 重渲——因为@State浅观察,只监听数组引用变化和增删,不监听项内部属性。要触发重渲有两种方式:
// 方式 1:重新赋值整个数组(强制 diff) this.cats = [...this.cats] // 方式 2:用 @Observed + @ObjectLink 深观察(本系列第 43 篇会讲)本项目用方式 1——主循环里this.cats = [...this.cats]强制 diff,ForEach 用密钥追踪移动。
六、绝对定位摆到棋盘坐标
.position({ x: `${cat.x * 100 / COLS}%`, y: `${cat.y * 100 / ROWS}%` })6.1 百分比坐标响应式
`${cat.x * 100 / COLS}%` // cat.x=2, COLS=8 → '25%'猫咪在第 2 列,定位到棋盘 25% 处——无论屏宽多少,都落在第 3 列。百分比坐标是响应式棋盘的甜区。
6.2 position 基准是父容器左上角
Column() { ForEach(this.cats, (cat: Cat) => { Stack() { /* ... */ } .position({ x: '...', y: '...' }) }, ...) } .width('100%').height('100%') // 父容器齔满棋盘关键:position的基准是父容器左上角。父Column必须width('100%').height('100%')齔满棋盘,猫咪的百分比定位才正确。
6.3 vs 固定 vp 坐标
// 方式 1:百分比(响应式,本项目) .position({ x: `${cat.x * 100 / COLS}%`, y: `${cat.y * 100 / ROWS}%` }) // 方式 2:固定 vp(不响应屏宽) const CELL_SIZE = 48 // 每格 48vp .position({ x: cat.x * CELL_SIZE, y: cat.y * CELL_SIZE })百分比优势:屏宽变化棋盘自动缩放,猫咪跟随。固定 vp 优势:格子大小一致,棋盘居中不齔屏时用。
七、完整代码:猫咪层渲染
@Entry @Component struct Index { @State cats: Cat[] = [] // ... 其他 state getCatEmoji(level: CatLevel): string { switch (level) { case CatLevel.LEVEL_1: return '🐱' case CatLevel.LEVEL_2: return '😺' case CatLevel.LEVEL_3: return '😸' case CatLevel.LEVEL_4: return '😻' case CatLevel.LEVEL_5: return '🐼' case CatLevel.LEVEL_6: return '🐯' default: return '🐱' } } getCatColor(level: CatLevel): string { switch (level) { case CatLevel.LEVEL_1: return '#FFC107' case CatLevel.LEVEL_2: return '#FF9800' case CatLevel.LEVEL_3: return '#E74C3C' case CatLevel.LEVEL_4: return '#9B59B6' case CatLevel.LEVEL_5: return '#2ECC71' case CatLevel.LEVEL_6: return '#3498DB' default: return '#FFC107' } } @Builder GameView() { Stack() { // 第 1 层:棋盘背景(第 13 篇讲) Column() { /* ... */ }.width('100%').height('100%').zIndex(0) // 第 2 层:猫咪层 Column() { ForEach(this.cats, (cat: Cat) => { Stack() { Circle({ width: 30, height: 30 }).fillColor(this.getCatColor(cat.level)) Text(this.getCatEmoji(cat.level)).fontSize(20) } .alignContent(Alignment.Center) .position({ x: `${cat.x * 100 / COLS}%`, y: `${cat.y * 100 / ROWS}%` }) }, (cat: Cat) => `cat-${cat.id}`) } .width('100%').height('100%').zIndex(1) // 第 3 层:顶部 HUD(第 11 篇讲) GameHUD().zIndex(2) } .width('100%').height('100%') .alignContent(Alignment.Top) } }八、调试技巧:猫咪渲染怎么调
console.info打坐标:Builder 里加console.info('cat', cat.id, cat.x, cat.y),追每只猫定位。- 密钥冲突排查:猫少了?查
cat.id是否唯一,密钥cat-${id}是否冲突。 - 临时加 border:给 Stack 加
.border({ width: 1, color: Color.Red }),看清猫占框。 - 真机看 Emoji:预览器某些 Emoji 缺字形,以真机为准。
九、性能与最佳实践
- 密钥用 id 非坐标:坐标会变,用坐标做密钥每次移动都重建。id 不变,追踪移动复用。
- 圆背景 + Emoji 叠层:比纯 Emoji 棋子多一层视觉信息(等级色),且 Circle 矢量省资源。
- 百分比定位配百分比格子:棋盘格和猫都用百分比,屏宽变化整体缩放一致。
- 数组项内部改要重新赋值:
this.cats[0].y += 1不触发重渲,要this.cats = [...this.cats]强制 diff。
总结
本篇我们从猫咪层渲染切入,掌握Catinterface 数据契约、getCatEmoji/getCatColor等级映射函数、Stack叠圆背景 + Emoji、ForEach用 id 做密钥追踪移动、position百分比绝对定位五大要点,并给出了猫咪层完整代码。核心要点:interface 显式契约禁 any,密钥用 id 非坐标追踪移动,圆 30 配 Emoji 20 是 1.5:1 甜区,百分比定位响应式。
下一篇我们将继续游戏页,拆解绝对定位的 position 坐标摆放细节。
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- 「猫猫大作战」项目源码:本仓库
entry/src/main/ets/pages/Index.ets、entry/src/main/ets/components/GameTypes.ets - ForEach 循环渲染官方指南
- Shape 容器官方指南
- Stack API 参考
- 状态管理概述 — HarmonyOS 官方文档
- 开源鸿蒙跨平台社区
- HarmonyOS 开发者官方文档首页
- 系列索引:本仓库
articles/INDEX.md