源码仓库地址:https://gitcode.com/feng8403000/math_app_study
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述

引言

数字连线游戏是一款经典的注意力训练游戏,玩家需要按照顺序点击打乱的数字或字母。在本文中,我们将深入探讨如何使用HarmonyOS ArkTS技术栈实现这款游戏,包括核心算法、关卡设计、状态管理和UI交互等方面。

一、游戏概述与设计理念

1.1 游戏规则

数字连线游戏的核心玩法非常简单:

  • 游戏界面显示一个N×N的网格,每个格子中显示一个数字、字母或其他符号
  • 这些内容被随机打乱排列
  • 玩家需要按照顺序依次点击对应的内容(如1→2→3…或A→B→C…)
  • 正确点击的格子会高亮显示,错误点击则无反应
  • 完成所有顺序点击后游戏胜利

1.2 设计目标

在设计这款游戏时,我们设定了以下核心目标:

目标类型 具体目标 实现方式
训练价值 提升注意力和反应速度 通过限时挑战和顺序点击机制
难度递进 从简单到困难的渐进式关卡 网格大小4×4→5×5→6×6
内容多样性 支持多种内容类型 数字、字母、中文、罗马数字、符号、混合
用户体验 流畅的交互和视觉反馈 实时计时、点击高亮、胜利提示
进度追踪 记录通关进度和最佳时间 全局状态管理+数据持久化

1.3 技术选型

技术领域 选择方案 理由
UI框架 ArkUI Declarative 声明式UI,组件化开发,响应式状态管理
状态管理 AppState单例模式 全局状态共享,跨页面数据同步
路由导航 @kit.ArkUI router 内置路由能力,参数传递便捷
数据持久化 Preferences 轻量级本地存储,适合游戏进度保存

二、项目架构与组件设计

2.1 页面结构

数字连线游戏包含两个核心页面:

├── NumberConnectLevelPage.ets  # 关卡选择页面
└── NumberConnectPage.ets       # 游戏主页面

页面职责划分:

  • NumberConnectLevelPage:展示关卡列表,支持按难度分类,显示通关时间,提供解锁功能
  • NumberConnectPage:游戏核心逻辑,包括内容生成、点击验证、计时、胜利判定

2.2 数据模型设计

为了支持多样化的关卡配置,我们设计了两个核心数据类:

class LevelInfo {
  level: number = 0;
  gridSize: number = 0;
  content: string = '';
  label: string = '';

