在这里插入图片描述
在这里插入图片描述

一、游戏概述

舒尔特竞速是一款经典的注意力训练游戏,玩家需要按照顺序快速点击从1开始的所有数字。本项目基于HarmonyOS ArkTS技术栈实现,采用深蓝色背景配金色主题的视觉设计,包含20个难度递进的关卡。

1.1 游戏规则

  • 游戏开始时,数字1到n随机排列在网格中
  • 玩家需要按顺序点击数字1、2、3……直到所有数字都被点击
  • 点击正确的数字后,该数字会变成天蓝色标记
  • 完成所有数字的点击即可通关
  • 游戏记录通关时间,追求最快速度

1.2 关卡设计

难度等级 网格大小 数字范围 关卡范围 目标时间
入门 4×4 1-16 1-5 30秒
简单 5×5 1-25 6-10 45秒
中等 6×6 1-36 11-15 60秒
困难 7×7 1-49 16-20 90秒

1.3 技术架构

┌─────────────────────────────────────────────────────────────┐
│                    舒尔特竞速游戏架构                        │
├─────────────────────────────────────────────────────────────┤
│                                                             │
│  ┌──────────────────┐      ┌──────────────────┐            │
│  │ SchulteRace      │──────▶│ SchulteRace      │            │
│  │ LevelPage.ets    │      │ Page.ets         │            │
│  │ (关卡选择页面)    │      │ (游戏主页面)      │            │
│  └──────────────────┘      └──────────┬────────┘            │
│                                       │                     │
│                                       ▼                     │
│  ┌────────────────────────────────────────────────────────┐ │
│  │                    AppState.ets                        │ │
│  │  - schulteRaceCompletedLevels: Array<number>           │ │
│  │  - schulteRaceLevelTimes: Array<number>                │ │
│  └────────────────────────────────────────────────────────┘ │
│                                                             │
└─────────────────────────────────────────────────────────────┘

二、关卡选择页面实现

2.1 页面结构

关卡选择页面采用标准的分类展示模式,将20个关卡分为4个难度类别,每个类别展示5个关卡。页面顶部包含返回按钮、标题和解锁按钮,中间区域展示分类关卡网格,底部预留60px的背景覆盖区域。

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类封装了关卡的核心信息:关卡编号、网格大小、显示标签
  • Category类组织了难度分类:名称、描述、主题色和所属关卡列表
  • 通过面向对象的方式管理关卡数据,便于扩展和维护

2.3 分类初始化逻辑

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('简单', '5×5 方格', '#00CED1');
  for (let i = 6; i <= 10; i++) {
    cat2.levels.push(new LevelInfo(i, 5, '5×5'));
  }

  let cat3: Category = new Category('中等', '6×6 方格', '#FFD700');
  for (let i = 11; i <= 15; i++) {
    cat3.levels.push(new LevelInfo(i, 6, '6×6'));
  }

  let cat4: Category = new Category('困难', '7×7 方格', '#FF6347');
  for (let i = 16; i <= 20; i++) {
    cat4.levels.push(new LevelInfo(i, 7, '7×7'));
  }

  this.categories = [cat1, cat2, cat3, cat4];
}

设计要点

  • 每个难度类别的网格大小递增:4×4 → 5×5 → 6×6 → 7×7
  • 主题色根据难度递增变化:绿色→青色→金色→红色,直观区分难度
  • 使用循环批量创建关卡信息,减少重复代码

2.4 关卡状态判断

private isLevelCompleted(level: number): boolean {
  for (let i = 0; i < this.completedLevels.length; i++) {
    if (completedLevels[i] === level) {
      return true;
    }
  }
  return false;
}

private getLevelTime(level: number): string {
  if (level >= this.levelTimes.length) {
    return '--:--';
  }
  let time: number = this.levelTimes[level];
  if (time === 0) {
    return '--:--';
  }
  return this.formatTime(time);
}

实现说明

  • isLevelCompleted通过遍历已完成关卡数组判断关卡状态
  • getLevelTime根据关卡索引获取最佳时间,未通关显示"–:–"
  • 时间格式化为"MM:SS"格式,便于用户阅读

2.5 页面生命周期管理

