Flutter for OpenHarmony游戏集合App实战之记忆翻牌配对消除

通过网盘分享的文件:game_flutter_openharmony.zip

链接: https://pan.baidu.com/s/1ryUS1A0zcvXGrDaStu530w 提取码: tqip

前言

上一篇讲了记忆翻牌的表情图案,这篇来聊聊配对消除的逻辑。

记忆翻牌的核心玩法是:翻开两张牌,相同就配对成功,不同就翻回去。这个看似简单的逻辑,实现起来需要考虑很多细节:点击时机的控制、状态的流转、延迟翻回的处理等等。

我在实现这个功能的时候,最开始没有加canTap锁定,结果玩家可以在等待期间疯狂点击,把游戏状态搞得一团糟。后来加上了这个限制,游戏体验才正常了。

状态变量定义

dart 复制代码
List<bool> revealed = [];

这个列表记录每张卡片是否被翻开。revealed[i]为true表示第i张卡片正面朝上,玩家可以看到图案。初始时所有卡片都是false,背面朝上。

dart 复制代码
List<bool> matched = [];

这个列表记录每张卡片是否已经配对成功。matched[i]为true表示第i张卡片已经找到了它的配对,不会再翻回去。配对成功的卡片会一直显示正面。

dart 复制代码
int? firstIndex;

记录第一张翻开的卡片的索引。当玩家翻开第一张牌时,把索引存在这里;翻开第二张牌后,和firstIndex指向的牌比较,然后清空。

dart 复制代码
bool canTap = true;

控制是否允许点击。当两张牌不匹配需要翻回去时,设为false禁止点击,800毫秒后恢复。这个变量是防止玩家作弊的关键。

dart 复制代码
int moves = 0;

记录玩家的尝试次数。每翻开两张牌算一次尝试,不管是否配对成功。最后显示在胜利对话框里,让玩家知道自己的表现。

点击处理

dart 复制代码
void _tap(int index) {
  if (!canTap || revealed[index] || matched[index]) return;

  setState(() {
    revealed[index] = true;
    if (firstIndex == null) {
      firstIndex = index;
    } else {
      moves++;
      if (cards[firstIndex!] == cards[index]) {
        matched[firstIndex!] = matched[index] = true;
        firstIndex = null;
        if (matched.every((m) => m)) _showWinDialog();
      } else {
        canTap = false;
        Timer(const Duration(milliseconds: 800), () {
          setState(() {
            revealed[firstIndex!] = revealed[index] = false;
            firstIndex = null;
            canTap = true;
          });
        });
      }
    }
  });
}

这是整个游戏最核心的方法,处理玩家的每次点击。代码虽然不长,但逻辑很紧凑,每一行都有它的作用。

前置检查

dart 复制代码
if (!canTap || revealed[index] || matched[index]) return;

三种情况不能点击:

  • !canTap: 正在等待翻回去,禁止点击。这是最重要的检查,防止玩家在800毫秒等待期间乱点
  • revealed[index]: 这张牌已经翻开了,不能重复翻
  • matched[index]: 这张牌已经配对了,游戏规则不允许点击已配对的牌

这三个条件用||连接,任何一个满足就直接return,不执行后面的逻辑。这种"提前返回"的写法让代码更清晰,避免了深层嵌套。

翻开卡片

dart 复制代码
revealed[index] = true;

把点击的卡片设为翻开状态。这行代码执行后,UI会立即更新,显示卡片的正面图案。setState包裹了整个逻辑,确保状态变化能触发重绘。

第一张牌

dart 复制代码
if (firstIndex == null) {
  firstIndex = index;
}

如果firstIndex是null,说明这是本轮的第一张牌。把当前索引存起来,等待玩家翻第二张。这时候不需要做任何判断,只是记录。

第二张牌

dart 复制代码
} else {
  moves++;

firstIndex不是null,说明已经有一张牌翻开了,这是第二张。尝试次数加1,不管接下来是否配对成功。

配对成功

dart 复制代码
if (cards[firstIndex!] == cards[index]) {
  matched[firstIndex!] = matched[index] = true;
  firstIndex = null;
  if (matched.every((m) => m)) _showWinDialog();
}

两张牌的图案相同,配对成功:

  1. 把两张牌都标记为已配对,它们会一直显示正面
  2. 清空firstIndex,准备下一轮配对
  3. 检查是否全部配对完成,如果是就显示胜利对话框

注意这里用了firstIndex!,感叹号是Dart的空断言操作符,告诉编译器"我确定这个值不是null"。因为我们在else分支里,firstIndex一定有值。

配对失败

dart 复制代码
} else {
  canTap = false;
  Timer(const Duration(milliseconds: 800), () {
    setState(() {
      revealed[firstIndex!] = revealed[index] = false;
      firstIndex = null;
      canTap = true;
    });
  });
}