  constructor(level: number, gridSize: number, content: string, label: string) {
    this.level = level;
    this.gridSize = gridSize;
    this.content = content;
    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/5/6)
content string 内容类型(numbers/letters/chinese/roman/symbols/mixed)
label string 显示标签(如"116"、"AP")

Category字段说明:

字段 类型 说明
name string 分类名称(如"入门·数字")
description string 分类描述(如"4×4 数字 1~16")
color string 分类颜色标识
levels Array<LevelInfo> 该分类下的关卡列表

2.3 组件状态定义

游戏页面使用了以下状态变量:

@State gridSize: number = 5;
@State currentLevel: number = 1;
@State content: string = 'numbers';
@State label: string = '1~25';
@State isPlaying: boolean = false;
@State items: Array<string> = [];
@State clickedIndices: Array<number> = [];
@State timerSeconds: number = 0;
private timerId: number = 0;

状态变量说明:

变量 类型 说明
gridSize number 当前网格大小
currentLevel number 当前关卡
content string 内容类型
label string 显示标签
isPlaying boolean 是否正在游戏中
items Array<string> 网格中的所有内容项
clickedIndices Array<number> 已正确点击的索引列表
timerSeconds number 计时秒数
timerId number 计时器ID

三、核心游戏逻辑实现

3.1 内容生成算法

游戏支持六种内容类型,每种类型有不同的生成逻辑:

3.1.1 数字生成
private generateItems(): Array<string> {
  let result: Array<string> = [];
  let total: number = this.gridSize * this.gridSize;

  if (this.content === 'numbers') {
    for (let i = 1; i <= total; i++) {
      result.push(i.toString());
    }
  } else if (this.content === 'letters') {
    for (let i = 0; i < total; i++) {
      if (i < 26) {
        result.push(String.fromCharCode(65 + i));
      } else {
        result.push(String.fromCharCode(65 + (i % 26)) + "'");
      }
    }
  } else if (this.content === 'chinese') {
    result = this.generateChineseNumbers(total);
  } else if (this.content === 'roman') {
    for (let i = 1; i <= total; i++) {
      result.push(this.toRoman(i));
    }
  } else if (this.content === 'symbols') {
    let symbols: string = '!@#$%^&*()_+-=[]{}|;:,.<>?~`';
    for (let i = 0; i < total; i++) {
      result.push(symbols[i % symbols.length]);
    }
  } else {
    result = this.generateMixed(total);
  }
  return result;
}
3.1.2 中文数字生成

中文数字生成需要处理特殊的中文数字规则:

private generateChineseNumbers(total: number): Array<string> {
  let result: Array<string> = [];
  let chineseNums: Array<string> = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
  
  for (let i = 1; i <= total; i++) {
    if (i <= 10) {
      result.push(chineseNums[i]);
    } else if (i < 20) {
      result.push('十' + chineseNums[i - 10]);
    } else if (i === 20) {
      result.push('二十');
    } else if (i < 30) {
      result.push('二十' + chineseNums[i - 20]);
    } else if (i === 30) {
      result.push('三十');
    } else {
      result.push('三十' + chineseNums[i - 30]);
    }
  }
  return result;
}

算法说明:

  • 1-10:直接使用中文数字(一、二、三…十)
  • 11-19:使用"十X"格式(十一、十二…十九)
  • 20:特殊处理为"二十"
  • 21-29:使用"二十X"格式
  • 30:特殊处理为"三十"
  • 31-36:使用"三十X"格式
3.1.3 罗马数字生成
private toRoman(num: number): string {
  let result: string = '';
  let values: Array<number> = [100, 90, 50, 40, 10, 9, 5, 4, 1];
  let symbols: Array<string> = ['C', 'XC', 'L', 'XL', 'X', 'IX', 'V', 'IV', 'I'];
  
  for (let i = 0; i < values.length; i++) {
    while (num >= values[i]) {
      result += symbols[i];
      num -= values[i];
    }
  }
  return result;
}

算法原理:

罗马数字采用减法计数法,使用预定义的数值-符号映射表,从大到小依次减去对应数值并拼接符号。

3.1.4 混合内容生成
private generateMixed(total: number): Array<string> {
  let result: Array<string> = [];
  let part1: number = Math.floor(total * 0.3);
  let part2: number = Math.floor(total * 0.3);
  let part3: number = total - part1 - part2;
  
  for (let i = 1; i <= part1; i++) {
    result.push(i.toString());
  }
  
  for (let i = 0; i < part2; i++) {
    result.push(String.fromCharCode(65 + i));
  }
  
  let symbols: string = '!@#$%^&*()_+-=';
  for (let i = 0; i < part3; i++) {
    result.push(symbols[i % symbols.length]);
  }
  
  return result;
}

混合策略:

  • 30% 数字(1, 2, 3…)
  • 30% 字母(A, B, C…)
  • 40% 符号(!, @, #…)

3.2 Fisher-Yates洗牌算法

生成的内容需要随机打乱,这里使用经典的Fisher-Yates算法:

private shuffle(array: Array<string>): Array<string> {
  let result: Array<string> = [];
  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: string = result[i];
    result[i] = result[j];
    result[j] = temp;
  }
  return result;
}

算法分析:

特性 Fisher-Yates洗牌
时间复杂度 O(n)
空间复杂度 O(n)(创建副本)
随机性 均匀随机,每个排列概率相等
原地操作 否(为避免修改原数组,创建了副本)

注意事项:

由于ArkTS中数组是引用类型,直接修改数组元素不会触发UI更新,因此需要创建新数组。这也是我们在shuffle方法中先复制数组的原因。

3.3 点击验证逻辑

点击验证是游戏的核心交互逻辑:

private clickItem(index: number): void {
  let expectedIndex: number = this.clickedIndices.length;
  let expectedItem: string = this.getExpectedItem(expectedIndex);
  let clickedItem: string = this.items[index];
  if (clickedItem === expectedItem) {
    this.clickedIndices.push(index);
    if (this.clickedIndices.length === this.items.length) {
      this.stopTimer();
      this.handleWin();
    }
  }
}

private getExpectedItem(index: number): string {
  let total: number = this.gridSize * this.gridSize;
  if (this.content === 'numbers') {
    return (index + 1).toString();
  } else if (this.content === 'letters') {
    if (index < 26) {
      return String.fromCharCode(65 + index);
    } else {
      return String.fromCharCode(65 + (index % 26)) + "'";
    }
  } else if (this.content === 'chinese') {
    let chineseNums: Array<string> = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
    let num: number = index + 1;
    if (num <= 10) {
      return chineseNums[num];
    } else if (num < 20) {
      return '十' + chineseNums[num - 10];
    } else if (num === 20) {
      return '二十';
    } else if (num < 30) {
      return '二十' + chineseNums[num - 20];
    } else if (num === 30) {
      return '三十';
    } else {
      return '三十' + chineseNums[num - 30];
    }
  } else if (this.content === 'roman') {
    return this.toRoman(index + 1);
  } else if (this.content === 'symbols') {
    let symbols: string = '!@#$%^&*()_+-=[]{}|;:,.<>?~`';
    return symbols[index % symbols.length];
  } else {
    let part1: number = Math.floor(total * 0.3);
    let part2: number = Math.floor(total * 0.3);
    if (index < part1) {
      return (index + 1).toString();
    } else if (index < part1 + part2) {
      return String.fromCharCode(65 + (index - part1));
    } else {
      let symbols: string = '!@#$%^&*()_+-=';
      return symbols[(index - part1 - part2) % symbols.length];
    }
  }
}

验证流程:

用户点击格子 → 获取点击的内容 → 计算期望的下一个内容 → 比较是否匹配
    ↓
匹配成功 → 将索引加入clickedIndices → 检查是否全部完成 → 完成则胜利
    ↓
匹配失败 → 无反应,等待下一次点击

3.4 胜利处理与进度更新

private handleWin(): void {
  let completed: Array<number> = appState.numberConnectCompletedLevels;
  let alreadyCompleted: boolean = false;
  for (let i = 0; i < completed.length; i++) {
    if (completed[i] === this.currentLevel) {
      alreadyCompleted = true;
      break;
    }
  }
  if (!alreadyCompleted) {
    let newCompleted: Array<number> = [];
    for (let i = 0; i < completed.length; i++) {
      newCompleted.push(completed[i]);
    }
    newCompleted.push(this.currentLevel);
    appState.numberConnectCompletedLevels = newCompleted;
  }

  let levelTimes: Array<number> = appState.numberConnectLevelTimes;
  while (levelTimes.length <= this.currentLevel) {
    levelTimes.push(0);
  }
  if (levelTimes[this.currentLevel] === 0 || this.timerSeconds < levelTimes[this.currentLevel]) {
    levelTimes[this.currentLevel] = this.timerSeconds;
    appState.numberConnectLevelTimes = levelTimes;
  }

  if (this.currentLevel < 50) {
    let nextLevel: number = this.currentLevel + 1;
    this.currentLevel = nextLevel;
    this.updateLevelConfig(nextLevel);
    setTimeout(() => {
      this.startGame();
    }, 500);
  } else {
    this.isPlaying = false;
  }
}

胜利处理步骤:

  1. 检查是否首次通关:遍历已完成关卡数组,判断当前关卡是否已通关
  2. 记录通关进度:如果是首次通关,将关卡编号加入已完成数组
  3. 更新最佳时间:比较当前用时与历史最佳时间,更新更优的记录
  4. 进入下一关:如果未达到最后一关,自动配置下一关参数并开始游戏

四、关卡系统设计

4.1 关卡分类体系

游戏采用四级难度递进体系,共50个关卡:

难度等级 关卡范围 网格大小 内容类型 颜色标识
入门 1-10 4×4 数字/字母 绿色 #32CD32
简单 11-25 5×5 数字/字母/中文 青色 #00CED1
中等 26-40 6×6 数字/字母/罗马 金色 #FFD700
困难 41-50 6×6 符号/混合 橙色/红色

4.2 关卡初始化

private initCategories(): void {
  let cat1: Category = new Category('入门·数字', '4×4 数字 1~16', '#32CD32');
  for (let i = 1; i <= 5; i++) {
    cat1.levels.push(new LevelInfo(i, 4, 'numbers', '1~16'));
  }

  let cat2: Category = new Category('入门·字母', '4×4 字母 A~P', '#32CD32');
  for (let i = 6; i <= 10; i++) {
    cat2.levels.push(new LevelInfo(i, 4, 'letters', 'A~P'));
  }

  let cat3: Category = new Category('简单·数字', '5×5 数字 1~25', '#00CED1');
  for (let i = 11; i <= 15; i++) {
    cat3.levels.push(new LevelInfo(i, 5, 'numbers', '1~25'));
  }

  let cat4: Category = new Category('简单·字母', '5×5 字母 A~Y', '#00CED1');
  for (let i = 16; i <= 20; i++) {
    cat4.levels.push(new LevelInfo(i, 5, 'letters', 'A~Y'));
  }

  let cat5: Category = new Category('简单·中文', '5×5 中文数字', '#00CED1');
  for (let i = 21; i <= 25; i++) {
    cat5.levels.push(new LevelInfo(i, 5, 'chinese', '一~二十五'));
  }

  let cat6: Category = new Category('中等·数字', '6×6 数字 1~36', '#FFD700');
  for (let i = 26; i <= 30; i++) {
    cat6.levels.push(new LevelInfo(i, 6, 'numbers', '1~36'));
  }

  let cat7: Category = new Category('中等·字母', '6×6 字母 A~Z+', '#FFD700');
  for (let i = 31; i <= 35; i++) {
    cat7.levels.push(new LevelInfo(i, 6, 'letters', 'A~Z+'));
  }

  let cat8: Category = new Category('中等·罗马', '6×6 罗马数字', '#FFD700');
  for (let i = 36; i <= 40; i++) {
    cat8.levels.push(new LevelInfo(i, 6, 'roman', 'I~XXXVI'));
  }

  let cat9: Category = new Category('困难·符号', '6×6 符号', '#FFA500');
  for (let i = 41; i <= 45; i++) {
    cat9.levels.push(new LevelInfo(i, 6, 'symbols', '符号'));
  }

  let cat10: Category = new Category('困难·混合', '6×6 混合内容', '#FF6347');
  for (let i = 46; i <= 50; i++) {
    cat10.levels.push(new LevelInfo(i, 6, 'mixed', '混合'));
  }

  this.categories = [cat1, cat2, cat3, cat4, cat5, cat6, cat7, cat8, cat9, cat10];
}

4.3 关卡配置动态更新

当玩家完成一关后,系统自动配置下一关参数:

private updateLevelConfig(level: number): void {
  if (level <= 5) {
    this.gridSize = 4;
    this.content = 'numbers';
  } else if (level <= 10) {
    this.gridSize = 4;
    this.content = 'letters';
  } else if (level <= 15) {
    this.gridSize = 5;
    this.content = 'numbers';
  } else if (level <= 20) {
    this.gridSize = 5;
    this.content = 'letters';
  } else if (level <= 25) {
    this.gridSize = 5;
    this.content = 'chinese';
  } else if (level <= 30) {
    this.gridSize = 6;
    this.content = 'numbers';
  } else if (level <= 35) {
    this.gridSize = 6;
    this.content = 'letters';
  } else if (level <= 40) {
    this.gridSize = 6;
    this.content = 'roman';
  } else if (level <= 45) {
    this.gridSize = 6;
    this.content = 'symbols';
  } else {
    this.gridSize = 6;
    this.content = 'mixed';
  }
  this.updateLabel();
}

难度递进设计思路:

  • 阶段1(1-10关):熟悉游戏规则,使用4×4网格,数字和字母内容
  • 阶段2(11-25关):增加难度,使用5×5网格,引入中文数字
  • 阶段3(26-40关):中等难度,使用6×6网格,引入罗马数字
  • 阶段4(41-50关):困难挑战,使用6×6网格,符号和混合内容

五、计时系统实现

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')}`;
}

格式输出示例:

秒数 格式化结果
5 00:05
65 01:05
120 02:00
3600 60:00

六、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')

      Text('第' + this.currentLevel + '关')
        .fontSize(14)
        .fontColor('#87CEEB')
        .width(60)
    }
    .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 }) {
        // 难度选择按钮
        // 内容类型选择按钮
        // 开始游戏按钮
      }
    } else {
      // 游戏进行界面
      Column() {
        // 游戏提示文字
        // 网格区域
        // 游戏说明
      }
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#0A192F')
  .padding({ bottom: 80 })
}

布局层次结构:

Column (主容器)
├── Row (顶部导航栏)
│   ├── Button (返回)
│   ├── Text (标题)
│   └── Text (关卡)
├── Text (计时器)
└── Column (内容区域)
    ├── [未开始] Column
    │   ├── Text (选择难度)
    │   ├── Row (网格大小选择)
    │   ├── Row (内容类型选择)
    │   └── Button (开始游戏)
    └── [游戏中] Column
        ├── Text (游戏提示)
        ├── Grid (游戏网格)
        └── Column (游戏说明)

6.2 游戏网格动态生成

Grid() {
  ForEach(this.items, (item: string, index: number) => {
    GridItem() {
      Button(item)
        .fontSize(this.getFontSize())
        .fontColor(this.clickedIndices.indexOf(index) >= 0 ? '#87CEEB' : '#FFFFFF')
        .backgroundColor(this.clickedIndices.indexOf(index) >= 0 ? 'rgba(135, 206, 235, 0.2)' : 'rgba(255, 215, 0, 0.1)')
        .borderColor('#FFD700')
        .borderWidth(1)
        .borderRadius(6)
        .width('100%')
        .height('100%')
        .onClick(() => this.clickItem(index))
    }
  })
}
.width('95%')
.aspectRatio(1)
.columnsTemplate(this.getColumnsTemplate())
.rowsTemplate(this.getRowsTemplate())

网格配置方法:

private getFontSize(): number {
  if (this.gridSize === 4) return 32;
  if (this.gridSize === 5) return 26;
  return 20;
}

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;
}

字体大小适配策略:

网格大小 字体大小 说明
4×4 32sp 16个格子,空间充裕
5×5 26sp 25个格子,适当缩小
6×6 20sp 36个格子,最小字体

6.3 视觉反馈设计

游戏通过颜色变化提供清晰的视觉反馈:

状态 字体颜色 背景颜色 说明
未点击 #FFFFFF (白色) rgba(255, 215, 0, 0.1) 默认状态
已点击 #87CEEB (天蓝色) rgba(135, 206, 235, 0.2) 正确点击后高亮

这种设计让玩家能够直观地看到自己的进度,同时保持视觉一致性。

七、路由与参数传递

7.1 路由参数定义