onPageShow(): void {
  this.completedLevels = appState.schulteRaceCompletedLevels;
  this.levelTimes = appState.schulteRaceLevelTimes;
  this.initCategories();
}

关键逻辑

  • 在页面显示时从全局状态获取最新的通关进度和最佳时间
  • 重新初始化分类数据,确保显示最新的关卡状态

三、游戏主页面实现

3.1 状态管理

@Entry
@Component
struct SchulteRacePage {
  @State gridSize: number = 5;
  @State currentLevel: number = 1;
  @State isPlaying: boolean = false;
  @State timerSeconds: number = 0;
  @State numbers: Array<number> = [];
  @State clickedNumbers: Array<number> = [];
  @State bestTime: number = 0;
  private timerId: number = 0;

状态变量说明

变量名 类型 作用
gridSize number 网格大小
currentLevel number 当前关卡编号
isPlaying boolean 游戏是否进行中
timerSeconds number 计时秒数
numbers Array<number> 随机排列的数字数组
clickedNumbers Array<number> 已点击的数字数组
bestTime number 最佳通关时间
timerId number 计时器标识

3.2 路由参数接收

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.schulteRaceLevelTimes;
  if (levelTimes.length > this.currentLevel && levelTimes[this.currentLevel] > 0) {
    this.bestTime = levelTimes[this.currentLevel];
  }
}

实现细节

  • 通过router.getParams()获取路由传递的关卡参数
  • 从全局状态获取该关卡的最佳时间,用于显示和比较

3.3 游戏开始逻辑

private startGame(): void {
  this.isPlaying = true;
  this.timerSeconds = 0;
  this.clickedNumbers = [];
  this.numbers = [];
  let total: number = this.gridSize * this.gridSize;
  for (let i = 1; i <= total; i++) {
    this.numbers.push(i);
  }
  this.numbers = this.shuffle(this.numbers);
  this.startTimer();
}

游戏初始化流程

  1. 设置游戏状态为进行中
  2. 重置计时器和已点击数字数组
  3. 生成数字序列1到n(n = gridSize × gridSize)
  4. 对数字数组进行随机打乱(Fisher-Yates洗牌算法)
  5. 启动计时器

3.4 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)(如果创建新数组)。

算法步骤

  1. 创建原始数组的副本,避免修改原数组
  2. 从数组末尾开始,向前遍历
  3. 对于每个位置i,生成一个0到i的随机索引j
  4. 交换位置i和位置j的元素
  5. 继续向前遍历,直到处理完所有元素

为什么选择Fisher-Yates算法

  • 均匀分布:每个元素出现在每个位置的概率相等
  • 时间效率:O(n)的时间复杂度,效率最高
  • 实现简单:代码简洁,易于理解和维护

3.5 点击数字逻辑

private clickNumber(num: number): void {
  let expected: number = this.clickedNumbers.length + 1;
  if (num === expected) {
    this.clickedNumbers.push(num);
    if (this.clickedNumbers.length === this.gridSize * this.gridSize) {
      this.stopTimer();
      let completedLevels: Array<number> = appState.schulteRaceCompletedLevels;
      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.schulteRaceCompletedLevels = completedLevels;
      }
      let levelTimes: Array<number> = appState.schulteRaceLevelTimes;
      while (levelTimes.length <= this.currentLevel) {
        levelTimes.push(0);
      }
      if (levelTimes[this.currentLevel] === 0 || this.timerSeconds < levelTimes[this.currentLevel]) {
        levelTimes[this.currentLevel] = this.timerSeconds;
        appState.schulteRaceLevelTimes = levelTimes;
        this.bestTime = this.timerSeconds;
      }
      this.isPlaying = false;
    }
  }
}

点击验证逻辑

  1. 计算期望的下一个数字:clickedNumbers.length + 1
  2. 如果点击的数字等于期望数字,则添加到已点击数组
  3. 检查是否完成所有数字的点击
  4. 如果完成,停止计时器并更新通关进度和最佳时间

通关处理流程

  1. 停止计时器
  2. 检查当前关卡是否已完成,未完成则添加到完成列表
  3. 更新最佳时间记录(如果当前时间更短)
  4. 设置isPlaying为false,返回开始界面

3.6 单元格颜色系统