两张牌不同,配对失败:

  1. 禁止点击(canTap = false),防止玩家在等待期间继续点击
  2. 启动一个800毫秒的定时器
  3. 定时器触发后,把两张牌翻回去(revealed设为false)
  4. 清空firstIndex,准备下一轮
  5. 恢复点击(canTap = true)

这里的Timer是dart:async包提供的,用于延迟执行代码。800毫秒是一个经过测试的时间,足够玩家记住图案,又不会等太久影响游戏节奏。

canTap的作用

dart 复制代码
bool canTap = true;

这个变量防止玩家在等待期间乱点。

如果不加这个限制,玩家可以在800毫秒内快速点击其他牌,打乱游戏状态。比如玩家翻开了A和B两张不同的牌,在等待翻回去的时候又点击了C,这时候firstIndex还指向A,但A马上要被翻回去,逻辑就乱了。

我最开始实现的时候没有这个变量,测试的时候发现快速点击会导致各种奇怪的bug:有时候牌翻不回去,有时候配对判断出错。加上canTap之后,这些问题都解决了。

这种"锁定"机制在游戏开发中很常见,任何需要等待的操作都应该考虑加锁,防止用户在等待期间触发新的操作。

Timer延迟

dart 复制代码
Timer(const Duration(milliseconds: 800), () {
  // 翻回去
});

用Timer实现延迟执行。Timer是dart:async包提供的类,第一个参数是延迟时间,第二个参数是回调函数。

800毫秒是个适中的时间,足够玩家记住图案,又不会等太久。我试过500毫秒,感觉太快了,玩家来不及看清;试过1000毫秒,又觉得节奏太慢。800毫秒刚刚好。

为什么不用Future.delayed

dart 复制代码
// 也可以这样写
await Future.delayed(Duration(milliseconds: 800));
revealed[firstIndex!] = revealed[index] = false;

但这需要把方法改成async,而且setState的时机不好控制。async方法里的setState可能会在widget已经dispose之后执行,导致报错。

Timer更直接,而且可以在需要的时候取消(虽然这个游戏里不需要)。

Duration的写法

dart 复制代码
const Duration(milliseconds: 800)

Duration是Dart的时间间隔类,可以用不同的单位创建:

  • Duration(milliseconds: 800) - 800毫秒
  • Duration(seconds: 1) - 1秒
  • Duration(minutes: 5) - 5分钟

加const是因为这个Duration是编译时常量,可以提高性能。

胜利检查

dart 复制代码
if (matched.every((m) => m)) _showWinDialog();

every方法检查列表的每个元素是否都满足条件。

如果所有卡片都已配对(matched全是true),游戏胜利。every方法接收一个函数,对列表的每个元素调用这个函数,如果所有调用都返回true,every就返回true。

这里的(m) => m是一个简单的lambda,直接返回元素本身。因为matched是bool列表,元素本身就是true或false,所以这个写法等价于"检查是否所有元素都是true"。

胜利对话框

dart 复制代码
void _showWinDialog() {
  showDialog(context: context, builder: (_) => AlertDialog(
    title: const Text('恭喜!'), content: Text('完成配对! 共$moves次尝试'),
    actions: [TextButton(onPressed: () { Navigator.pop(context); setState(_initGame); }, child: const Text('再来一局'))],
  ));
}

显示尝试次数,8对卡片最少需要8次(每次都配对成功)。实际上很难做到,因为一开始不知道牌的位置,需要靠记忆。

对话框用showDialog显示,builder返回一个AlertDialog。AlertDialog有三个主要部分:

  • title: 标题,显示"恭喜!"
  • content: 内容,显示尝试次数
  • actions: 按钮列表,这里只有一个"再来一局"按钮

点击按钮后,先用Navigator.pop关闭对话框,然后调用_initGame重新初始化游戏。setState(_initGame)是一个简写,等价于setState(() { _initGame(); })。

配对成功的视觉反馈

dart 复制代码
color: matched[i] ? Colors.green[200] : ...

配对成功的卡片变成浅绿色,和其他卡片区分。

图案保持显示,玩家可以看到已经配对了哪些。这个视觉反馈很重要,让玩家知道哪些牌已经"安全"了,可以专注于剩下的牌。

绿色是一个积极的颜色,代表"成功"、"完成",用在这里很合适。Colors.green[200]是浅绿色,不会太刺眼。

卡片渲染逻辑

dart 复制代码
Container(
  decoration: BoxDecoration(
    color: matched[i] ? Colors.green[200] : (revealed[i] ? Colors.white : Colors.blue[300]),
    borderRadius: BorderRadius.circular(8),
  ),
  child: Center(
    child: revealed[i] || matched[i] 
      ? Text(cards[i], style: const TextStyle(fontSize: 32))
      : const Text('?', style: TextStyle(fontSize: 24, color: Colors.white)),
  ),
)