interface RouteParams {
  level: number;
  gridSize: number;
  content: string;
  label: string;
}

7.2 页面跳转

private startLevel(info: LevelInfo): void {
  router.pushUrl({ 
    url: 'pages/NumberConnectPage', 
    params: { 
      level: info.level, 
      gridSize: info.gridSize, 
      content: info.content, 
      label: info.label 
    } 
  });
}

7.3 参数接收

onPageShow(): void {
  let params: Object = router.getParams();
  if (params !== null && params !== undefined) {
    let routeParams: RouteParams = params as RouteParams;
    if (routeParams.level !== undefined) {
      this.currentLevel = routeParams.level;
    }
    if (routeParams.gridSize !== undefined) {
      this.gridSize = routeParams.gridSize;
    }
    if (routeParams.content !== undefined) {
      this.content = routeParams.content;
    }
    if (routeParams.label !== undefined) {
      this.label = routeParams.label;
      this.startGame();
    }
  }
}

参数传递流程:

关卡选择页 → 点击关卡 → 构造参数 → router.pushUrl → 游戏页
                                                       ↓
                                              onPageShow → 解析参数 → 初始化游戏

八、状态管理与数据持久化

8.1 全局状态管理

游戏进度通过AppState单例进行管理:

numberConnectCompletedLevels: Array<number> = [];
numberConnectLevelTimes: Array<number> = [];

8.2 通关记录更新

let completed: Array<number> = appState.numberConnectCompletedLevels;
let alreadyCompleted: boolean = false;
for (let i = 0; i < completed.length; i++) {
  if (completed[i] === this.currentLevel) {
    alreadyCompleted = true;
    break;
  }
}
if (!alreadyCompleted) {
  let newCompleted: Array<number> = [];
  for (let i = 0; i < completed.length; i++) {
    newCompleted.push(completed[i]);
  }
  newCompleted.push(this.currentLevel);
  appState.numberConnectCompletedLevels = newCompleted;
}

注意事项:

由于ArkTS的响应式状态更新机制,直接修改数组元素不会触发UI更新,必须创建新数组并重新赋值。这也是为什么我们需要复制数组内容到新数组后再添加元素。

8.3 最佳时间记录

let levelTimes: Array<number> = appState.numberConnectLevelTimes;
while (levelTimes.length <= this.currentLevel) {
  levelTimes.push(0);
}
if (levelTimes[this.currentLevel] === 0 || this.timerSeconds < levelTimes[this.currentLevel]) {
  levelTimes[this.currentLevel] = this.timerSeconds;
  appState.numberConnectLevelTimes = levelTimes;
}

时间记录策略:

  • 使用数组索引对应关卡编号,便于快速查找
  • 0表示未通关或无记录
  • 只记录最佳时间(最短用时)

九、性能优化策略

9.1 数组操作优化

在游戏中,数组操作是频繁发生的,因此需要注意优化:

// 避免方案:直接修改数组元素
// this.items[index] = newValue; // 不会触发UI更新

// 推荐方案:创建新数组
let newItems: Array<string> = [];
for (let i = 0; i < this.items.length; i++) {
  newItems.push(this.items[i]);
}
newItems[index] = newValue;
this.items = newItems; // 触发UI更新

9.2 点击验证优化

点击验证需要检查索引是否在已点击列表中,使用indexOf方法:

this.clickedIndices.indexOf(index) >= 0

时间复杂度分析:

  • indexOf方法的时间复杂度为O(n)
  • 对于6×6网格(36个格子),最坏情况下需要遍历36次
  • 由于网格大小有限(最大36),这种方法在当前场景下是可接受的

9.3 计时器优化

使用递归setTimeout避免累积误差:

private tick(): void {
  if (this.timerId !== 0) {
    this.timerSeconds++;
    setTimeout(() => {
      this.tick();
    }, 1000);
  }
}

