在这里插入图片描述

在前面的文章中,我们实现了多个不同类型的游戏,从益智类到文字类,涵盖了各种游戏机制。这次我们要实现一个颜色匹配游戏,这是一个考验反应速度和注意力的游戏。通过实现这个游戏,你将学习到如何使用周期性定时器、处理颜色数据、实现倒计时等技能。

颜色匹配游戏的玩法

颜色匹配游戏的玩法非常有趣:屏幕上会显示一个词语,比如"红色",但这个词语的颜色可能是红色,也可能是其他颜色,比如蓝色。玩家需要快速判断词语的文字内容和显示颜色是否匹配。如果匹配,点击"匹配"按钮;如果不匹配,点击"不匹配"按钮。

这个游戏利用了斯特鲁普效应(Stroop Effect),即当词语的含义和颜色不一致时,人的反应会变慢。比如看到蓝色的"红色"两个字,大脑需要更长时间来判断。游戏有30秒的时间限制,玩家需要在时间内尽可能多地做出正确判断。答对得分,答错扣分,考验玩家的反应速度和注意力。

页面结构的搭建

ColorMatchPage是一个有状态组件,需要管理颜色、文字和计时器:

class ColorMatchPage extends StatefulWidget {
  const ColorMatchPage({super.key});

  
  State<ColorMatchPage> createState() => _ColorMatchPageState();
}

使用StatefulWidget是因为游戏状态会随着时间和玩家的操作不断变化。每秒钟倒计时会减少,每次玩家做出判断后颜色和文字会改变。这些变化都需要通过State类来管理,并通过setState方法触发UI更新。相比前面的游戏,颜色匹配涉及到周期性定时器和生命周期管理。

游戏状态的定义

在State类中,我们需要定义游戏的核心状态和数据:

class _ColorMatchPageState extends State<ColorMatchPage> {
  final List<Color> colors = [Colors.red, Colors.blue, Colors.green, 
                               Colors.yellow, Colors.purple, Colors.orange];
  final List<String> colorNames = ['红色', '蓝色', '绿色', 
                                    '黄色', '紫色', '橙色'];
  