这段代码决定了卡片的外观。根据revealed和matched的状态,显示不同的颜色和内容。

颜色逻辑

dart 复制代码
color: matched[i] ? Colors.green[200] : (revealed[i] ? Colors.white : Colors.blue[300])

这是一个嵌套的三元表达式:

  1. 如果已配对(matched[i]),显示浅绿色
  2. 否则,如果已翻开(revealed[i]),显示白色
  3. 否则,显示蓝色(背面)

内容逻辑

dart 复制代码
child: revealed[i] || matched[i] 
  ? Text(cards[i], style: const TextStyle(fontSize: 32))
  : const Text('?', style: TextStyle(fontSize: 24, color: Colors.white))

如果卡片翻开或已配对,显示图案(表情符号);否则显示问号。

图案用32号字体,比较大,容易看清;问号用24号字体,稍微小一点,而且是白色的,和蓝色背景搭配。

状态流转

卡片有三种状态:

  1. 未翻开: revealed=false, matched=false

    • 显示蓝色背面和问号
    • 可以点击翻开
  2. 已翻开: revealed=true, matched=false

    • 显示白色正面和图案
    • 临时状态,可能翻回去
    • 不能再次点击
  3. 已配对: matched=true

    • 显示绿色正面和图案
    • 永久状态,不会变
    • 不能点击

状态只能按照特定的顺序流转:未翻开 → 已翻开 → 已配对(或翻回未翻开)。不可能从已配对变回其他状态。

理解这个状态流转对于理解游戏逻辑很重要。每次点击都会触发状态变化,而状态变化又会触发UI更新。

firstIndex的作用

dart 复制代码
int? firstIndex;

记录第一张翻开的牌的索引。

  • null: 还没翻开任何牌,或者刚完成一轮配对
  • 有值: 已经翻开一张牌,等待第二张

这个变量是配对逻辑的核心。它让我们知道当前是在等待第一张牌还是第二张牌,以及第一张牌是哪一张。

用可空类型(int?)而不是用-1表示"没有",是Dart的推荐做法。null的语义更清晰,而且编译器会帮我们检查空值处理。

游戏初始化

dart 复制代码
void _initGame() {
  List<String> emojis = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼'];
  cards = [...emojis, ...emojis]..shuffle();
  revealed = List.filled(16, false);
  matched = List.filled(16, false);
  firstIndex = null;
  canTap = true;
  moves = 0;
}

初始化游戏状态:

  1. 创建8对表情符号(16张牌)
  2. 打乱顺序
  3. 所有牌设为未翻开、未配对
  4. 清空firstIndex
  5. 允许点击
  6. 尝试次数归零

这个方法在游戏开始和"再来一局"时调用。

小结

这篇讲了记忆翻牌的配对消除,核心知识点:

  • 状态变量: revealed、matched、firstIndex、canTap、moves各有作用
  • firstIndex: 记录第一张牌,区分第一次和第二次点击
  • 配对判断: 比较两张牌的图案是否相同
  • Timer延迟: 800毫秒后翻回去,给玩家记忆时间
  • canTap锁定: 防止等待期间乱点,保证游戏状态正确
  • every方法: 检查是否全部配对完成
  • 三种状态: 未翻开、已翻开、已配对,状态流转清晰
  • 颜色反馈: 配对成功变绿色,给玩家积极反馈
  • 嵌套三元: 根据多个条件决定显示内容

配对消除是记忆翻牌的核心逻辑,理解了状态流转和延迟处理,游戏就完成了。这个游戏虽然简单,但涉及到的状态管理、异步处理、用户交互等概念,在更复杂的应用中也会用到。


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

相关推荐
BlockChain8881 小时前
电脑卡顿解决方法大全(2025终极版)| 开机慢、运行卡、游戏掉帧?14种快速修复方案+长期优化指南
游戏·电脑
2501_944526421 小时前
Flutter for OpenHarmony 万能游戏库App实战 - 设置功能实现
android·javascript·flutter·游戏·harmonyos
m0_748240441 小时前
Laravel5.6核心更新全解析
开发语言·php
鹿角片ljp1 小时前
Java网络编程入门:从Socket到多线程服务器
java·服务器·网络
曹牧1 小时前
C#:Obsolete
开发语言·c#
kirk_wang2 小时前
Flutter艺术探索-Flutter异步编程:Future、async/await深度解析
flutter·移动开发·flutter教程·移动开发教程
我是苏苏2 小时前
Web开发:使用C#的System.Drawing.Common将png图片转化为icon图片
开发语言·c#
走进IT2 小时前
DDD项目分层结构说明
java
橙露2 小时前
嵌入式实时操作系统 FreeRTOS:任务调度与信号量的核心应用
java·大数据·服务器