HarmonyOS ArkTS舒尔特竞速游戏实现技术


一、游戏概述
舒尔特竞速是一款经典的注意力训练游戏,玩家需要按照顺序快速点击从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到n(n = gridSize × gridSize)
- 对数字数组进行随机打乱(Fisher-Yates洗牌算法)
- 启动计时器
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)(如果创建新数组)。
算法步骤:
- 创建原始数组的副本,避免修改原数组
- 从数组末尾开始,向前遍历
- 对于每个位置i,生成一个0到i的随机索引j
- 交换位置i和位置j的元素
- 继续向前遍历,直到处理完所有元素
为什么选择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;
}
}
}
点击验证逻辑:
- 计算期望的下一个数字:
clickedNumbers.length + 1 - 如果点击的数字等于期望数字,则添加到已点击数组
- 检查是否完成所有数字的点击
- 如果完成,停止计时器并更新通关进度和最佳时间
通关处理流程:
- 停止计时器
- 检查当前关卡是否已完成,未完成则添加到完成列表
- 更新最佳时间记录(如果当前时间更短)
- 设置
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动态生成columnsTemplate和rowsTemplate - 例如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 难度递进策略
- 网格大小递增:从4×4到7×7,数字数量从16增加到49,增加3倍
- 数字密度增加:更大的网格意味着更多的数字需要识别和点击
- 时间压力增大:数字越多,需要的时间越长,对反应速度要求更高
- 注意力要求提高:更多的数字意味着需要更强的视觉搜索能力
6.3 策略分析
- 入门难度(4×4):数字少,布局稀疏,适合新手熟悉规则
- 简单难度(5×5):标准舒尔特方格大小,游戏体验最佳
- 中等难度(6×6):数字增多,需要更快的反应和搜索能力
- 困难难度(7×7):挑战极限,需要极高的注意力和反应速度
七、舒尔特方格训练原理
7.1 舒尔特方格的起源
舒尔特方格是由德国神经科医生舒尔特发明的一种注意力训练工具,最初用于帮助飞行员提高注意力和反应速度。
7.2 训练效果
- 视觉搜索能力:快速在杂乱的数字中找到目标数字
- 注意力集中:在规定时间内保持高度专注
- 反应速度:减少识别和点击的时间
- 手眼协调:视觉识别与手指操作的协调配合
7.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 游戏增强功能
- 音效反馈:点击正确/错误数字时播放不同音效
- 连击奖励:连续正确点击获得额外奖励
- 计时精度:毫秒级计时,更精确的成绩记录
- 训练模式:不限时模式,专注训练注意力
- 排行榜:本地排行榜,记录历史最佳成绩
9.2 技术优化方向
- 算法优化:实现更高效的数字查找和验证逻辑
- 动画效果:添加点击动画和过渡效果
- 数据统计:记录玩家的训练数据和进步曲线
- 多模式训练:支持字母、符号等多种模式
十、总结
舒尔特竞速游戏是一款经典的注意力训练游戏,本项目基于HarmonyOS ArkTS技术栈实现了完整的游戏功能:
- 核心玩法:按顺序快速点击数字1到n
- 随机排列:使用Fisher-Yates算法确保均匀随机分布
- 关卡系统:20个难度递进的关卡,从4×4到7×7网格
- 进度管理:全局状态管理通关进度和最佳时间
- 视觉设计:深蓝色背景配金色主题,已点击数字天蓝色标记
- 用户体验:实时计时、最佳时间记录、简洁界面
游戏的核心难点在于:
- 实现高效的随机打乱算法
- 设计合理的难度递进曲线
- 确保流畅的游戏体验和响应速度
通过本项目的实践,可以深入理解ArkTS的状态管理机制、UI组件的动态布局、以及游戏逻辑的实现方法。同时,舒尔特竞速游戏也体现了游戏化学习的理念,通过趣味性的方式训练注意力和反应能力。
更多推荐

所有评论(0)