  Color currentColor = Colors.red;
  String currentText = '红色';
  int score = 0;
  int timeLeft = 30;
  Timer? timer;

colors是颜色列表,包含6种常见颜色。colorNames是对应的颜色名称列表。这两个列表的索引是对应的,比如colors[0]是红色,colorNames[0]是"红色"。使用final修饰,因为这些数据在游戏过程中不会改变。

currentColor是当前显示的颜色,currentText是当前显示的文字。score记录得分,timeLeft记录剩余时间,初始为30秒。timer是Timer对象的引用,用于管理倒计时。使用可空类型Timer?,因为timer可能为null。

这种将颜色和名称分开存储的设计,让我们可以灵活地组合它们。比如可以显示红色的"蓝色",或者蓝色的"红色"。通过随机选择颜色和名称的索引,可以生成各种组合。

初始化和清理

页面创建和销毁时需要进行初始化和清理:


void initState() {
  super.initState();
  _generateNew();
  _startTimer();
}


void dispose() {
  timer?.cancel();
  super.dispose();
}

initState方法在页面创建时调用一次,我们在这里调用_generateNew生成初始的颜色和文字,调用_startTimer启动倒计时。这种在initState中初始化游戏的做法,确保用户看到页面时游戏已经开始了。

dispose方法在页面销毁时调用,我们在这里取消定时器。timer?.cancel()使用了空安全的调用语法,如果timer为null就不调用cancel。这是非常重要的清理操作,如果不取消定时器,会导致内存泄漏,定时器会继续运行并尝试更新已销毁的页面。

这种生命周期管理是Flutter开发的基础。initState用于初始化,dispose用于清理。所有需要清理的资源,比如定时器、动画控制器、流订阅等,都应该在dispose中释放。

倒计时的实现

倒计时是游戏的核心机制,使用Timer.periodic实现:

void _startTimer() {
  timer = Timer.periodic(const Duration(seconds: 1), (timer) {
    if (timeLeft > 0) {
      setState(() => timeLeft--);
    } else {
      timer.cancel();
    }
  });
}

Timer.periodic创建一个周期性定时器,每秒执行一次回调函数。回调函数接收timer参数,可以用来取消定时器。如果剩余时间大于0,减少时间并更新UI。如果时间到了,取消定时器,游戏结束。

这里使用setState(() => timeLeft–)这种简洁的写法,等价于setState(() { timeLeft–; })。Dart支持这种箭头函数的语法,当函数体只有一条语句时,可以省略花括号。

Timer.periodic与Timer的区别是:Timer是一次性的,执行一次后就结束。Timer.periodic是周期性的,会重复执行,直到被取消。在实现倒计时、轮询、动画等功能时,Timer.periodic非常有用。

需要注意的是,定时器的精度不是绝对的。由于系统调度和其他因素,实际的执行间隔可能略有偏差。但对于游戏来说,这种偏差是可以接受的。

颜色和文字的生成

每次玩家做出判断后,需要生成新的颜色和文字:

void _generateNew() {
  final random = Random();
  setState(() {
    currentColor = colors[random.nextInt(colors.length)];
    currentText = colorNames[random.nextInt(colorNames.length)];
  });
}

使用Random类生成随机索引。random.nextInt(colors.length)生成0到colors.length-1的随机整数,用作颜色索引。random.nextInt(colorNames.length)生成颜色名称索引。这两个索引是独立随机的,所以颜色和文字可能匹配,也可能不匹配。

这种独立随机的设计,让游戏有大约1/6的概率生成匹配的组合(因为有6种颜色)。这个概率既不会太高让游戏太简单,也不会太低让游戏太难。可以通过调整颜色数量来改变难度,颜色越多,匹配概率越低,游戏越难。

setState包裹状态更新,触发UI重新构建。玩家会看到新的颜色和文字立即显示在屏幕上,可以继续做出判断。这种快速的反馈循环,让游戏节奏紧凑,保持玩家的注意力。

答案判断逻辑

当玩家做出判断时,需要验证答案是否正确:

void _answer(bool match) {
  if (timeLeft == 0) return;

  final isMatch = colors.indexOf(currentColor) == 
                  colorNames.indexOf(currentText);
  if (match == isMatch) {
    setState(() => score++);
  } else {
    setState(() => score = max(0, score - 1));
  }
  _generateNew();
}

_answer方法接收一个布尔值参数,表示玩家认为是否匹配。首先检查时间是否已经用完,如果用完了就不处理。然后计算实际是否匹配:使用indexOf找到当前颜色和文字在列表中的索引,如果索引相同,说明匹配。

比较玩家的判断和实际情况,如果相同说明答对了,增加得分。如果不同说明答错了,减少得分。使用max(0, score - 1)确保得分不会变成负数。最后调用_generateNew生成新的颜色和文字,继续游戏。

这里使用indexOf方法来判断匹配,而不是直接比较currentColor和colors[colorNames.indexOf(currentText)]。这种方式更加清晰,表达了"找到颜色的索引和找到文字的索引,比较它们是否相同"这个逻辑。

页面UI的构建

游戏页面的UI包括AppBar、时间和得分显示、颜色文字显示和判断按钮:


Widget build(BuildContext context) {
  return Scaffold(
    appBar: AppBar(
      title: const Text('颜色匹配'),
      backgroundColor: const Color(0xFF16213e),
    ),
    body: Center(
      child: Column(
        mainAxisAlignment: MainAxisAlignment.center,
        children: [
          Text('时间: $timeLeft秒', 
               style: TextStyle(fontSize: 20.sp)),
          SizedBox(height: 10.h),
          Text('得分: $score', 
               style: TextStyle(fontSize: 24.sp, 
                              fontWeight: FontWeight.bold, 
                              color: Colors.amber)),
          SizedBox(height: 50.h),

AppBar的标题显示"颜色匹配",背景色使用深蓝色。Center让内容居中显示,Column垂直排列页面内容。最上面显示剩余时间,使用20号字体。下面显示得分,使用24号粗体琥珀色字体。SizedBox添加50个单位的间距,将得分和游戏内容分隔开。

游戏进行中的显示:

          if (timeLeft > 0) ...[
            Text('颜色和文字是否匹配?', 
                 style: TextStyle(fontSize: 18.sp)),
            SizedBox(height: 30.h),
            Text(currentText, 
                 style: TextStyle(fontSize: 48.sp, 
                                fontWeight: FontWeight.bold, 
                                color: currentColor)),
            SizedBox(height: 50.h),

如果时间还没用完,显示提示文字和当前的颜色文字。currentText使用48号粗体字显示,颜色设置为currentColor。这是游戏的核心:文字内容和显示颜色可能不一致,玩家需要快速判断它们是否匹配。

这里使用了Dart的集合展开运算符…和if语句的组合。if (timeLeft > 0) …[…]表示如果时间还没用完,就将方括号中的Widget添加到children列表中。这种语法让条件渲染的代码非常简洁。

判断按钮的实现

两个判断按钮是玩家与游戏交互的主要方式:

            Row(
              mainAxisAlignment: MainAxisAlignment.center,
              children: [
                ElevatedButton(
                  onPressed: () => _answer(true),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.green,
                    padding: EdgeInsets.symmetric(
                        horizontal: 40.w, vertical: 20.h),
                    shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12.r)),
                  ),
                  child: Text('匹配', 
                              style: TextStyle(fontSize: 18.sp)),
                ),
                SizedBox(width: 20.w),

Row横向排列两个按钮,mainAxisAlignment设置为center让按钮居中。第一个按钮是"匹配"按钮,背景色设置为绿色,表示肯定的选择。padding设置水平40个单位、垂直20个单位的内边距,让按钮有足够的点击区域。shape设置圆角为12个单位。

第二个按钮的实现:

                ElevatedButton(
                  onPressed: () => _answer(false),
                  style: ElevatedButton.styleFrom(
                    backgroundColor: Colors.red,
                    padding: EdgeInsets.symmetric(
                        horizontal: 40.w, vertical: 20.h),
                    shape: RoundedRectangleBorder(
                        borderRadius: BorderRadius.circular(12.r)),
                  ),
                  child: Text('不匹配', 
                              style: TextStyle(fontSize: 18.sp)),
                ),
              ],
            ),

"不匹配"按钮的背景色设置为红色,表示否定的选择。其他样式与"匹配"按钮相同。这种使用颜色区分按钮功能的设计,让玩家可以快速识别,不需要仔细阅读文字。绿色表示肯定,红色表示否定,这是通用的视觉语言。

游戏结束的显示

当时间用完后,显示游戏结束画面:

          ] else ...[
            Text('⏰ 时间到!', 
                 style: TextStyle(fontSize: 32.sp, 
                                fontWeight: FontWeight.bold)),
            SizedBox(height: 20.h),
            Text('最终得分: $score', 
                 style: TextStyle(fontSize: 28.sp, 
                                color: Colors.amber)),
          ],
        ],
      ),
    ),
  );
}