.fontColor(this.clickedNumbers.indexOf(num) >= 0 ? '#87CEEB' : '#FFFFFF')
.backgroundColor(this.clickedNumbers.indexOf(num) >= 0 ? 'rgba(135, 206, 235, 0.2)' : 'rgba(255, 215, 0, 0.1)')

颜色设计规则

状态 字体颜色 背景颜色 设计意图
已点击 天蓝色(#87CEEB) 天蓝色半透明(0.2) 标记已完成的数字
未点击 白色(#FFFFFF) 金色半透明(0.1) 常规显示未点击的数字

3.7 动态字体大小

.fontSize(this.gridSize === 5 ? 24 : (this.gridSize === 7 ? 18 : 14))

字体大小策略

网格大小 字体大小 设计原因
4×4 24sp 格子较大,使用较大字体
5×5 24sp 标准字体大小
6×6 14sp 格子变小,使用较小字体
7×7 18sp 格子更小,使用更小字体

3.8 计时器实现

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

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

实现说明

  • 使用递归setTimeout实现计时器,每秒递增timerSeconds
  • stopTimer通过设置timerId为0停止计时
  • 时间格式化为"MM:SS"格式,使用padStart确保两位数显示

3.9 网格动态布局

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

动态生成模板

  • 根据gridSize动态生成columnsTemplaterowsTemplate
  • 例如5×5网格生成"1fr 1fr 1fr 1fr 1fr",确保网格均分显示

四、UI界面设计

4.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(this.gridSize + '×' + this.gridSize + ' 舒尔特竞速')
          .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() {
        Grid() {
          ForEach(this.numbers, (num: number) => {
            GridItem() {
              Button(num.toString())
                .fontSize(this.gridSize === 5 ? 24 : (this.gridSize === 7 ? 18 : 14))
                .fontColor(this.clickedNumbers.indexOf(num) >= 0 ? '#87CEEB' : '#FFFFFF')
                .backgroundColor(this.clickedNumbers.indexOf(num) >= 0 ? 'rgba(135, 206, 235, 0.2)' : 'rgba(255, 215, 0, 0.1)')
                .borderColor('#FFD700')
                .borderWidth(1)
                .borderRadius(8)
                .width('100%')
                .height('100%')
                .onClick(() => this.clickNumber(num))
            }
          })
        }
        .width('90%')
        .aspectRatio(1)
        .columnsTemplate(this.getColumnsTemplate())
        .rowsTemplate(this.getRowsTemplate())

        Column({ space: 8 }) {
          Text('游戏说明')
            .fontSize(14)
            .fontWeight(FontWeight.Bold)
            .fontColor('#FFD700')
          Text('按顺序点击 1 ~ ' + (this.gridSize * this.gridSize))
            .fontSize(12)
            .fontColor('#87CEEB')
            .textAlign(TextAlign.Center)
        }
        .width('90%')
        .padding({ top: 8, bottom: 16 })
      }
    }
  }
  .width('100%')
  .height('100%')
  .backgroundColor('#0A192F')
  .padding({ bottom: 80 })
}

4.2 双态界面设计

游戏页面包含两种状态:

状态一:未开始游戏

  • 显示关卡信息和网格大小
  • 显示"开始游戏"按钮

状态二:游戏进行中

  • 显示实时计时器
  • 显示网格游戏区域
  • 已点击数字显示天蓝色标记
  • 显示游戏说明

4.3 视觉设计规范

  • 背景色:深蓝色#0A192F
  • 主题色:金色#FFD700用于标题、按钮和高亮元素
  • 辅助色:天蓝色#87CEEB用于提示文字和已点击数字标记
  • 边框:金色边框,圆角设计
  • 按钮:金色背景配深蓝色文字,圆角12px

五、全局状态集成

5.1 数据持久化

舒尔特竞速游戏通过AppState单例管理全局状态:

// AppState.ets中定义
schulteRaceCompletedLevels: Array<number> = [];
schulteRaceLevelTimes: Array<number> = [];

5.2 进度更新流程

// 通关时更新进度
let completedLevels: Array<number> = appState.schulteRaceCompletedLevels;
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.schulteRaceCompletedLevels = completedLevels;
}

