HarmonyOS ArkTS找不同数字游戏实现技术


引言
找不同数字游戏是一款经典的视觉注意力训练游戏,玩家需要在一堆相同数字中快速找到唯一不同的那个。这款游戏看似简单,但随着网格规模的增大,难度会呈指数级增长。在本文中,我们将深入探讨如何使用HarmonyOS ArkTS技术栈实现这款游戏,包括核心算法、动态网格布局、视觉设计和性能优化等方面。
一、游戏概述与设计理念
1.1 游戏规则
找不同数字游戏的核心玩法非常直观:
- 游戏界面显示一个N×N的网格,大部分格子中显示相同的数字
- 只有一个格子中的数字与其他不同
- 玩家需要快速找到并点击这个不同的数字
- 游戏记录完成时间,挑战更快的反应速度
- 点击正确的数字后游戏胜利
1.2 设计目标
在设计这款游戏时,我们设定了以下核心目标:
| 目标类型 | 具体目标 | 实现方式 |
|---|---|---|
| 训练价值 | 提升视觉注意力和快速识别能力 | 在大量相似元素中寻找差异 |
| 难度递进 | 从简单到困难的渐进式关卡 | 网格大小4×4→6×6→8×8→10×10 |
| 公平竞争 | 所有数字视觉样式完全一致 | 统一字体颜色和背景,消除视觉线索 |
| 用户体验 | 流畅的交互和即时反馈 | 点击即判定,无需额外确认 |
| 进度追踪 | 记录通关进度和最佳时间 | 全局状态管理+数据持久化 |
1.3 难度递增策略
游戏难度主要通过网格大小来控制:
| 难度等级 | 网格大小 | 格子总数 | 找到不同数字的概率 |
|---|---|---|---|
| 入门 | 4×4 | 16 | 1/16 ≈ 6.25% |
| 简单 | 6×6 | 36 | 1/36 ≈ 2.78% |
| 中等 | 8×8 | 64 | 1/64 ≈ 1.56% |
| 困难 | 10×10 | 100 | 1/100 = 1% |
随着网格增大,玩家需要扫描的区域呈平方级增长,大大增加了游戏难度。
二、项目架构与组件设计
2.1 页面结构
找不同数字游戏包含两个核心页面:
├── FindDifferentLevelPage.ets # 关卡选择页面
└── FindDifferentPage.ets # 游戏主页面
页面职责划分:
- FindDifferentLevelPage:展示关卡列表,按难度分类(入门/简单/中等/困难),显示通关时间,提供解锁功能
- FindDifferentPage:游戏核心逻辑,包括数字生成、点击验证、计时、胜利判定
2.2 数据模型设计
为了支持多样化的关卡配置,我们设计了两个核心数据类:
class LevelInfo {
level: number = 0;
gridSize: number = 0;
label: string = '';
constructor(level: number, gridSize: number, label: string) {
this.level = level;
this.gridSize = gridSize;
this.label = label;
}
}
class Category {
name: string = '';
description: string = '';
color: string = '';
levels: Array<LevelInfo> = [];
constructor(name: string, description: string, color: string) {
this.name = name;
this.description = description;
this.color = color;
this.levels = [];
}
}
LevelInfo字段说明:
| 字段 | 类型 | 说明 |
|---|---|---|
| level | number | 关卡编号 |
| gridSize | number | 网格大小(4/6/8/10) |
| label | string | 显示标签(如"4×4") |
2.3 组件状态定义
游戏页面使用了以下状态变量:
@State gridSize: number = 5;
@State currentLevel: number = 1;
@State isPlaying: boolean = false;
@State numbers: Array<number> = [];
@State targetNumber: number = 0;
@State timerSeconds: number = 0;
@State bestTime: number = 0;
private timerId: number = 0;
状态变量说明:
| 变量 | 类型 | 说明 |
|---|---|---|
| gridSize | number | 当前网格大小 |
| currentLevel | number | 当前关卡 |
| isPlaying | boolean | 是否正在游戏中 |
| numbers | Array<number> | 网格中的所有数字 |
| targetNumber | number | 需要找到的目标数字 |
| timerSeconds | number | 计时秒数 |
| bestTime | number | 当前关卡最佳时间 |
| timerId | number | 计时器ID |
三、核心游戏逻辑实现
3.1 数字生成算法
游戏的核心在于生成大部分相同、只有一个不同的数字数组:
private startGame(): void {
this.isPlaying = true;
this.timerSeconds = 0;
this.numbers = [];
let mainNumber: number = Math.floor(Math.random() * 9) + 1;
let differentNumber: number = mainNumber;
while (differentNumber === mainNumber) {
differentNumber = Math.floor(Math.random() * 9) + 1;
}
this.targetNumber = differentNumber;
let total: number = this.gridSize * this.gridSize;
for (let i = 0; i < total - 1; i++) {
this.numbers.push(mainNumber);
}
this.numbers.push(differentNumber);
this.numbers = this.shuffle(this.numbers);
this.startTimer();
}
生成流程分析:
1. 随机生成主数字(1-9)
↓
2. 生成不同数字,确保与主数字不同
↓
3. 根据网格大小生成(total-1)个主数字
↓
4. 添加1个不同数字
↓
5. 打乱数组顺序
↓
6. 开始计时
算法特点:
- 使用1-9的数字范围,避免0的混淆
- 通过while循环确保不同数字与主数字确实不同
- 只生成一个不同数字,保持游戏的简洁性
3.2 Fisher-Yates洗牌算法
生成的数字数组需要随机打乱,确保不同数字的位置不确定:
private shuffle(array: Array<number>): Array<number> {
let result: Array<number> = [...array];
for (let i = result.length - 1; i > 0; i--) {
let j: number = Math.floor(Math.random() * (i + 1));
let temp: number = result[i];
result[i] = result[j];
result[j] = temp;
}
return result;
}
算法分析:
| 特性 | Fisher-Yates洗牌 |
|---|---|
| 时间复杂度 | O(n) |
| 空间复杂度 | O(n)(创建副本) |
| 随机性 | 均匀随机,每个排列概率相等 |
| 公平性 | 确保每次游戏的不同数字位置完全随机 |
注意事项:
代码中使用了展开运算符[...array]来复制数组,但根据ArkTS语法约束,展开运算符仅支持数组展开到rest参数或数组字面量中。在实际编译时,这可能需要调整为手动复制:
private shuffle(array: Array<number>): Array<number> {
let result: Array<number> = [];
for (let i = 0; i < array.length; i++) {
result.push(array[i]);
}
for (let i = result.length - 1; i > 0; i--) {
let j: number = Math.floor(Math.random() * (i + 1));
let temp: number = result[i];
result[i] = result[j];
result[j] = temp;
}
return result;
}
3.3 点击验证逻辑
点击验证是游戏的核心交互逻辑:
private clickNumber(num: number): void {
if (num === this.targetNumber) {
this.stopTimer();
let completedLevels: Array<number> = appState.findDifferentCompletedLevels;
let exists: boolean = false;
for (let i = 0; i < completedLevels.length; i++) {
if (completedLevels[i] === this.currentLevel) {
exists = true;
break;
}
}
if (!exists) {
completedLevels.push(this.currentLevel);
completedLevels.sort((a: number, b: number) => a - b);
appState.findDifferentCompletedLevels = completedLevels;
}
let levelTimes: Array<number> = appState.findDifferentLevelTimes;
while (levelTimes.length <= this.currentLevel) {
levelTimes.push(0);
}
if (levelTimes[this.currentLevel] === 0 || this.timerSeconds < levelTimes[this.currentLevel]) {
levelTimes[this.currentLevel] = this.timerSeconds;
appState.findDifferentLevelTimes = levelTimes;
this.bestTime = this.timerSeconds;
}
this.isPlaying = false;
}
}
验证流程:
用户点击格子 → 获取点击的数字 → 比较是否等于目标数字
↓
匹配成功 → 停止计时 → 记录通关进度 → 更新最佳时间 → 结束游戏
↓
匹配失败 → 无反应,继续游戏
胜利处理步骤:
- 停止计时:立即停止计时器,记录当前用时
- 检查通关状态:判断当前关卡是否已通关
- 记录通关进度:如果是首次通关,将关卡编号加入已完成数组
- 更新最佳时间:比较当前用时与历史最佳时间,更新更优的记录
- 结束游戏:设置isPlaying为false,返回开始界面
3.4 视觉一致性设计
为了增加游戏难度,所有数字必须使用完全相同的视觉样式,消除任何可能的视觉线索:
Grid() {
ForEach(this.numbers, (num: number) => {
GridItem() {
Column() {
Text(num.toString())
.fontSize(this.getFontSize())
.fontColor('#FFFFFF')
}
.backgroundColor('rgba(255, 215, 0, 0.05)')
.borderColor('#FFD70040')
.borderWidth(this.gridSize <= 6 ? 1 : 0.5)
.borderRadius(this.gridSize <= 6 ? 8 : 2)
.width('100%')
.height('100%')
.justifyContent(FlexAlign.Center)
.alignItems(HorizontalAlign.Center)
.onClick(() => this.clickNumber(num))
}
})
}
视觉设计要点:
- 统一字体颜色:所有数字使用白色(#FFFFFF),消除颜色差异
- 统一背景颜色:所有格子使用相同的金色透明背景(rgba(255, 215, 0, 0.05))
- 统一边框样式:根据网格大小调整边框宽度和圆角
- 动态字体大小:根据网格大小自适应调整字体大小
这种设计确保玩家只能通过数字本身的差异来找到目标,而不能依赖任何视觉提示。
四、关卡系统设计
4.1 关卡分类体系
游戏采用四级难度递进体系,共20个关卡:
| 难度等级 | 关卡范围 | 网格大小 | 颜色标识 |
|---|---|---|---|
| 入门 | 1-5 | 4×4 | 绿色 #32CD32 |
| 简单 | 6-10 | 6×6 | 青色 #00CED1 |
| 中等 | 11-15 | 8×8 | 金色 #FFD700 |
| 困难 | 16-20 | 10×10 | 红色 #FF6347 |
4.2 关卡初始化
private initCategories(): void {
let cat1: Category = new Category('入门', '4×4 网格', '#32CD32');
for (let i = 1; i <= 5; i++) {
cat1.levels.push(new LevelInfo(i, 4, '4×4'));
}
let cat2: Category = new Category('简单', '6×6 网格', '#00CED1');
for (let i = 6; i <= 10; i++) {
cat2.levels.push(new LevelInfo(i, 6, '6×6'));
}
let cat3: Category = new Category('中等', '8×8 网格', '#FFD700');
for (let i = 11; i <= 15; i++) {
cat3.levels.push(new LevelInfo(i, 8, '8×8'));
}
let cat4: Category = new Category('困难', '10×10 网格', '#FF6347');
for (let i = 16; i <= 20; i++) {
cat4.levels.push(new LevelInfo(i, 10, '10×10'));
}
this.categories = [cat1, cat2, cat3, cat4];
}
4.3 难度设计思路
阶段1(1-5关):4×4网格
- 16个格子,其中15个相同数字,1个不同数字
- 适合初学者熟悉游戏规则
- 找到不同数字的概率约为6.25%
阶段2(6-10关):6×6网格
- 36个格子,其中35个相同数字,1个不同数字
- 需要更仔细的观察
- 找到不同数字的概率约为2.78%
阶段3(11-15关):8×8网格
- 64个格子,其中63个相同数字,1个不同数字
- 需要高效的搜索策略
- 找到不同数字的概率约为1.56%
阶段4(16-20关):10×10网格
- 100个格子,其中99个相同数字,1个不同数字
- 最高难度挑战
- 找到不同数字的概率仅为1%
五、计时系统实现
5.1 计时器设计
游戏使用基于setTimeout的递归计时方案:
private startTimer(): void {
this.timerId = 1;
this.tick();
}
private tick(): void {
if (this.timerId !== 0) {
this.timerSeconds++;
setTimeout(() => {
this.tick();
}, 1000);
}
}
private stopTimer(): void {
if (this.timerId !== 0) {
clearInterval(this.timerId);
this.timerId = 0;
}
}
计时实现说明:
- 使用递归setTimeout而非setInterval,避免累积误差
- timerId作为计时状态标志,非零表示计时器运行中
- 停止时使用clearInterval清除定时器(实际上应使用clearTimeout)
5.2 时间格式化
private formatTime(seconds: number): string {
let mins: number = Math.floor(seconds / 60);
let secs: number = seconds % 60;
return `${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`;
}
格式输出示例:
| 秒数 | 格式化结果 |
|---|---|
| 3 | 00:03 |
| 45 | 00:45 |
| 120 | 02:00 |
5.3 最佳时间显示
Text('最佳: ' + this.formatTime(this.bestTime))
.fontSize(10)
.fontColor('#87CEEB')
最佳时间在页面头部显示,让玩家随时了解自己的历史最佳成绩。
六、UI布局与动态适配
6.1 页面布局结构
build() {
Column({ space: 16 }) {
Row() {
Button('← 返回')
.fontSize(14)
.fontColor('#FFD700')
.backgroundColor('transparent')
.borderColor('#FFD700')
.borderWidth(1)
.padding({ left: 12, right: 12 })
.onClick(() => {
this.stopTimer();
router.back();
})
Text('找不同数字')
.fontSize(18)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD700')
Column({ space: 2 }) {
Text('第' + this.currentLevel + '关')
.fontSize(12)
.fontColor('#87CEEB')
Text('最佳: ' + this.formatTime(this.bestTime))
.fontSize(10)
.fontColor('#87CEEB')
}
}
.width('100%')
.justifyContent(FlexAlign.SpaceBetween)
.padding({ left: 20, right: 20 })
.margin({ top: 40 })
Text(this.formatTime(this.timerSeconds))
.fontSize(32)
.fontColor('#87CEEB')
.fontWeight(FontWeight.Bold)
if (!this.isPlaying) {
Column({ space: 16 }) {
Text('找到不同的数字')
.fontSize(18)
.fontColor('#FFD700')
Button('开始游戏')
.fontSize(18)
.fontColor('#0A192F')
.backgroundColor('#FFD700')
.borderRadius(12)
.padding({ left: 40, right: 40, top: 12, bottom: 12 })
.onClick(() => this.startGame())
}
} else {
Column() {
Text('找到不同的数字')
.fontSize(18)
.fontColor('#FFD700')
Scroll() {
Grid() {
// 网格内容
}
}
.width('100%')
.height(this.gridSize <= 6 ? 350 : 450)
Column({ space: 8 }) {
Text('游戏说明')
.fontSize(14)
.fontWeight(FontWeight.Bold)
.fontColor('#FFD700')
Text('在一堆相同数字中找到唯一不同的那个')
.fontSize(12)
.fontColor('#87CEEB')
.textAlign(TextAlign.Center)
}
.width('90%')
.padding({ top: 8, bottom: 16 })
}
}
}
.width('100%')
.height('100%')
.backgroundColor('#0A192F')
.padding({ bottom: 80 })
}
布局层次结构:
Column (主容器)
├── Row (顶部导航栏)
│ ├── Button (返回)
│ ├── Text (标题)
│ └── Column (关卡信息)
│ ├── Text (关卡)
│ └── Text (最佳时间)
├── Text (计时器)
└── Column (内容区域)
├── [未开始] Column
│ ├── Text (游戏提示)
│ └── Button (开始游戏)
└── [游戏中] Column
├── Text (游戏提示)
├── Scroll (滚动容器)
│ └── Grid (游戏网格)
└── Column (游戏说明)
6.2 动态网格配置
网格的列数和行数需要根据网格大小动态生成:
private getColumnsTemplate(): string {
let result: string = '';
for (let i = 0; i < this.gridSize; i++) {
result += '1fr';
if (i < this.gridSize - 1) {
result += ' ';
}
}
return result;
}
private getRowsTemplate(): string {
let result: string = '';
for (let i = 0; i < this.gridSize; i++) {
result += '1fr';
if (i < this.gridSize - 1) {
result += ' ';
}
}
return result;
}
模板生成示例:
| 网格大小 | columnsTemplate | rowsTemplate |
|---|---|---|
| 4 | “1fr 1fr 1fr 1fr” | “1fr 1fr 1fr 1fr” |
| 6 | “1fr 1fr 1fr 1fr 1fr 1fr” | “1fr 1fr 1fr 1fr 1fr 1fr” |
| 8 | “1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr” | “1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr” |
| 10 | “1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr” | “1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr 1fr” |
6.3 动态字体大小适配
随着网格增大,每个格子的空间变小,需要动态调整字体大小以确保可读性:
private getFontSize(): number {
if (this.gridSize <= 4) {
return 36;
} else if (this.gridSize <= 6) {
return 28;
} else if (this.gridSize <= 8) {
return 24;
} else {
return 20;
}
}
字体大小适配策略:
| 网格大小 | 字体大小 | 说明 |
|---|---|---|
| 4×4 | 36sp | 16个格子,空间充裕 |
| 6×6 | 28sp | 36个格子,适当缩小 |
| 8×8 | 24sp | 64个格子,继续缩小 |
| 10×10 | 20sp | 100个格子,最小字体 |
这种渐进式的字体调整确保了在任何网格大小下,数字都能清晰显示。
6.4 滚动容器设计
对于较大的网格(8×8和10×10),需要使用Scroll容器来确保所有内容都能被访问:
Scroll() {
Grid() {
// 网格内容
}
.width('98%')
.height(this.gridSize <= 6 ? 350 : 450)
.columnsTemplate(this.getColumnsTemplate())
.rowsTemplate(this.getRowsTemplate())
}
.width('100%')
.height(this.gridSize <= 6 ? 350 : 450)
滚动容器高度适配:
| 网格大小 | 容器高度 | 说明 |
|---|---|---|
| 4×4 | 350px | 较小网格,不需要滚动 |
| 6×6 | 350px | 中等网格,不需要滚动 |
| 8×8 | 450px | 较大网格,需要滚动 |
| 10×10 | 450px | 最大网格,需要滚动 |
6.5 边框样式适配
根据网格大小调整边框宽度和圆角:
.borderWidth(this.gridSize <= 6 ? 1 : 0.5)
.borderRadius(this.gridSize <= 6 ? 8 : 2)
边框样式适配策略:
| 网格大小 | 边框宽度 | 圆角半径 | 说明 |
|---|---|---|---|
| ≤6 | 1px | 8px | 较大格子,边框清晰可见 |
| >6 | 0.5px | 2px | 较小格子,边框细窄不影响视觉 |
七、路由与参数传递
7.1 路由参数定义
interface RouteParams {
level: number;
gridSize: number;
}
7.2 页面跳转
private startLevel(info: LevelInfo): void {
router.pushUrl({
url: 'pages/FindDifferentPage',
params: {
level: info.level,
gridSize: info.gridSize
}
});
}
7.3 参数接收
aboutToAppear(): void {
let params: RouteParams = router.getParams() as RouteParams;
if (params !== undefined) {
if (params.level !== undefined) {
this.currentLevel = params.level;
}
if (params.gridSize !== undefined) {
this.gridSize = params.gridSize;
}
}
let levelTimes: Array<number> = appState.findDifferentLevelTimes;
if (levelTimes.length > this.currentLevel && levelTimes[this.currentLevel] > 0) {
this.bestTime = levelTimes[this.currentLevel];
}
}
参数传递流程:
关卡选择页 → 点击关卡 → 构造参数 → router.pushUrl → 游戏页
↓
aboutToAppear → 解析参数 → 加载最佳时间
八、状态管理与数据持久化
8.1 全局状态管理
游戏进度通过AppState单例进行管理:
findDifferentCompletedLevels: Array<number> = [];
findDifferentLevelTimes: Array<number> = [];
8.2 通关记录更新
let completedLevels: Array<number> = appState.findDifferentCompletedLevels;
let exists: boolean = false;
for (let i = 0; i < completedLevels.length; i++) {
if (completedLevels[i] === this.currentLevel) {
exists = true;
break;
}
}
if (!exists) {
completedLevels.push(this.currentLevel);
completedLevels.sort((a: number, b: number) => a - b);
appState.findDifferentCompletedLevels = completedLevels;
}
注意事项:
由于ArkTS的响应式状态更新机制,直接修改数组元素不会触发UI更新,必须重新赋值。在这个实现中,我们直接在原数组上push元素并排序,然后重新赋值给appState,这是一种可行的方案。
8.3 最佳时间记录
let levelTimes: Array<number> = appState.findDifferentLevelTimes;
while (levelTimes.length <= this.currentLevel) {
levelTimes.push(0);
}
if (levelTimes[this.currentLevel] === 0 || this.timerSeconds < levelTimes[this.currentLevel]) {
levelTimes[this.currentLevel] = this.timerSeconds;
appState.findDifferentLevelTimes = levelTimes;
this.bestTime = this.timerSeconds;
}
时间记录策略:
- 使用数组索引对应关卡编号,便于快速查找
- 0表示未通关或无记录
- 只记录最佳时间(最短用时)
- 同时更新本地bestTime状态,用于UI显示
九、性能优化策略
9.1 数组操作优化
在游戏中,数组操作是频繁发生的,因此需要注意优化:
// 初始化数组
this.numbers = [];
for (let i = 0; i < total - 1; i++) {
this.numbers.push(mainNumber);
}
this.numbers.push(differentNumber);
优化要点:
- 使用push方法逐步添加元素,避免一次性创建大数组
- 在shuffle方法中创建新数组,避免直接修改原数组
9.2 滚动性能优化
对于较大的网格,滚动性能尤为重要:
Scroll() {
Grid() {
ForEach(this.numbers, (num: number) => {
GridItem() {
// 内容
}
})
}
.width('98%')
.height(this.gridSize <= 6 ? 350 : 450)
}
.width('100%')
.height(this.gridSize <= 6 ? 350 : 450)
优化策略:
- 设置固定的Grid高度,避免动态计算
- 使用ForEach进行列表渲染,ArkUI会自动优化渲染性能
- 对于10×10的网格(100个元素),渲染压力较小,无需额外优化
9.3 计时器优化
使用递归setTimeout避免累积误差:
private tick(): void {
if (this.timerId !== 0) {
this.timerSeconds++;
setTimeout(() => {
this.tick();
}, 1000);
}
}
优势:
- setTimeout在回调执行后重新计算下一次延迟
- 避免setInterval因执行时间波动导致的累积误差
- 适合需要高精度计时的场景
十、训练价值与教育意义
10.1 认知训练原理
找不同数字游戏基于以下认知心理学原理:
| 训练维度 | 原理说明 | 游戏机制 |
|---|---|---|
| 视觉注意力 | 通过在相似元素中寻找差异训练视觉辨别能力 | 在大量相同数字中找到不同的一个 |
| 反应速度 | 限时挑战提升信息处理速度 | 计时器显示和记录 |
| 空间扫描 | 训练系统的视觉搜索策略 | 有序地扫描网格区域 |
| 专注力 | 需要高度集中注意力才能找到差异 | 长时间保持专注 |
10.2 不同难度的训练价值
| 难度等级 | 网格大小 | 训练重点 | 适用人群 |
|---|---|---|---|
| 入门 | 4×4 | 基础视觉辨别 | 儿童、初学者 |
| 简单 | 6×6 | 中等难度视觉搜索 | 普通玩家 |
| 中等 | 8×8 | 高效扫描策略 | 进阶玩家 |
| 困难 | 10×10 | 极限专注力 | 高级玩家 |
10.3 科学训练建议
根据认知科学研究,建议以下训练方案:
每周训练次数:3-5次
每次训练时长:10-15分钟
训练间隔:每次训练后休息5分钟
难度递进:从4×4开始,逐步提升到10×10
训练效果评估:
| 评估指标 | 说明 |
|---|---|
| 完成时间 | 记录每关用时,观察是否逐渐缩短 |
| 错误次数 | 统计点击错误的次数(当前版本不记录) |
| 连续通关数 | 一次训练中连续通关的关卡数 |
10.4 实际应用场景
这款游戏在以下场景中具有实际应用价值:
| 场景 | 应用方式 |
|---|---|
| 儿童教育 | 训练儿童的数字识别和视觉注意力 |
| 老年人认知训练 | 延缓认知衰退,保持大脑活跃 |
| 职业训练 | 提升需要高度专注的职业技能(如医生、飞行员) |
| 日常放松 | 快速放松大脑,缓解工作压力 |
十一、代码优化建议
11.1 洗牌算法兼容性
当前代码使用了展开运算符,这在ArkTS中可能不支持:
// 当前实现(可能不兼容)
let result: Array<number> = [...array];
// 推荐实现(兼容ArkTS)
let result: Array<number> = [];
for (let i = 0; i < array.length; i++) {
result.push(array[i]);
}
11.2 计时器实现改进
当前停止计时器使用clearInterval,但实际上应该使用clearTimeout:
private stopTimer(): void {
if (this.timerId !== 0) {
clearTimeout(this.timerId); // 改为clearTimeout
this.timerId = 0;
}
}
11.3 游戏结束提示
当前游戏结束后只是返回开始界面,没有胜利提示。可以添加胜利弹窗:
// 添加胜利状态
@State isGameWon: boolean = false;
// 胜利时设置状态
private clickNumber(num: number): void {
if (num === this.targetNumber) {
this.stopTimer();
// ... 记录进度 ...
this.isGameWon = true;
}
}
// 在build中添加胜利界面
if (this.isGameWon) {
Column() {
Text('恭喜通关!')
.fontSize(36)
.fontColor('#FFD700')
.fontWeight(FontWeight.Bold)
// ... 其他胜利信息 ...
}
}
11.4 错误点击反馈
当前错误点击没有任何反馈,可以添加简单的动画或震动反馈:
// 添加错误状态
@State lastClickWrong: boolean = false;
private clickNumber(num: number): void {
if (num === this.targetNumber) {
// 正确处理
} else {
this.lastClickWrong = true;
setTimeout(() => {
this.lastClickWrong = false;
}, 200);
}
}
十二、总结
通过本文的详细分析,我们深入了解了找不同数字游戏的完整实现过程。从核心算法到UI交互,从状态管理到性能优化,每个环节都体现了HarmonyOS ArkTS开发的最佳实践。
关键技术要点:
- 数字生成策略:生成大部分相同、只有一个不同的数字数组
- Fisher-Yates洗牌:高效的随机打乱算法,保证游戏的公平性
- 视觉一致性:所有数字使用完全相同的视觉样式,消除视觉线索
- 动态适配:根据网格大小自适应调整字体大小、边框样式和容器高度
- 滚动容器:支持较大网格的滚动浏览
- 全局状态管理:AppState单例实现跨页面数据共享
这款游戏不仅是一个娱乐应用,更是一个科学的认知训练工具。通过合理的难度设计和动态适配,玩家可以在游戏中提升视觉注意力、反应速度和专注力。
在未来的迭代中,可以考虑增加更多难度级别(如12×12网格),引入多人对战模式,添加错误点击反馈,以及添加更详细的训练数据分析功能,为用户提供更全面的认知训练体验。
更多推荐

所有评论(0)