优势:

  • setTimeout在回调执行后重新计算下一次延迟
  • 避免setInterval因执行时间波动导致的累积误差
  • 适合需要高精度计时的场景

十、训练价值与教育意义

10.1 认知训练原理

数字连线游戏基于以下认知心理学原理:

训练维度 原理说明 游戏机制
注意力集中 通过顺序查找训练视觉注意力 按顺序点击数字/字母
反应速度 限时挑战提升信息处理速度 计时器显示和记录
工作记忆 记住当前应点击的内容 追踪点击顺序
视觉搜索 在复杂场景中快速定位目标 打乱的网格布局

10.2 不同内容类型的训练价值

内容类型 训练重点 适用人群
数字 数字识别能力 儿童、数学初学者
字母 字母表顺序记忆 英语学习者
中文 中文数字认知 中文母语者
罗马数字 特殊符号识别 进阶玩家
符号 抽象符号记忆 高级玩家
混合 综合认知能力 所有玩家

10.3 科学训练建议

根据认知科学研究,建议以下训练方案:

每周训练次数:3-5次
每次训练时长:15-20分钟
训练间隔:每次训练后休息5-10分钟
难度递进:从4×4数字开始,逐步提升

训练效果评估:

评估指标 说明
完成时间 记录每关用时,观察是否逐渐缩短
错误次数 统计点击错误的次数(当前版本不记录)
连续通关数 一次训练中连续通关的关卡数

十一、代码优化建议

11.1 代码冗余问题

当前代码中,generateChineseNumbersgetExpectedItem 中的中文数字生成逻辑重复,可以提取为公共方法:

private getChineseNumber(num: number): string {
  let chineseNums: Array<string> = ['零', '一', '二', '三', '四', '五', '六', '七', '八', '九', '十'];
  
  if (num <= 10) {
    return chineseNums[num];
  } else if (num < 20) {
    return '十' + chineseNums[num - 10];
  } else if (num === 20) {
    return '二十';
  } else if (num < 30) {
    return '二十' + chineseNums[num - 20];
  } else if (num === 30) {
    return '三十';
  } else {
    return '三十' + chineseNums[num - 30];
  }
}

11.2 关卡配置数据化

当前关卡配置是硬编码的,可以提取为配置文件或JSON数据:

// 优化后的配置方式
private levelConfigs: Array<LevelConfig> = [
  { level: 1, gridSize: 4, content: 'numbers', label: '1~16' },
  { level: 2, gridSize: 4, content: 'numbers', label: '1~16' },
  // ... 更多配置
];

11.3 计时器实现改进

当前停止计时器使用clearInterval,但实际上应该使用clearTimeout:

private stopTimer(): void {
  if (this.timerId !== 0) {
    clearTimeout(this.timerId); // 改为clearTimeout
    this.timerId = 0;
  }
}

十二、总结

通过本文的详细分析,我们深入了解了数字连线游戏的完整实现过程。从核心算法到UI交互,从状态管理到性能优化,每个环节都体现了HarmonyOS ArkTS开发的最佳实践。

关键技术要点:

  1. 内容生成多样性:支持6种内容类型,每种类型有独立的生成算法
  2. Fisher-Yates洗牌:高效的随机打乱算法,保证游戏的公平性
  3. 状态驱动UI:通过@State装饰器实现响应式界面更新
  4. 渐进式难度设计:从4×4到6×6,从数字到混合内容,逐步提升挑战
  5. 全局状态管理:AppState单例实现跨页面数据共享
  6. 视觉反馈机制:颜色变化提供清晰的交互反馈

这款游戏不仅是一个娱乐应用,更是一个科学的认知训练工具。通过合理的难度设计和多样化的内容类型,玩家可以在游戏中提升注意力、反应速度和记忆力。

在未来的迭代中,可以考虑增加更多内容类型(如表情符号、形状等),引入多人对战模式,以及添加更详细的训练数据分析功能,为用户提供更全面的认知训练体验。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