设计要点

  • 使用数组存储已完成的关卡编号
  • 通过重新赋值数组触发UI响应式更新
  • 保持数组有序,便于查找和显示

六、难度递进设计

6.1 难度参数

难度 网格大小 数字数量 预期通关时间
入门 4×4 16 20-30秒
简单 5×5 25 30-45秒
中等 6×6 36 45-60秒
困难 7×7 49 60-90秒

6.2 难度递进策略

  1. 网格大小递增:从4×4到7×7,数字数量从16增加到49,增加3倍
  2. 数字密度增加:更大的网格意味着更多的数字需要识别和点击
  3. 时间压力增大:数字越多,需要的时间越长,对反应速度要求更高
  4. 注意力要求提高:更多的数字意味着需要更强的视觉搜索能力

6.3 策略分析

  • 入门难度(4×4):数字少,布局稀疏,适合新手熟悉规则
  • 简单难度(5×5):标准舒尔特方格大小,游戏体验最佳
  • 中等难度(6×6):数字增多,需要更快的反应和搜索能力
  • 困难难度(7×7):挑战极限,需要极高的注意力和反应速度

七、舒尔特方格训练原理

7.1 舒尔特方格的起源

舒尔特方格是由德国神经科医生舒尔特发明的一种注意力训练工具,最初用于帮助飞行员提高注意力和反应速度。

7.2 训练效果

  • 视觉搜索能力:快速在杂乱的数字中找到目标数字
  • 注意力集中:在规定时间内保持高度专注
  • 反应速度:减少识别和点击的时间
  • 手眼协调:视觉识别与手指操作的协调配合

7.3 科学依据

舒尔特方格训练基于认知心理学中的"视觉搜索"理论:

  1. 并行搜索:大脑可以同时处理多个视觉信息
  2. 特征整合:将分散的数字特征整合成完整的数字识别
  3. 注意力分配:在多个目标中分配有限的注意力资源

八、性能优化考虑

8.1 响应式更新

ArkTS中数组的直接修改不会触发UI更新,必须创建新数组引用:

// 正确方式:创建新数组
completedLevels.push(this.currentLevel);
completedLevels.sort((a: number, b: number) => a - b);
appState.schulteRaceCompletedLevels = completedLevels;

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

8.3 网格渲染优化

  • 使用aspectRatio(1)确保网格为正方形
  • 根据网格大小动态调整字体大小
  • 使用width('100%')height('100%')确保按钮填满格子

8.4 计时器优化

使用递归setTimeout而非setInterval,避免累积误差:

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

九、扩展功能建议

9.1 游戏增强功能

  1. 音效反馈:点击正确/错误数字时播放不同音效
  2. 连击奖励:连续正确点击获得额外奖励
  3. 计时精度:毫秒级计时,更精确的成绩记录
  4. 训练模式:不限时模式,专注训练注意力
  5. 排行榜:本地排行榜,记录历史最佳成绩

9.2 技术优化方向

  1. 算法优化:实现更高效的数字查找和验证逻辑
  2. 动画效果:添加点击动画和过渡效果
  3. 数据统计:记录玩家的训练数据和进步曲线
  4. 多模式训练:支持字母、符号等多种模式

十、总结

舒尔特竞速游戏是一款经典的注意力训练游戏,本项目基于HarmonyOS ArkTS技术栈实现了完整的游戏功能:

  1. 核心玩法:按顺序快速点击数字1到n
  2. 随机排列:使用Fisher-Yates算法确保均匀随机分布
  3. 关卡系统:20个难度递进的关卡,从4×4到7×7网格
  4. 进度管理:全局状态管理通关进度和最佳时间
  5. 视觉设计:深蓝色背景配金色主题,已点击数字天蓝色标记
  6. 用户体验:实时计时、最佳时间记录、简洁界面

游戏的核心难点在于:

  • 实现高效的随机打乱算法
  • 设计合理的难度递进曲线
  • 确保流畅的游戏体验和响应速度

通过本项目的实践,可以深入理解ArkTS的状态管理机制、UI组件的动态布局、以及游戏逻辑的实现方法。同时,舒尔特竞速游戏也体现了游戏化学习的理念,通过趣味性的方式训练注意力和反应能力。

Logo

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

更多推荐