如果时间用完,显示"时间到!"和最终得分。使用32号粗体字显示提示,配合时钟emoji。最终得分使用28号琥珀色字体显示,非常醒目。这种简洁的结束画面,让玩家清楚地知道游戏已经结束,可以查看自己的成绩。

这里使用了else分支,与前面的if (timeLeft > 0)对应。这种if-else的结构,让代码逻辑清晰,要么显示游戏内容,要么显示结束画面,不会同时显示两者。

斯特鲁普效应的应用

颜色匹配游戏利用了心理学中的斯特鲁普效应。这个效应是由心理学家John Ridley Stroop在1935年发现的,描述了当词语的含义和颜色不一致时,人的反应会变慢的现象。

比如看到红色的"红色"两个字,大脑可以快速判断它们匹配。但看到蓝色的"红色"两个字,大脑需要抑制对文字含义的自动反应,专注于颜色本身,这需要更长的时间。

这个效应在认知心理学中有重要意义,说明了人类信息处理的特点。在游戏中应用这个效应,可以创造出有趣的挑战。玩家需要克服大脑的自动反应,专注于颜色本身,这需要注意力和自控力。

可以通过调整匹配和不匹配的比例来改变难度。如果大部分都是不匹配的,玩家会习惯性地选择"不匹配",反而容易答错匹配的情况。如果大部分都是匹配的,玩家会习惯性地选择"匹配",反而容易答错不匹配的情况。保持大约50%的匹配率,可以让游戏难度适中。

Timer.periodic的深入理解

Timer.periodic是实现周期性任务的核心工具,让我们深入了解一下它的用法:

Timer.periodic(Duration(seconds: 1), (timer) {
  // 每秒执行一次
});

Timer.periodic接收两个参数:执行间隔和回调函数。回调函数接收timer参数,可以用来取消定时器。定时器会一直运行,直到被取消或页面销毁。

可以在回调函数中根据条件取消定时器:

int count = 0;
Timer.periodic(Duration(seconds: 1), (timer) {
  count++;
  if (count >= 10) {
    timer.cancel();  // 执行10次后取消
  }
});

