从零到一:基于HarmonyOS ArkUI开发经典“别踩白块“游戏的完整技术实践
本文详细介绍了基于HarmonyOS ArkUI框架开发"别踩白块"游戏的技术实践。文章从项目背景出发,分析了游戏规则与用户体验目标,阐述了选择ArkUI声明式编程和Stage模型的技术决策。深入解析了HarmonyOS系统架构、ArkUI声明式编程模型、Stage应用架构和ETS语言特性等核心技术。通过项目目录结构和关键配置文件的说明,展示了完整的开发框架。该项目不仅实现了经典游戏的核心玩法,还
从零到一:基于HarmonyOS ArkUI开发经典"别踩白块"游戏的完整技术实践
##项目演示


一、项目概述与背景
在移动应用开发领域,休闲小游戏一直是用户获取即时满足感和碎片化娱乐的重要载体。其中,“别踩白块”(Don’t Tap The White Tile)作为一款曾经风靡全球的极简风格游戏,以其极低的学习门槛、极高的反应速度要求和无限的可玩性,成为了移动游戏史上的经典案例。本文将详细记录如何使用华为HarmonyOS的ArkUI框架,从零开始构建一个完整的"别踩白块"游戏应用,深入剖析其中的技术选型、架构设计、核心实现以及性能优化策略。
1.1 项目价值
在当今移动开发环境中,跨平台框架如React Native、Flutter等占据了一定的市场份额,但原生框架在性能和系统集成方面仍然具有不可替代的优势。HarmonyOS作为华为推出的分布式操作系统,其ArkUI声明式UI框架为开发者提供了一种全新的开发范式。通过这个简单游戏项目的实践,我们可以深入理解:
- ArkUI声明式编程范式:与传统命令式UI开发的差异和优势
- Stage模型应用架构:HarmonyOS新的应用模型设计理念
- TypeScript/ETS语言特性:强类型语言在UI开发中的优势
- 状态管理最佳实践:如何高效管理组件状态和用户交互
- 游戏循环设计:定时器驱动的游戏逻辑实现方式
1.2 游戏规则与用户体验
"别踩白块"的核心玩法非常简单,但却极具挑战性:
基本规则:
- 游戏区域被划分为4列N行的网格布局
- 每一行恰好有一个黑色方块,其余为白色方块
- 方块持续向下移动(或通过定时器模拟移动效果)
- 玩家需要在方块移出屏幕前点击黑色方块
- 点击白色方块或漏掉黑色方块均会导致游戏结束
- 随着分数的增加,游戏速度逐渐加快,难度递增
用户体验目标:
- 零学习成本:用户无需阅读复杂的教程即可上手
- 即时反馈:点击后立即显示视觉反馈(颜色变化、勾选标记)
- 流畅操作:60fps的流畅动画和响应体验
- 渐进式难度:确保游戏既有挑战性又不会令人沮丧
1.3 技术选型决策
在开始编码之前,我们需要做出一系列技术决策:
为什么选择ArkUI?
- 声明式编程:通过描述"UI应该是什么样的"而非"如何创建UI",代码更简洁易懂
- TypeScript/ETS:强类型支持提供更好的IDE提示和编译时错误检查
- 高性能:ArkUI引擎经过深度优化,支持复杂动画和交互
- 分布式能力:未来可扩展到多设备协同游戏
为什么选择Stage模型?
- 模块化设计:Ability和UI解耦,便于单元测试和维护
- 生命周期清晰:明确的页面和组件生命周期管理
- 配置化管理:通过module.json5统一管理应用配置
二、HarmonyOS与ArkUI技术栈深度解析
2.1 HarmonyOS系统架构
HarmonyOS采用分层设计,从底层到上层依次为:
- 内核层:基于Linux内核或LiteOS,提供基础操作系统能力
- 系统服务层:提供分布式软总线、设备虚拟化、数据管理等核心服务
- 框架层:提供应用框架、图形框架、多媒体框架等
- 应用层:开发者构建的应用程序
ArkUI位于框架层,是HarmonyOS提供的UI开发框架,支持两种开发范式:
- ArkTS声明式开发范式:本文采用的方式,适合现代UI开发
- Java/C++类Web式开发范式:传统的命令式开发方式
2.2 ArkUI声明式编程模型
ArkUI声明式编程模型的核心思想是"UI即状态的函数"。开发者只需要描述在特定状态下UI应该呈现的样子,框架会自动处理状态变化时的UI更新。
核心概念:
- @Component:装饰器,标记一个类为自定义组件
- @Entry:装饰器,标记组件为页面入口组件
- @State:装饰器,标记组件内部状态,状态变化会触发UI重建
- build():方法,描述组件的UI结构
- @Builder:装饰器,标记一个方法为可复用的UI构建函数
声明式vs命令式对比:
命令式(传统Android):
Button button = new Button(context);
button.setText("开始游戏");
button.setTextColor(Color.WHITE);
button.setOnClickListener(v -> {
// 处理点击
});
layout.addView(button);
声明式(ArkUI):
Button('开始游戏')
.fontColor(Color.White)
.onClick(() => {
// 处理点击
})
可以看到,声明式方式更加简洁,代码更具可读性,且UI结构一目了然。
2.3 Stage模型应用架构
HarmonyOS的Stage模型是一种全新的应用架构,与传统的FA模型相比具有以下优势:
组件化设计:
- UIAbility:应用的基本入口组件,负责管理页面栈和生命周期
- Page:应用中的单个页面,对应用户可见的一个屏幕
- Component:可复用的UI组件
配置管理:
每个应用模块通过module.json5文件进行配置,包括:
- 模块名称和类型
- 支持的设备类型
- 页面路由配置
- Ability配置
- 权限声明
本文项目的module.json5配置中,我们可以看到:
- 模块类型为
entry(主入口模块) - 支持设备类型为
phone - 主Ability为
EntryAbility - 页面配置引用
$profile:main_pages
资源管理:
HarmonyOS采用基于资源限定符的资源管理机制:
base目录:默认资源,任何情况都可使用dark目录:深色模式专用资源zh_CN等:语言和地区专用资源
资源引用方式:
$r('app.string.app_name'):引用应用级资源$r('app.media.background'):引用媒体资源$r('app.color.primary'):引用颜色资源
2.4 ETS/TypeScript语言特性
ArkUI使用ETS(Extended TypeScript)作为开发语言,它是TypeScript的超集,支持所有TypeScript特性,并添加了一些UI开发专用的装饰器。
关键语言特性:
- 类型系统:
// 枚举类型定义游戏状态
enum GameState {
START,
PLAYING,
END
}
// 类定义数据模型
class BlockData {
isBlack: boolean
clicked: boolean
constructor(isBlack: boolean) {
this.isBlack = isBlack
this.clicked = false
}
}
- 装饰器模式:
@Entry
@Component
struct GamePage {
@State score: number = 0
@State gameState: GameState = GameState.START
// ...
}
- 箭头函数:用于事件处理和数组操作,保持上下文绑定
this.rows.some(block => block.isBlack)
- 模板字符串:用于动态文本内容
Text(`分数: ${this.score}`)
三、项目架构与文件结构
3.1 项目目录结构
一个标准的HarmonyOS Stage模型项目具有以下目录结构:
xxx/
├── AppScope/ # 应用全局配置
│ ├── resources/ # 应用级资源
│ │ └── base/
│ │ ├── element/
│ │ │ └── string.json # 应用字符串资源
│ │ └── media/ # 应用媒体资源
│ └── app.json5 # 应用全局配置
├── entry/ # 主模块
│ └── src/
│ └── main/
│ ├── ets/ # ETS源码
│ │ ├── entryability/
│ │ │ └── EntryAbility.ets # 应用入口Ability
│ │ └── pages/
│ │ └── Index.ets # 主页面(游戏实现)
│ ├── resources/ # 模块级资源
│ │ └── base/
│ │ ├── element/
│ │ │ ├── color.json
│ │ │ ├── float.json
│ │ │ └── string.json
│ │ ├── media/
│ │ └── profile/
│ │ ├── backup_config.json
│ │ └── main_pages.json # 页面路由配置
│ └── module.json5 # 模块配置
├── oh-package.json5 # 依赖配置
├── build-profile.json5 # 构建配置
└── hvigorfile.ts # 构建脚本
3.2 关键配置文件解析
app.json5 - 应用全局配置:
{
"app": {
"bundleName": "com.example.xxx", // 应用包名,全球唯一
"vendor": "example", // 开发者/公司标识
"versionCode": 1000000, // 版本号(整数,用于升级判断)
"versionName": "1.0.0", // 版本名(显示给用户)
"icon": "$media:layered_image", // 应用图标
"label": "$string:app_name" // 应用名称
}
}
module.json5 - 模块配置:
{
"module": {
"name": "entry", // 模块名称
"type": "entry", // 模块类型:entry主模块/feature特性模块
"deviceTypes": ["phone"], // 支持的设备类型
"pages": "$profile:main_pages", // 页面路由配置
"abilities": [ // Ability配置
{
"name": "EntryAbility",
"srcEntry": "./ets/entryability/EntryAbility.ets",
"skills": [ // 声明能处理的Intent
{
"entities": ["entity.system.home"],
"actions": ["ohos.want.action.home"]
}
]
}
]
}
}
main_pages.json - 页面路由配置:
{
"src": [
"pages/Index" // 页面路径,不包含.ets后缀
]
}
3.3 组件架构设计
本游戏应用采用单层架构设计,所有逻辑集中在一个页面组件中。这种设计对于简单游戏来说是合理的,但对于更复杂的应用,可能需要考虑进一步的模块化。
组件层次结构:
Index (页面入口)
├── Row (标题栏)
│ └── Text ("别踩白块")
├── Row (分数栏)
│ └── Text ("分数: X")
└── Stack (游戏区域容器)
├── Column (方块网格)
│ └── ForEach (渲染每一行)
│ └── Row (一行方块)
│ └── ForEach (渲染每个方块)
│ └── Column (单个方块)
├── Column (开始界面覆盖层) [条件渲染]
└── Column (结束界面覆盖层) [条件渲染]
状态管理架构:
// 组件状态(响应式)
@State gameState: GameState = GameState.START
@State score: number = 0
@State rows: BlockData[][] = []
// 私有状态(非响应式)
private interval: number = -1
private speed: number = 1000
private rowCount: number = 5
四、核心功能实现详解
4.1 游戏状态管理
状态管理是任何交互式应用的核心。在ArkUI中,我们使用装饰器来标记哪些数据变化会触发UI更新。
状态设计原则:
- 最小化响应式状态:只将真正需要触发UI更新的数据标记为
@State - 数据类型选择:使用合适的数据类型(number, boolean, enum, object, array)
- 状态一致性:确保相关状态同时更新,避免中间状态
游戏状态枚举设计:
enum GameState {
START, // 开始状态:显示开始界面
PLAYING, // 游戏中:方块移动,接收用户输入
END // 结束状态:显示最终得分
}
使用枚举而非字符串或数字的优势:
- 类型安全:编译器会检查枚举值的有效性
- 可读性:
GameState.START比0或"start"更清晰 - 重构友好:修改枚举值不影响代码逻辑
核心状态变量:
@State gameState: GameState = GameState.START
- 控制当前显示的界面(开始/游戏中/结束)
- 决定是否响应用户点击
- 控制游戏循环的启动和停止
@State score: number = 0
- 实时更新分数显示
- 用于结束界面的最终得分展示
- 作为难度递增的判断依据
@State rows: BlockData[][] = []
- 存储所有方块数据的二维数组
- 第一层索引代表行,第二层代表列
@State装饰器确保数组修改时UI能够重新渲染
4.2 方块数据模型设计
数据模型的设计直接影响到游戏逻辑的复杂度和可维护性。
BlockData类设计:
class BlockData {
isBlack: boolean // 是否为黑色方块
clicked: boolean // 是否已被点击
constructor(isBlack: boolean) {
this.isBlack = isBlack
this.clicked = false
}
}
设计决策分析:
-
为什么不用接口?
- 接口只定义类型,不能包含初始化逻辑
- 类的构造函数可以统一初始化
clicked为false - 未来扩展时可以添加方法
-
为什么需要
clicked字段?- 防止重复点击同一方块
- 提供视觉反馈(显示✓标记)
- 作为漏掉黑色方块的判断依据
-
为什么是二维数组?
- 自然对应网格布局的行和列
- 便于通过索引访问特定位置的方块
ForEach组件可以直接嵌套渲染
4.3 页面初始化与生命周期
ArkUI组件提供了完整的生命周期钩子,允许我们在合适的时机执行初始化和清理操作。
生命周期钩子:
aboutToAppear() {
this.initGame()
}
aboutToDisappear() {
this.stopGame()
}
生命周期流程图:
组件创建
↓
aboutToAppear() [初始化游戏数据]
↓
build() [首次渲染UI]
↓
用户交互 / 定时器触发
↓
状态变化 → build()重新执行
↓
用户离开页面
↓
aboutToDisappear() [清理定时器]
↓
组件销毁
初始化逻辑实现:
initGame() {
this.rows = []
for (let i = 0; i < this.rowCount; i++) {
this.addRow()
}
}
这里使用unshift方法将新行添加到数组开头,确保方块从上到下显示。
4.4 方块生成与布局渲染
游戏的核心视觉元素是4列的方块网格。让我们深入了解其实现原理。
添加新行逻辑:
addRow() {
const blackIndex = Math.floor(Math.random() * 4) // 随机0-3
const row: BlockData[] = []
for (let col = 0; col < 4; col++) {
row.push(new BlockData(col === blackIndex))
}
this.rows.unshift(row) // 添加到数组开头
}
随机数生成解析:
Math.random():生成[0, 1)之间的随机小数Math.random() * 4:生成[0, 4)之间的随机小数Math.floor(...):向下取整,得到0、1、2、3中的一个
为什么固定4列?
- 经典游戏设计,经过市场验证
- 手指覆盖范围适合操作
- 难度与趣味性的最佳平衡点
UI渲染实现:
Column() {
ForEach(this.rows, (row: BlockData[], rowIndex: number) => {
Row() {
ForEach(row, (block: BlockData, colIndex: number) => {
Column() {
Text(block.clicked ? '✓' : '')
.fontSize(36)
.fontColor(Color.White)
}
.layoutWeight(1)
.height('100%')
.backgroundColor(block.clicked ? '#888888' : (block.isBlack ? Color.Black : Color.White))
.border({ color: '#CCCCCC', width: 1 })
.onClick(() => {
this.handleClick(rowIndex, colIndex)
})
})
}
.width('100%')
.layoutWeight(1)
})
}
ForEach组件详解:
ForEach是ArkUI提供的列表渲染组件,它的签名如下:
ForEach(
arr: any[], // 要遍历的数组
itemGenerator: (item: any, index: number) => void, // 每个元素的UI构建函数
keyGenerator?: (item: any) => string // 可选的key生成函数(性能优化)
)
layoutWeight属性的作用:
在Row中使用.layoutWeight(1)可以让子组件平分剩余空间:
- 4个方块都设置
layoutWeight(1),每个占1/4宽度 - 5行都设置
layoutWeight(1),每行占1/5高度 - 响应式布局,适应不同屏幕尺寸
样式链式调用:
ArkUI采用链式调用的方式设置组件属性,这种方式借鉴了SwiftUI和Flutter的设计理念:
Column()
.layoutWeight(1)
.height('100%')
.backgroundColor(Color.Black)
.border({ color: '#CCCCCC', width: 1 })
这种风格的优势:
- 代码结构清晰,UI描述一目了然
- 无需创建中间变量
- 支持代码提示和类型检查
4.5 游戏循环与方块"下落"实现
"别踩白块"的核心游戏体验来自于方块不断向下移动带来的紧迫感。让我们分析几种实现方案。
方案对比:
| 方案 | 实现方式 | 优点 | 缺点 |
|---|---|---|---|
| 方案A:translate动画 | 每个方块设置y偏移,定时增加 | 视觉效果流畅,真实下落感 | 实现复杂,需精确计算边界 |
| 方案B:定时器替换 | 定时移除底部行,顶部添加新行 | 实现简单,逻辑清晰 | 视觉效果是"闪烁"而非滑动 |
| 方案C:混合方式 | 短间隔translate + 边界替换 | 视觉效果好 | 最复杂,调试困难 |
本文采用方案B的实现:
startTimer() {
if (this.interval !== -1) {
clearInterval(this.interval)
}
this.interval = setInterval(() => {
this.moveDown()
}, this.speed)
}
moveDown() {
const bottomRow = this.rows[this.rows.length - 1]
let hasUnclickedBlack = false
for (let col = 0; col < 4; col++) {
if (bottomRow[col].isBlack && !bottomRow[col].clicked) {
hasUnclickedBlack = true
break
}
}
if (hasUnclickedBlack) {
this.gameOver()
return
}
this.rows.pop() // 移除底部行
this.addRow() // 顶部添加新行
}
JavaScript定时器机制:
setInterval是浏览器/JavaScript环境提供的定时执行函数:
setInterval(callback, delay)
callback:要周期性执行的函数delay:执行间隔(毫秒)- 返回值:定时器ID,用于后续取消
为什么要先clearInterval?
if (this.interval !== -1) {
clearInterval(this.interval)
}
- 防止重复启动导致多个定时器同时运行
- 确保重新开始游戏时使用新的速度值
- 这是一种防御性编程实践
游戏逻辑流程:
定时器触发
↓
获取最底部一行
↓
检查是否有未点击的黑色方块
↓
┌─ 有 → 游戏结束 → return
│
└─ 无 → 继续
↓
移除底部行
↓
顶部添加新行(随机黑色位置)
↓
等待下一次定时器触发
4.6 用户输入处理
游戏的交互核心是方块点击事件。让我们分析点击处理逻辑。
点击处理实现:
handleClick(rowIndex: number, colIndex: number) {
if (this.gameState !== GameState.PLAYING) return
const block = this.rows[rowIndex][colIndex]
if (block.clicked) return
if (block.isBlack) {
block.clicked = true
this.score++
if (this.score > 0 && this.score % 5 === 0 && this.speed > 300) {
this.speed -= 100
this.startTimer()
}
} else {
this.gameOver()
}
}
防御性检查解析:
if (this.gameState !== GameState.PLAYING) return
- 在开始/结束界面点击方块不响应
- 防止游戏状态不一致
if (block.clicked) return
- 防止重复点击
- 避免分数异常增加
- 已经点击的方块视觉上应该是灰色的
正确点击处理流程:
点击黑色方块
↓
标记为已点击
↓
分数+1
↓
检查是否达到难度提升条件
↓
┌─ 是(每5分)→ 速度加快 → 重启定时器
│
└─ 否 → 等待下一次点击
难度递增算法:
if (this.score > 0 && this.score % 5 === 0 && this.speed > 300) {
this.speed -= 100
this.startTimer()
}
- 初始速度:1000ms(1秒)
- 速度变化:每次-100ms
- 最小速度:300ms
- 触发条件:分数是5的倍数
| 分数区间 | 间隔时间 | 每秒行数 |
|---|---|---|
| 0-4 | 1000ms | 1行/秒 |
| 5-9 | 900ms | ~1.1行/秒 |
| 10-14 | 800ms | 1.25行/秒 |
| … | … | … |
| 35+ | 300ms | ~3.3行/秒 |
为什么最小限制300ms?
- 人类反应速度极限约为200-250ms
- 还需要加上手指移动到目标位置的时间
- 保持游戏的可玩性而非纯粹的测试反应极限
错误点击处理:
else {
this.gameOver()
}
- 点击白色方块立即结束游戏
- 这是"别踩白块"名称的由来
- 简单直接的失败条件
4.7 游戏结束与重新开始
结束处理:
gameOver() {
this.gameState = GameState.END
this.stopGame()
}
stopGame() {
if (this.interval !== -1) {
clearInterval(this.interval)
this.interval = -1
}
}
关键操作:
- 将状态改为END,触发结束界面渲染
- 清除定时器,停止方块移动
- 将interval重置为-1(哨兵值)
重新开始:
startGame() {
this.gameState = GameState.PLAYING
this.score = 0
this.speed = 1000
this.rows = []
this.initGame()
this.startTimer()
}
关键操作:
- 状态重置为PLAYING
- 所有状态变量恢复初始值
- 重新初始化方块数组
- 启动游戏循环
4.8 条件渲染与界面覆盖层
游戏有三个不同的界面状态,我们使用条件渲染来控制显示。
条件渲染语法:
if (this.gameState === GameState.START) {
// 渲染开始界面
}
if (this.gameState === GameState.END) {
// 渲染结束界面
}
ArkUI的条件渲染与普通TypeScript条件语句完全一致,但有一个重要区别:
- 编译时转换:ArkUI编译器会将
if语句转换为条件节点 - 惰性计算:条件不满足时,内部组件不会实例化
开始界面实现:
Column() {
Column() {
Text('别踩白块')
.fontSize(36)
.fontWeight(FontWeight.Bold)
.fontColor(Color.Black)
.margin({ bottom: 20 })
Text('游戏规则:')
.fontSize(18)
.fontColor('#666666')
.margin({ bottom: 10 })
Text('点击黑色方块得分')
.fontSize(16)
.fontColor('#666666')
.margin({ bottom: 5 })
// ... 更多规则说明
Button('开始游戏')
.width(160)
.height(50)
.fontSize(20)
.backgroundColor(Color.Black)
.fontColor(Color.White)
.onClick(() => {
this.startGame()
})
}
.padding(30)
.borderRadius(20)
.backgroundColor(Color.White)
.shadow({ radius: 10, color: '#999999' })
}
.width('100%')
.height('100%')
.backgroundColor('rgba(0,0,0,0.3)')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
视觉层级设计:
使用Stack容器实现覆盖层效果:
Stack (层叠容器)
├── 底层:方块网格(始终存在)
├── 中层:开始界面(条件显示)
└── 顶层:结束界面(条件显示)
Stack的特性:
- 子组件按顺序层叠,后添加的在上层
alignContent参数控制对齐方式- 适合实现弹窗、覆盖层等效果
半透明遮罩层:
.backgroundColor('rgba(0,0,0,0.3)')
rgba:红绿蓝+透明度0,0,0:黑色0.3:30%不透明度- 突出显示中央的白色卡片
卡片式设计:
内层Column的样式:
- 圆角20px
- 白色背景
- 阴影效果
- 内边距30px
这是现代UI设计中常用的"卡片"模式,能够有效区分内容区域和背景。
五、界面UI与用户体验设计
5.1 色彩体系设计
颜色方案分析:
| 用途 | 颜色值 | 设计考量 |
|---|---|---|
| 标题栏背景 | #333333 |
深灰色,与纯黑形成层次 |
| 分数栏背景 | #444444 |
比标题栏稍浅,形成区分 |
| 黑色方块 | Color.Black |
纯黑,视觉目标明确 |
| 白色方块 | Color.White |
纯白,对比强烈 |
| 已点击方块 | #888888 |
灰色,表示已处理 |
| 边框 | #CCCCCC |
浅灰色,分隔方块 |
| 遮罩层 | rgba(0,0,0,0.3) |
半透明,不遮挡底层 |
| 标题文字 | Color.Black/Red |
黑色为开始,红色为结束(警示) |
| 规则文字 | #666666 |
中灰色,不抢夺注意力 |
色彩心理学应用:
- 红色(结束界面标题):代表警告、停止、危险,符合游戏结束的语义
- 黑色(方块):代表目标、重点,引导用户点击
- 白色(方块):代表危险区域,需要避开
- 灰色(已点击/遮罩):代表非活跃、已处理状态
5.2 布局系统详解
ArkUI提供了多种布局容器,本项目主要使用了Column、Row和Stack。
Column容器:
Column() {
// 子组件垂直排列
}
.justifyContent(FlexAlign.Center) // 主轴(垂直)对齐
.alignItems(HorizontalAlign.Center) // 交叉轴(水平)对齐
常用的justifyContent值:
FlexAlign.Start:顶部对齐FlexAlign.Center:居中对齐FlexAlign.End:底部对齐FlexAlign.SpaceBetween:两端对齐FlexAlign.SpaceEvenly:均匀分布
Row容器:
Row() {
// 子组件水平排列
}
.justifyContent(FlexAlign.Center) // 主轴(水平)对齐
.alignItems(VerticalAlign.Center) // 交叉轴(垂直)对齐
Stack容器:
Stack({ alignContent: Alignment.TopStart }) {
// 子组件层叠排列
}
alignContent参数控制所有子组件的对齐方式:
Alignment.TopStart:左上角Alignment.Center:居中Alignment.BottomEnd:右下角- 等等
弹性布局(Flexbox):
ArkUI的布局系统基于CSS Flexbox规范,但进行了简化和优化。
layoutWeight属性的作用:
Row容器,宽度400px
├── 子组件A,layoutWeight(1) → 宽度100px
├── 子组件B,layoutWeight(1) → 宽度100px
├── 子组件C,layoutWeight(1) → 宽度100px
└── 子组件D,layoutWeight(1) → 宽度100px
总计权重4,每个权重对应100px。
如果权重不同:
Row容器,宽度400px
├── 子组件A,layoutWeight(1) → 宽度80px
├── 子组件B,layoutWeight(1) → 宽度80px
├── 子组件C,layoutWeight(2) → 宽度160px
└── 子组件D,layoutWeight(1) → 宽度80px
总计权重5,每个权重对应80px。
5.3 文本样式系统
Text组件属性详解:
Text('别踩白块')
.fontSize(24) // 字体大小
.fontWeight(FontWeight.Bold) // 字重
.fontColor(Color.White) // 字体颜色
字体大小设计:
| 元素 | 大小 | 作用 |
|---|---|---|
| 大标题(开始界面) | 36 | 吸引注意 |
| 结束标题 | 32 | 警示作用 |
| 页面标题 | 24 | 标准标题 |
| 得分 | 24/20 | 清晰可见 |
| 按钮文字 | 20 | 易于点击 |
| 规则标题 | 18 | 次级标题 |
| 规则内容 | 16 | 正文阅读 |
字重选择:
FontWeight.Bold:标题、按钮等重要元素- 默认(Regular):正文、辅助信息
颜色对比:
- 深色背景 + 白色文字:高对比度,易于阅读
- 白色背景 + 黑色文字:标准阅读配置
- 红色标题:强调警示
5.4 按钮与交互反馈
Button组件用法:
Button('开始游戏')
.width(160) // 固定宽度
.height(50) // 固定高度
.fontSize(20)
.backgroundColor(Color.Black)
.fontColor(Color.White)
.onClick(() => {
this.startGame()
})
按钮设计原则:
- 足够大的点击区域:50px高度,160px宽度,符合移动设备交互规范
- 高对比度:黑底白字,清晰可见
- 明确的语义:“开始游戏”、“再来一局”,操作意图明确
点击反馈设计:
本项目采用了两种视觉反馈方式:
- 方块点击:黑色→灰色,显示✓标记
- 状态切换:界面整体变化(开始→游戏中→结束)
未来可优化的反馈方式:
- 点击时的缩放动画
- 触觉反馈(震动)
- 音效反馈
5.5 阴影与圆角设计
阴影效果:
.shadow({
radius: 10, // 模糊半径
color: '#999999' // 阴影颜色
})
阴影的作用:
- 制造层次感,卡片从背景"浮起"
- 引导视觉焦点
- 现代UI设计的标配元素
圆角设计:
.borderRadius(20)
20px的圆角属于"中圆角"范畴:
- 比小圆角更柔和
- 比大圆角落差更小
- 卡片式设计的经典选择
六、性能优化与最佳实践
6.1 状态管理优化
最小化响应式状态:
// ✅ 好的实践
@State score: number = 0 // 必须响应式,要更新UI
// ❌ 不需要的响应式
private speed: number = 1000 // 非响应式,足够
private interval: number = -1
为什么rows需要@State?
@State rows: BlockData[][] = []
因为:
this.rows.pop():数组变化需要触发重新渲染this.rows.unshift():添加元素需要UI更新block.clicked = true:数组元素属性变化
数组更新的注意事项:
在ArkUI中,修改数组的某些操作可能无法自动触发UI更新。本项目使用的方式:
this.rows = [] // 重新赋值(可靠)
this.rows.pop() // 方法调用(应该能触发)
this.rows.unshift(row) // 方法调用
更可靠的方式是创建新数组:
// 替代方案:创建新数组
this.rows = [...this.rows.slice(1), newRow]
6.2 定时器资源管理
资源泄漏风险:
aboutToDisappear() {
this.stopGame()
}
如果忘记在页面销毁时清理定时器:
- 定时器继续运行,占用CPU
- 回调函数引用组件实例,导致内存泄漏
- 后台运行影响其他应用性能
哨兵值模式:
private interval: number = -1 // -1表示无定时器
if (this.interval !== -1) {
clearInterval(this.interval)
this.interval = -1 // 重置为哨兵值
}
使用-1而非null或undefined的优势:
- 数值类型,无需类型检查
- 与
setInterval返回值(正整数)明确区分 - 初始化时不需要条件判断
6.3 条件渲染性能
避免频繁的条件切换:
本项目的三个状态是互斥的,符合最佳实践:
START ──→ PLAYING ──→ END
↑ │
└───── 重新开始 ─────┘
状态转换简单清晰,没有复杂的状态叠加。
避免嵌套过深的条件:
当前结构:
if (START) { ... }
if (END) { ... }
良好的扁平化结构,易于理解和维护。
6.4 ForEach渲染优化
ForEach的工作原理:
当数组变化时,ArkUI会对比新旧数组,计算差异,然后更新UI。
影响性能的因素:
- 数组大小:本项目只有5行,完全不成问题
- key生成:如果提供keyGenerator,性能更好
- 组件复杂度:子组件越简单,渲染越快
可选的性能优化:
ForEach(
this.rows,
(row: BlockData[], rowIndex: number) => { /* ... */ },
(row: BlockData[], index: number) => index.toString() // key生成
)
对于简单数组,index作为key是可以接受的。但如果数组元素会移动,应该使用元素的唯一标识。
6.5 代码组织与可维护性
单一职责原则:
本项目的函数职责清晰:
| 函数 | 职责 |
|---|---|
initGame() |
初始化游戏数据 |
addRow() |
添加一行方块 |
startGame() |
开始游戏流程 |
startTimer() |
启动游戏循环 |
stopGame() |
停止游戏循环 |
moveDown() |
执行一次移动 |
handleClick() |
处理点击事件 |
gameOver() |
结束游戏 |
每个函数只做一件事,符合单一职责原则。
代码复用:
使用@Builder装饰器可以提取重复的UI代码。本项目虽然UI部分较长,但都是一次性使用,提取可能反而增加复杂度。
命名规范:
- 组件名:
Index(PascalCase) - 状态变量:
gameState,score(camelCase) - 私有变量:
interval,speed(camelCase) - 函数名:
initGame,startGame(camelCase,动词开头) - 类名:
BlockData,GameState(PascalCase) - 常量:无(可以考虑
ROW_COUNT,INITIAL_SPEED等)
6.6 TypeScript类型安全
枚举的优势:
// 使用枚举
if (this.gameState === GameState.START) { ... }
// 不使用枚举(容易出错)
if (this.gameState === 0) { ... } // 0是什么意思?
if (this.gameState === 'start') { ... } // 拼写错误?
类型注解的价值:
ForEach(this.rows, (row: BlockData[], rowIndex: number) => {
// row自动具有BlockData[]类型提示
// rowIndex自动具有number类型
})
IDE可以提供:
- 代码补全
- 类型错误检查
- 重构支持
七、测试与质量保证
7.1 单元测试策略
HarmonyOS项目集成了Hypium测试框架。
项目依赖:
{
"devDependencies": {
"@ohos/hypium": "1.0.25",
"@ohos/hamock": "1.0.0"
}
}
可测试的单元:
- BlockData类:构造函数初始化是否正确
- 游戏逻辑:
addRow()是否正确生成4个方块,1个黑色handleClick()对黑/白方块的处理- 难度递增计算是否正确
- 边界条件:
- 速度降到300ms后是否继续降低
- 游戏结束条件触发
7.2 测试用例设计
测试用例1:方块生成
it('addRow should generate 4 blocks with exactly 1 black', () => {
// 模拟调用
const row = generateRow()
// 断言
expect(row.length).assertEqual(4)
const blackCount = row.filter(b => b.isBlack).length
expect(blackCount).assertEqual(1)
})
测试用例2:点击处理
it('clicking black block should increase score', () => {
// 初始化
const score = 0
// 操作
handleBlackBlockClick()
// 断言
expect(score).assertEqual(1)
})
it('clicking white block should trigger game over', () => {
// 操作
handleWhiteBlockClick()
// 断言
expect(gameState).assertEqual(GameState.END)
})
测试用例3:难度递增
it('speed should decrease every 5 points until 300ms', () => {
// 测试分数5、10、15时的速度变化
// 测试分数40时速度是否为300
})
7.3 手动测试用例
除了自动化测试,还需要进行手动测试:
功能测试:
- 启动应用显示开始界面
- 点击"开始游戏"进入游戏
- 点击黑色方块得分+1
- 点击白色方块游戏结束
- 漏掉黑色方块(等待定时器)游戏结束
- 结束界面显示最终得分
- 点击"再来一局"重新开始
- 每得5分速度明显加快
边界测试:
- 快速连续点击同一方块
- 在状态切换瞬间点击
- 长时间运行游戏(内存泄漏检测)
设备适配测试:
- 不同屏幕尺寸
- 深色模式(如有)
- 横竖屏切换(如支持)
八、项目扩展与未来展望
8.1 功能扩展方向
当前功能:
- 经典模式:无限下落,直到失误
可扩展功能:
-
多种游戏模式:
- 经典模式:已有
- 限时模式:30秒内尽可能多得分
- 街机模式:速度持续增加
- 禅模式:没有时间限制,没有速度增加
-
音效系统:
- 点击音效
- 错误音效
- 背景音乐
-
视觉增强:
- 点击动画
- 连击特效
- 粒子效果
-
数据持久化:
- 本地最高分记录
- 游戏统计
- 成就系统
-
社交功能:
- 排行榜
- 分享战绩
8.2 技术架构升级
当前架构:单页面组件
未来架构方向:
Index (页面)
├── GameLogicService (游戏逻辑服务)
├── ScoreManager (分数管理)
├── AudioManager (音效管理)
├── StorageManager (存储管理)
└── UI Components
├── StartScreen
├── GameBoard
├── GameOverScreen
└── Block
模块化重构:
将游戏逻辑提取为独立服务:
class GameService {
score: number = 0
gameState: GameState = GameState.START
startGame() { /* ... */ }
endGame() { /* ... */ }
handleClick(row: number, col: number): boolean { /* ... */ }
}
这样做的好处:
- 逻辑与UI分离
- 便于单元测试
- 可在多个页面复用
8.3 性能优化方向
动画优化:
当前使用定时器+数组替换模拟移动,未来可以:
- 使用ArkUI动画API实现平滑过渡
- 使用requestAnimationFrame替代setInterval
- 实现帧率控制
// 使用animateTo实现平滑动画
animateTo({ duration: 500 }, () => {
this.offsetY += 100
})
渲染优化:
- 实现对象池,避免频繁创建BlockData
- 减少不必要的状态更新
- 使用
@Observed和@ObjectLink优化对象状态
@Observed
class BlockData {
@ObjectLink isBlack: boolean
// ...
}
8.4 分布式能力探索
HarmonyOS的核心优势是分布式能力,游戏可以扩展为:
多设备协同:
- 手机显示游戏画面
- 平板作为控制器
- 多人同屏对战
跨端迁移:
- 在手机上开始游戏
- 无缝迁移到平板继续
- 游戏状态同步
能力分享:
- 调用其他设备的震动马达
- 使用智慧屏的更大屏幕
九、总结与反思
9.1 项目技术价值回顾
通过这个简单的游戏项目,我们深入了解了:
技术收获:
- ArkUI声明式开发:掌握了@Component、@State、@Entry等核心装饰器的用法
- Stage模型架构:理解了Ability、Page、Component的层次关系
- TypeScript/ETS:熟练运用枚举、类、类型注解等特性
- 游戏循环设计:定时器驱动的状态机模式
- UI布局系统:Column、Row、Stack的灵活运用
项目亮点:
- 代码简洁:不到300行实现完整游戏
- 逻辑清晰:状态机设计易于理解
- 性能良好:没有复杂计算,UI更新最小化
- 用户体验:即时反馈,渐进式难度
9.2 开发经验总结
最佳实践:
-
状态设计要谨慎:
- 只将必要的数据标记为@State
- 思考状态变化的触发条件
- 保持状态一致性
-
资源管理要严格:
- 定时器、订阅等必须释放
- 使用哨兵值标记无效状态
- 在生命周期钩子中清理
-
代码组织要清晰:
- 函数单一职责
- 命名规范统一
- 适当的注释说明
-
用户体验要重视:
- 即时视觉反馈
- 清晰的操作指引
- 合理的难度曲线
常见陷阱:
-
状态更新死循环:
- 在build中修改状态
- 状态变化触发无限重建
-
资源泄漏:
- 忘记清理定时器
- 回调函数持有强引用
-
UI闪烁:
- 不必要的状态更新
- 数组直接修改不触发更新
9.3 对初学者的建议
对于刚接触ArkUI和HarmonyOS开发的同学,建议:
-
从小项目开始:
- 像"别踩白块"这样的简单游戏是绝佳练习
- 功能有限但覆盖核心技术点
- 可以快速获得成就感
-
深入理解响应式:
- @State的工作原理
- 状态变化如何触发UI更新
- 状态管理的最佳实践
-
多读官方文档:
- ArkUI组件库
- 生命周期管理
- 性能优化指南
-
动手实践最重要:
- 阅读代码与编写代码是两回事
- 遇到问题先尝试自己解决
- 在错误中学习成长
9.4 结语
"别踩白块"是一个看似简单实则蕴含丰富技术点的项目。通过本文的深入剖析,我们不仅了解了如何用ArkUI实现这个游戏,更重要的是掌握了:
- 声明式UI开发的思维方式
- 游戏应用的架构设计模式
- 状态管理和资源管理的最佳实践
- 用户体验设计的基本原则
HarmonyOS和ArkUI代表了移动开发的一个新方向。声明式编程、分布式能力、一次开发多端部署等特性,为开发者提供了强大的工具。相信随着生态的不断完善,会有越来越多的优秀应用诞生在这个平台上。
希望本文能对你的HarmonyOS开发之旅有所帮助。记住,最好的学习方式是动手实践,从一个简单的项目开始,逐步扩展,你会发现编程的乐趣和成就感。
参考资源:
更多推荐



所有评论(0)