Flutter for OpenHarmony 游戏中心App实战:反应测试游戏实现
本文介绍了如何实现一个反应测试游戏,用于测量玩家的反应速度。游戏通过随机延时(1-4秒)后显示绿色提示,记录玩家点击响应时间。文章详细讲解了游戏的状态管理(等待、准备、结果显示)、时间测量方法、随机延时实现,以及根据状态变化更新UI的逻辑。重点包括:使用Timer实现随机延时、通过DateTime计算反应时间、处理过早点击情况,以及显示平均反应时间等统计数据。该游戏简单但富有挑战性,适合用来测试和

在前面的文章中,我们实现了九个不同类型的游戏,从拼图到颜色匹配,涵盖了各种游戏机制和技术要点。这次我们要实现最后一个游戏:反应测试。这是一个测试玩家反应速度的游戏,玩法简单但极具挑战性。通过实现这个游戏,你将学习到如何测量时间间隔、处理异步延时、计算统计数据等技能。
反应测试游戏的玩法
反应测试游戏的玩法非常简单:屏幕初始显示紫色背景和"点击开始"提示。玩家点击后,背景变成红色,显示"等待…“。经过随机的延时(1-4秒),背景突然变成绿色,显示"点击!”。玩家需要尽快点击屏幕,游戏会记录从绿色出现到玩家点击的时间,这就是反应时间。
如果玩家在绿色出现前就点击了,会显示"太早了!"的提示,这次测试无效。游戏可以进行多次测试,会计算平均反应时间。这种简单但有挑战性的玩法,让玩家可以测试和提高自己的反应速度。
页面结构的搭建
ReactionGamePage是一个有状态组件,需要管理游戏状态和测试数据:
class ReactionGamePage extends StatefulWidget {
const ReactionGamePage({super.key});
State<ReactionGamePage> createState() => _ReactionGamePageState();
}
使用StatefulWidget是因为游戏状态会随着玩家的操作不断变化。从等待状态到准备状态,从测试到结果显示,每个阶段都有不同的UI和逻辑。这些变化都需要通过State类来管理,并通过setState方法触发UI更新。相比前面的游戏,反应测试涉及到更精确的时间测量和状态机设计。
游戏状态的定义
在State类中,我们需要定义游戏的核心状态:
class _ReactionGamePageState extends State<ReactionGamePage> {
bool isWaiting = false;
bool isReady = false;
DateTime? startTime;
int? reactionTime;
List<int> times = [];
isWaiting标记是否正在等待绿色出现,isReady标记绿色是否已经出现。这两个布尔值定义了游戏的状态机:初始状态(都为false)、等待状态(isWaiting为true)、准备状态(isReady为true)。
startTime记录绿色出现的时间,使用DateTime类型。使用可空类型DateTime?,因为在初始状态和等待状态时,startTime为null。reactionTime记录反应时间(毫秒),也使用可空类型。times是一个列表,记录所有测试的反应时间,用于计算平均值。
这种使用多个布尔值定义状态机的方式,在游戏开发中很常见。虽然也可以使用枚举类型定义状态,但对于简单的状态机,使用布尔值更加直观。需要注意的是,要确保状态之间的转换是正确的,避免出现不一致的状态。
开始测试的逻辑
当玩家点击开始测试时,需要进入等待状态:
void _startTest() {
setState(() {
isWaiting = true;
isReady = false;
reactionTime = null;
});
final delay = Random().nextInt(3000) + 1000;
Timer(Duration(milliseconds: delay), () {
if (mounted && isWaiting) {
setState(() {
isReady = true;
startTime = DateTime.now();
});
}
});
}
_startTest方法首先更新状态:isWaiting设置为true,isReady设置为false,reactionTime设置为null。这表示进入等待状态,准备显示绿色。
然后生成一个随机延时,范围是1000到4000毫秒(1到4秒)。使用Timer创建一个一次性定时器,延时后执行回调函数。回调函数中首先检查mounted和isWaiting,确保页面还存在且还在等待状态。然后更新状态:isReady设置为true,startTime记录当前时间。
这里的mounted检查很重要。如果玩家在等待期间退出了页面,定时器的回调函数仍然会执行。如果不检查mounted,会尝试更新已销毁的页面,导致错误。这是异步编程中常见的问题,需要特别注意。
随机延时的设计让游戏更有挑战性。如果延时是固定的,玩家可以预判绿色出现的时间,提前做好准备。随机延时让玩家无法预判,必须真正依靠反应速度。
点击处理的逻辑
当玩家点击屏幕时,需要根据当前状态进行不同的处理:
void _tap() {
if (!isWaiting) {
_startTest();
return;
}
if (!isReady) {
setState(() {
isWaiting = false;
reactionTime = -1;
});
return;
}
_tap方法首先检查是否在等待状态。如果不在等待状态,说明是初始状态或结果显示状态,点击应该开始新的测试,调用_startTest方法。
如果在等待状态但绿色还没出现(!isReady),说明玩家点击太早了。更新状态:isWaiting设置为false,reactionTime设置为-1(用-1表示太早点击)。这会触发UI更新,显示"太早了!"的提示。
计算反应时间:
final time = DateTime.now().difference(startTime!).inMilliseconds;
setState(() {
reactionTime = time;
times.add(time);
isWaiting = false;
isReady = false;
});
}
如果绿色已经出现,计算反应时间。DateTime.now()获取当前时间,difference方法计算时间差,inMilliseconds获取毫秒数。将反应时间添加到times列表,更新状态。
这里使用startTime!强制解包,因为我们知道在isReady为true时,startTime一定不为null。虽然Dart的空安全系统无法自动推断这一点,但我们可以通过逻辑保证。如果不确定,应该使用startTime?.difference或者添加null检查。
页面UI的构建
游戏页面的UI根据状态显示不同的内容:
Widget build(BuildContext context) {
final avgTime = times.isEmpty
? 0
: times.reduce((a, b) => a + b) ~/ times.length;
return Scaffold(
appBar: AppBar(
title: const Text('反应测试'),
backgroundColor: const Color(0xFF16213e),
),
body: Column(
children: [
Padding(
padding: EdgeInsets.all(20.w),
child: Column(
children: [
Text('平均反应时间: ${avgTime}ms',
style: TextStyle(fontSize: 18.sp)),
SizedBox(height: 10.h),
Text('测试次数: ${times.length}',
style: TextStyle(fontSize: 16.sp,
color: Colors.white60)),
],
),
),
首先计算平均反应时间。如果times列表为空,平均时间为0。否则使用reduce方法求和,然后除以列表长度。reduce是一个归约操作,类似于fold,但不需要初始值,直接从列表的第一个元素开始。
AppBar的标题显示"反应测试",背景色使用深蓝色。Padding设置20个单位的内边距。最上面显示平均反应时间和测试次数,让玩家可以看到自己的整体表现。
游戏区域的实现:
Expanded(
child: GestureDetector(
onTap: _tap,
child: Container(
color: isReady ? Colors.green
: (isWaiting ? Colors.red : Colors.purpleAccent),
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded让Container占据剩余的所有空间。GestureDetector包裹整个Container,点击时调用_tap方法。Container的颜色根据状态决定:如果isReady为true显示绿色,如果isWaiting为true显示红色,否则显示紫色。
这种根据状态动态设置颜色的设计,让玩家可以清楚地看到当前的状态。紫色表示初始状态,红色表示等待状态,绿色表示准备状态。颜色的变化给玩家明确的视觉反馈。
不同状态的内容显示
根据游戏状态显示不同的提示内容:
if (!isWaiting && reactionTime == null) ...[
Icon(Icons.touch_app,
size: 80.sp,
color: Colors.white),
SizedBox(height: 20.h),
Text('点击开始',
style: TextStyle(fontSize: 28.sp,
fontWeight: FontWeight.bold)),
] else if (isWaiting && !isReady) ...[
Text('等待...',
style: TextStyle(fontSize: 32.sp,
fontWeight: FontWeight.bold)),
] else if (isReady) ...[
Text('点击!',
style: TextStyle(fontSize: 48.sp,
fontWeight: FontWeight.bold)),
使用多个if-else分支根据状态显示不同的内容。初始状态显示手指图标和"点击开始"提示。等待状态显示"等待…"提示。准备状态显示"点击!"提示,使用48号粗体字,非常醒目。
这种使用集合展开运算符和if-else的语法,让条件渲染的代码非常简洁。每个状态都有对应的UI,玩家可以清楚地知道当前应该做什么。
错误和结果的显示:
] else if (reactionTime == -1) ...[
Text('太早了!',
style: TextStyle(fontSize: 32.sp,
fontWeight: FontWeight.bold,
color: Colors.red)),
SizedBox(height: 20.h),
Text('点击重试',
style: TextStyle(fontSize: 18.sp)),
] else if (reactionTime != null) ...[
Text('$reactionTime ms',
style: TextStyle(fontSize: 48.sp,
fontWeight: FontWeight.bold,
color: Colors.amber)),
SizedBox(height: 20.h),
Text('点击继续',
style: TextStyle(fontSize: 18.sp)),
],
],
),
),
),
),
),
],
),
);
}
如果reactionTime为-1,显示"太早了!"的错误提示,使用红色字体。如果reactionTime不为null且不为-1,显示反应时间,使用48号粗体琥珀色字体。下面显示"点击继续"提示,引导玩家进行下一次测试。
这种完整的状态机设计,让游戏的交互流程非常清晰。每个状态都有对应的UI和逻辑,状态之间的转换也很明确。玩家可以轻松理解游戏的玩法,不会感到困惑。
DateTime的深入理解
DateTime是Dart中处理日期和时间的核心类,让我们深入了解一下它的用法:
DateTime now = DateTime.now(); // 获取当前时间
DateTime utc = DateTime.now().toUtc(); // 转换为UTC时间
DateTime local = utc.toLocal(); // 转换为本地时间
DateTime可以进行各种操作:
DateTime tomorrow = now.add(Duration(days: 1)); // 加一天
DateTime yesterday = now.subtract(Duration(days: 1)); // 减一天
Duration diff = now.difference(yesterday); // 计算时间差
bool isBefore = yesterday.isBefore(now); // 是否在之前
bool isAfter = tomorrow.isAfter(now); // 是否在之后
在我们的游戏中,使用DateTime.now()记录绿色出现的时间,然后使用difference方法计算时间差。inMilliseconds获取毫秒数,这是测量反应时间的精度。
需要注意的是,DateTime的精度取决于系统。在大多数系统上,精度是毫秒级的,但在某些系统上可能只有秒级精度。对于反应测试游戏来说,毫秒级精度已经足够了。
如果需要更高精度的时间测量,可以使用Stopwatch类:
Stopwatch stopwatch = Stopwatch()..start();
// 执行操作
stopwatch.stop();
int microseconds = stopwatch.elapsedMicroseconds; // 微秒
Stopwatch可以测量微秒级的时间,适合性能分析等场景。但对于游戏来说,DateTime已经足够了。
reduce方法的应用
reduce是Dart中的列表归约方法,让我们深入了解一下它的用法:
int sum = times.reduce((a, b) => a + b);
reduce方法接收一个归约函数,函数接收两个参数:累积值和当前元素。与fold不同,reduce不需要初始值,直接从列表的第一个元素开始。
reduce的执行过程是这样的:首先将列表的前两个元素作为参数调用归约函数,得到结果。然后将结果和第三个元素作为参数再次调用归约函数。重复这个过程,直到处理完所有元素。
除了求和,reduce还可以用于很多其他场景:
int max = numbers.reduce((a, b) => a > b ? a : b); // 求最大值
int min = numbers.reduce((a, b) => a < b ? a : b); // 求最小值
String concat = words.reduce((a, b) => a + b); // 字符串拼接
需要注意的是,如果列表为空,reduce会抛出异常。所以在使用reduce前,应该检查列表是否为空。在我们的代码中,使用三元运算符检查times.isEmpty,避免了这个问题。
reduce和fold的区别是:reduce不需要初始值,返回类型与列表元素类型相同。fold需要初始值,返回类型可以与列表元素类型不同。选择哪个方法取决于具体需求。
状态机的设计模式
反应测试游戏使用了状态机的设计模式。状态机是一种常见的设计模式,用于管理对象的状态和状态之间的转换。
我们的游戏有四个状态:
- 初始状态:!isWaiting && reactionTime == null
- 等待状态:isWaiting && !isReady
- 准备状态:isReady
- 结果状态:reactionTime != null
状态之间的转换是:
- 初始状态 -> 等待状态:点击开始
- 等待状态 -> 准备状态:延时结束
- 等待状态 -> 错误状态:太早点击
- 准备状态 -> 结果状态:点击屏幕
- 结果状态 -> 初始状态:点击继续
这种状态机的设计让游戏逻辑清晰,易于理解和维护。每个状态都有明确的UI和行为,状态之间的转换也有明确的触发条件。
在更复杂的应用中,可以使用枚举类型定义状态:
enum GameState { initial, waiting, ready, result, error }
GameState state = GameState.initial;
然后使用switch语句根据状态进行不同的处理。这种方式更加类型安全,但对于简单的状态机,使用布尔值已经足够了。
异步编程的陷阱
反应测试游戏涉及到异步编程,需要注意一些常见的陷阱:
内存泄漏:如果定时器的回调函数引用了State对象,而定时器没有被取消,会导致State对象无法被垃圾回收,造成内存泄漏。解决方法是在回调函数中检查mounted,或者在dispose中取消定时器。
状态不一致:如果在异步操作完成后更新状态,但页面已经销毁,会导致错误。解决方法是在更新状态前检查mounted。
竞态条件:如果有多个异步操作同时进行,可能会出现竞态条件。比如玩家快速点击多次,可能会创建多个定时器。解决方法是在开始新操作前取消旧操作,或者使用标志位防止重复操作。
在我们的游戏中,通过检查mounted和isWaiting,避免了这些问题。这是异步编程的最佳实践,应该在所有异步操作中遵循。
用户体验的优化
反应测试游戏的用户体验设计注重了几个方面。首先是清晰的视觉反馈,不同状态使用不同的颜色,玩家可以清楚地看到当前的状态。其次是明确的提示文字,告诉玩家应该做什么。
随机延时的设计增加了游戏的挑战性,让玩家无法预判。平均反应时间的显示让玩家可以看到自己的进步。错误提示的设计让玩家知道自己点击太早了,可以调整策略。
可以添加更多的反馈机制提升体验:
音效反馈:绿色出现时播放提示音,点击时播放反馈音。这可以增强游戏的沉浸感。
震动反馈:在关键时刻震动手机,给玩家触觉反馈。
成就系统:设置不同的成就,比如"反应时间低于200ms"、"连续测试10次"等,激励玩家挑战自己。
排行榜:记录最快反应时间,让玩家可以与其他玩家比较。
这些反馈机制可以让游戏更加生动有趣,提高玩家的参与度。
性能优化的考虑
反应测试游戏的性能表现很好,因为游戏逻辑简单,UI更新频率不高。每次状态改变时才更新UI,不会频繁重建。
需要注意的是,GestureDetector包裹了整个Container,这意味着整个屏幕都是可点击的。这种设计让玩家可以快速点击,不需要瞄准特定的按钮。但也意味着任何地方的点击都会触发_tap方法,需要确保方法执行效率高。
DateTime的操作是很轻量级的,不会影响性能。reduce方法虽然需要遍历列表,但times列表通常不会很长,性能影响可以忽略。
如果将来需要添加更多功能,比如动画效果、音效等,需要注意性能影响。可以使用Flutter DevTools的性能分析工具,监控应用的帧率和内存使用。
扩展功能的思考
反应测试游戏还有很多可以扩展的功能。比如可以添加难度选择,简单模式延时更长,困难模式延时更短。可以添加多次测试模式,自动进行多次测试,计算平均值。可以添加视觉干扰,在等待期间显示干扰信息,增加难度。
还可以添加不同的测试模式,比如听觉反应测试(听到声音后点击)、触觉反应测试(感到震动后点击)等。可以添加对比功能,显示玩家的反应时间在人群中的排名。可以添加训练模式,帮助玩家提高反应速度。
这些扩展功能的实现都不需要大幅修改现有代码。比如添加难度选择,只需要根据难度调整延时范围。添加多次测试模式,只需要添加一个计数器,自动重复测试流程。良好的代码组织让扩展变得容易。
总结
本文详细介绍了反应测试游戏的实现。我们从游戏玩法设计开始,定义了游戏状态,实现了状态机、时间测量、统计计算等核心逻辑,最后构建了游戏的UI。每个部分都有详细的代码和讲解,帮助你理解实现的原理。
反应测试游戏虽然玩法简单,但涉及到精确的时间测量和复杂的状态管理。通过实现这个游戏,你学习到了如何使用DateTime测量时间间隔,如何使用reduce计算统计数据,如何设计状态机管理游戏流程。我们还深入探讨了异步编程的陷阱、状态机设计模式、性能优化等高级话题。
至此,我们已经完成了游戏中心应用的所有10个游戏的实现。从拼图游戏到反应测试,每个游戏都有不同的玩法和实现方式,涵盖了Flutter游戏开发的各个方面。通过学习这些游戏,你已经掌握了状态管理、定时器、动画、文本处理、数学计算等核心技能,可以开发出各种各样的游戏和应用。
这些技能不仅适用于游戏开发,也适用于其他类型的应用开发。状态管理是所有交互式应用的核心,定时器在很多场景中都会用到,时间测量在性能分析和用户行为追踪中很重要。掌握了这些技能,你就可以成为一名优秀的Flutter开发者。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐

所有评论(0)