需要注意的是,Timer.periodic的回调函数是在主线程执行的。如果回调函数执行时间过长,会阻塞UI,导致卡顿。应该避免在回调函数中进行耗时操作,比如复杂计算、网络请求等。

如果需要在后台线程执行任务,可以使用Isolate。但对于简单的游戏逻辑,在主线程执行已经足够了。

Timer.periodic的精度受系统调度影响,实际执行间隔可能略有偏差。如果需要高精度的定时,可以使用Stopwatch记录实际经过的时间,根据实际时间来更新状态。

颜色的使用技巧

Flutter提供了丰富的颜色API,让我们深入了解一下颜色的使用:

Color red = Colors.red;              // 预定义颜色
Color custom = Color(0xFFFF0000);    // 自定义颜色(ARGB格式)
Color fromRGB = Color.fromRGBO(255, 0, 0, 1.0);  // 从RGB创建
Color fromARGB = Color.fromARGB(255, 255, 0, 0);  // 从ARGB创建

颜色可以进行各种操作:

Color lighter = red.withOpacity(0.5);  // 调整透明度
Color darker = Color.lerp(red, Colors.black, 0.5);  // 颜色插值
int alpha = red.alpha;    // 获取alpha值
int redValue = red.red;   // 获取红色分量

在我们的游戏中,使用了预定义的颜色Colors.red、Colors.blue等。这些颜色是Material Design规范中定义的,有统一的视觉效果。如果需要自定义颜色,可以使用Color构造函数。

颜色的选择也很重要。我们选择了红、蓝、绿、黄、紫、橙六种颜色,它们的色相差异明显,容易区分。如果选择相近的颜色,比如深红和浅红,玩家可能难以区分,影响游戏体验。

性能优化的考虑

颜色匹配游戏的性能表现很好,因为游戏逻辑简单,UI更新频率适中。每秒更新一次倒计时,每次判断后更新一次颜色和文字,这些更新都很轻量级。

需要注意的是,定时器的回调函数会频繁执行。虽然我们的回调函数很简单,只是减少时间变量,但如果在回调函数中进行复杂操作,可能会影响性能。应该保持回调函数简洁高效。

另一个需要注意的是,定时器必须在页面销毁时取消。如果忘记取消,定时器会继续运行,尝试更新已销毁的页面,导致错误和内存泄漏。这是Flutter开发中常见的错误,需要特别注意。

可以使用Flutter DevTools的性能分析工具,监控应用的帧率和内存使用。如果发现性能问题,可以使用Timeline工具查看具体的性能瓶颈。

扩展功能的思考

颜色匹配游戏还有很多可以扩展的功能。比如可以添加难度选择,简单模式有更多时间,困难模式时间更短。可以添加连击系统,连续答对可以获得额外奖励。可以添加生命值系统,答错扣除生命值,生命值用完游戏结束。

还可以添加更多的颜色,增加游戏难度。可以添加特殊模式,比如只显示匹配的组合,或者只显示不匹配的组合。可以添加排行榜,记录最高分和最快速度。

这些扩展功能的实现都不需要大幅修改现有代码。比如添加难度选择,只需要根据难度调整初始时间。添加连击系统,只需要添加一个连击计数器,连续答对时增加,答错时重置。良好的代码组织让扩展变得容易。

总结

本文详细介绍了颜色匹配游戏的实现。我们从游戏玩法设计开始,定义了游戏状态,实现了倒计时、颜色生成、答案判断等核心逻辑,最后构建了游戏的UI。每个部分都有详细的代码和讲解,帮助你理解实现的原理。

颜色匹配游戏利用了斯特鲁普效应,考验玩家的反应速度和注意力。通过实现这个游戏,你学习到了如何使用Timer.periodic实现周期性任务,如何在dispose中清理资源,如何使用颜色API。我们还深入探讨了斯特鲁普效应、定时器精度、性能优化等高级话题。

这些技能不仅适用于颜色匹配游戏,也适用于其他需要定时任务和生命周期管理的应用。Timer.periodic在实现倒计时、轮询、动画等功能时很有用。生命周期管理是Flutter开发的基础,正确地初始化和清理资源很重要。

接下来的文章中,我们会继续实现最后一个游戏。通过学习这些游戏,你已经掌握了Flutter游戏开发的各种技巧,可以开发出各种各样的游戏和应用。


欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