Flutter for OpenHarmony游戏集合App实战之记忆翻牌表情图案

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

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

前言

记忆翻牌是一个配对游戏,翻开两张牌,如果图案相同就配对成功,不同就翻回去。

图案是游戏的核心元素,用什么图案、怎么显示,这篇来聊聊。

表情符号

dart 复制代码
final List<String> emojis = ['🍎', '🍊', '🍋', '🍇', '🍓', '🍒', '🥝', '🍑', '🍌', '🥭', '🍍', '🥥'];

这行代码定义了一个字符串列表,包含12种水果表情。

  • final表示这个列表引用不会改变(但列表内容可以改变)
  • 每个元素都是一个Unicode表情字符
  • 选择水果是因为颜色鲜艳、容易区分

为什么用表情

  1. 不需要图片资源:表情是Unicode字符,直接用Text显示,不需要额外的图片文件
  2. 跨平台一致:大多数设备都支持这些表情,iOS、Android、鸿蒙都能正常显示
  3. 色彩丰富:水果表情颜色鲜艳,红橙黄绿都有,容易区分
  4. 简单直接:一个字符串就是一个图案,不需要复杂的图片加载逻辑

💡 用表情是我偷懒的做法。正经的游戏应该用精美的图片,但表情足够用了,而且开发速度快。

选择水果

dart 复制代码
List<String> selected = (emojis..shuffle()).take(pairs).toList();

这行代码从12种水果中随机选8种。让我拆解一下:

  • emojis..shuffle()..是级联操作符,先调用shuffle()打乱emojis,然后返回emojis本身
  • .take(pairs):取前pairs个元素(pairs=8),返回一个Iterable
  • .toList():把Iterable转成List

为什么要随机选择?因为每局游戏用不同的水果组合,增加新鲜感。

级联操作符的妙用

dart 复制代码
// 这两种写法等价
List<String> selected = (emojis..shuffle()).take(pairs).toList();

// 等价于
emojis.shuffle();
List<String> selected = emojis.take(pairs).toList();

级联操作符..让代码更紧凑。它执行方法但返回对象本身,而不是方法的返回值。

卡片数据

dart 复制代码
static const int pairs = 8;
late List<String> cards;

这里定义了两个变量:

  • pairs:配对数量,8对就是16张卡片
  • cards:存储所有卡片的图案,延迟初始化

生成卡片

dart 复制代码
cards = [...selected, ...selected]..shuffle();

这行代码生成16张卡片并打乱。

  • [...selected, ...selected]:展开运算符...把selected的元素展开,相当于把两个列表合并
  • 每种图案出现两次,形成配对
  • ..shuffle():打乱顺序

举个例子,如果selected是['🍎', '🍊', '🍋'],那么:

  • [...selected, ...selected]得到['🍎', '🍊', '🍋', '🍎', '🍊', '🍋']
  • shuffle后可能变成['🍊', '🍎', '🍋', '🍋', '🍎', '🍊']

卡片状态

dart 复制代码
late List<bool> revealed;
late List<bool> matched;

两个布尔数组,分别记录每张卡片的状态:

  • revealed: 是否翻开(临时状态,可能翻回去)
  • matched: 是否已配对(永久状态,不会变)

初始化

dart 复制代码
revealed = List.filled(cards.length, false);
matched = List.filled(cards.length, false);

List.filled创建指定长度的列表,所有元素都是同一个值。

  • cards.length是16
  • 初始值都是false
  • 所有卡片都是背面(revealed=false)、未配对(matched=false)

为什么用两个数组

一个数组不够吗?不够,因为状态转换不同:

  • revealed可以从true变回false(翻开后不匹配,翻回去)
  • matched只能从false变成true(配对成功后不会取消)

如果只用一个数组,就无法区分"已翻开但未配对"和"已配对"两种状态。

卡片渲染

dart 复制代码
child: AnimatedContainer(
  duration: const Duration(milliseconds: 300),
  decoration: BoxDecoration(
    color: matched[i] ? Colors.green[200] : (revealed[i] ? Colors.white : Colors.blue[400]),
    borderRadius: BorderRadius.circular(8),
    boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4, offset: const Offset(2, 2))],
  ),

这是卡片的容器,使用AnimatedContainer实现动画效果。

三种颜色

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

嵌套的三元运算符,根据状态决定颜色:

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

颜色的含义:

  • 浅绿色(green[200]): 配对成功,给玩家正向反馈
  • 白色: 正面,显示图案
  • 蓝色(blue[400]): 背面,隐藏图案

AnimatedContainer

dart 复制代码
AnimatedContainer(
  duration: const Duration(milliseconds: 300),

AnimatedContainer是Flutter的动画容器,当属性变化时自动产生动画。

  • duration:动画时长300毫秒
  • 颜色变化会有平滑过渡,不是瞬间切换
  • 不需要手动管理动画控制器

💡 AnimatedContainer是懒人福音。不用写AnimationController、Tween那些复杂的东西,属性变了自动动画。

圆角和阴影

dart 复制代码
borderRadius: BorderRadius.circular(8),
boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4, offset: const Offset(2, 2))],

装饰细节:

  • borderRadius: 8像素圆角,让卡片看起来更柔和
  • boxShadow : 阴影效果,增加立体感
    • color: Colors.black26:26%透明度的黑色
    • blurRadius: 4:模糊半径4像素
    • offset: const Offset(2, 2):向右下偏移2像素

卡片内容

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

卡片的内容根据状态显示不同的东西。

正面显示

dart 复制代码
revealed[i] || matched[i]
    ? Text(cards[i], style: const TextStyle(fontSize: 32))

条件:翻开或已配对时显示正面。

  • cards[i]是这张卡片的表情符号
  • fontSize: 32:字号32,足够大,容易看清
  • Text可以直接显示表情符号,因为表情就是Unicode字符

背面显示

dart 复制代码
: const Icon(Icons.question_mark, color: Colors.white, size: 32),

未翻开时显示问号图标。

  • Icons.question_mark:Material Icons的问号
  • color: Colors.white:白色,在蓝色背景上清晰
  • size: 32:和表情一样大

为什么用问号

问号表示"未知",符合记忆翻牌的主题------你不知道背面是什么。

也可以用其他图标,比如Icons.helpIcons.visibility_off,但问号最直观。

GridView布局

dart 复制代码
GridView.builder(
  physics: const NeverScrollableScrollPhysics(),
  gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(crossAxisCount: 4, mainAxisSpacing: 8, crossAxisSpacing: 8),
  itemCount: cards.length,

GridView.builder用于构建网格布局。

禁止滚动

dart 复制代码
physics: const NeverScrollableScrollPhysics(),

记忆翻牌的卡片是固定的,不需要滚动。禁止滚动后,手势不会被GridView拦截。

4x4网格

dart 复制代码
crossAxisCount: 4,

每行4张卡片。16张卡片正好4行4列。

如果想要更多卡片,可以改成5x4(20张)或6x6(36张),相应地调整pairs的值。

间距

dart 复制代码
mainAxisSpacing: 8, crossAxisSpacing: 8,

卡片之间8像素间距,让卡片不会挤在一起。

  • mainAxisSpacing:主轴(垂直)方向间距
  • crossAxisSpacing:交叉轴(水平)方向间距

尝试次数

dart 复制代码
Padding(padding: const EdgeInsets.all(16), child: Text('尝试次数: $moves', style: const TextStyle(fontSize: 20))),

显示玩家翻了多少次牌。

  • moves变量在每次翻开第二张牌时加1
  • 玩家可以追求更少次数完成
  • 8对卡片最少需要8次(每次都配对成功)

完整的初始化流程

dart 复制代码
void _initGame() {
  List<String> selected = (emojis..shuffle()).take(pairs).toList();
  cards = [...selected, ...selected]..shuffle();
  revealed = List.filled(cards.length, false);
  matched = List.filled(cards.length, false);
  firstIndex = null;
  moves = 0;
  canTap = true;
}

初始化游戏的完整步骤:

  1. 从12种表情中随机选8种
  2. 每种复制一份,得到16张卡片
  3. 打乱卡片顺序
  4. 初始化revealed和matched数组
  5. 清空firstIndex(第一张翻开的牌)
  6. 重置尝试次数
  7. 允许点击

表情的可扩展性

如果想用其他图案,只需要修改emojis列表:

dart 复制代码
// 动物主题
final List<String> emojis = ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮'];

动物表情同样色彩丰富,而且更可爱。

dart 复制代码
// 交通工具主题
final List<String> emojis = ['🚗', '🚕', '🚙', '🚌', '🚎', '🏎️', '🚓', '🚑', '🚒', '🚐', '🛻', '🚚'];

交通工具适合男孩子。

dart 复制代码
// 运动主题
final List<String> emojis = ['⚽', '🏀', '🏈', '⚾', '🎾', '🏐', '🏉', '🎱', '🏓', '🏸', '🥊', '⛳'];

运动主题适合体育爱好者。

代码不用改,只换图案列表就行。这就是数据和逻辑分离的好处。

翻牌动画

当前实现用AnimatedContainer做颜色过渡,但没有真正的"翻牌"效果。如果想要3D翻转动画:

dart 复制代码
AnimatedBuilder(
  animation: _controller,
  builder: (context, child) {
    return Transform(
      alignment: Alignment.center,
      transform: Matrix4.identity()
        ..setEntry(3, 2, 0.001) // 透视效果
        ..rotateY(_animation.value * pi),
      child: _animation.value < 0.5
          ? _buildCardBack()
          : Transform(
              alignment: Alignment.center,
              transform: Matrix4.identity()..rotateY(pi),
              child: _buildCardFront(),
            ),
    );
  },
)

这个动画比较复杂,需要AnimationController和Matrix4变换。

简化版翻牌

如果不想用3D变换,可以用缩放模拟翻牌:

dart 复制代码
AnimatedScale(
  scale: revealed[i] ? 1.0 : 0.95,
  duration: const Duration(milliseconds: 200),
  child: AnimatedContainer(
    duration: const Duration(milliseconds: 200),
    // ... 其他属性
  ),
)

翻开时稍微放大,有一个"弹出"的感觉。

当前实现简化了,只用颜色变化表示翻牌。

配对逻辑

翻牌的核心是配对逻辑:

dart 复制代码
int? firstIndex;
bool canTap = true;

void _onCardTap(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!] = true;
        matched[index] = true;
        firstIndex = null;
        
        // 检查是否全部配对
        if (matched.every((m) => m)) {
          _showWinDialog();
        }
      } else {
        // 配对失败,延迟翻回去
        canTap = false;
        Future.delayed(const Duration(milliseconds: 1000), () {
          setState(() {
            revealed[firstIndex!] = false;
            revealed[index] = false;
            firstIndex = null;
            canTap = true;
          });
        });
      }
    }
  });
}

状态变量

  • firstIndex: 第一张翻开的牌的索引,null表示还没翻
  • canTap: 是否可以点击,配对失败时暂时禁止点击

翻开第一张

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

记录索引,等待翻开第二张。

翻开第二张

dart 复制代码
if (cards[firstIndex!] == cards[index]) {
  // 配对成功
  matched[firstIndex!] = true;
  matched[index] = true;
}

比较两张牌的图案,相同就配对成功。

配对失败

dart 复制代码
canTap = false;
Future.delayed(const Duration(milliseconds: 1000), () {
  setState(() {
    revealed[firstIndex!] = false;
    revealed[index] = false;
    firstIndex = null;
    canTap = true;
  });
});

延迟1秒后翻回去,让玩家有时间看清图案。

canTap = false防止在延迟期间继续点击。

难度设置

可以通过调整pairs来改变难度:

dart 复制代码
enum Difficulty { easy, medium, hard }

int getPairs(Difficulty difficulty) {
  switch (difficulty) {
    case Difficulty.easy: return 6;   // 12张牌,3x4
    case Difficulty.medium: return 8; // 16张牌,4x4
    case Difficulty.hard: return 12;  // 24张牌,4x6
  }
}

牌越多,记忆难度越大。

动态网格

根据牌数调整网格:

dart 复制代码
int getCrossAxisCount(int pairs) {
  if (pairs <= 6) return 3;
  if (pairs <= 8) return 4;
  return 6;
}

6对用3列,8对用4列,12对用6列。

计时和计分

计时

dart 复制代码
Stopwatch stopwatch = Stopwatch();

void _initGame() {
  // ... 初始化代码 ...
  stopwatch.reset();
  stopwatch.start();
}

String get timeString {
  int seconds = stopwatch.elapsed.inSeconds;
  return '${seconds ~/ 60}:${(seconds % 60).toString().padLeft(2, '0')}';
}

游戏开始时启动计时器,显示用时。

计分

可以根据尝试次数和用时计算分数:

dart 复制代码
int calculateScore() {
  int baseScore = 1000;
  int movePenalty = moves * 10;
  int timePenalty = stopwatch.elapsed.inSeconds;
  return (baseScore - movePenalty - timePenalty).clamp(0, 1000);
}

尝试次数越少、用时越短,分数越高。

音效

可以加音效增强反馈:

dart 复制代码
import 'package:audioplayers/audioplayers.dart';

final AudioPlayer _player = AudioPlayer();

void _playFlipSound() {
  _player.play(AssetSource('sounds/flip.mp3'));
}

void _playMatchSound() {
  _player.play(AssetSource('sounds/match.mp3'));
}

翻牌时播放翻牌音效,配对成功时播放成功音效。

需要添加audioplayers依赖和音效文件。当前实现简化了。

主题扩展

除了水果,可以支持多种主题:

dart 复制代码
class CardTheme {
  final String name;
  final List<String> emojis;
  final Color cardBackColor;
  
  const CardTheme({
    required this.name,
    required this.emojis,
    required this.cardBackColor,
  });
}

final fruitTheme = CardTheme(
  name: '水果',
  emojis: ['🍎', '🍊', '🍋', '🍇', '🍓', '🍒', '🥝', '🍑', '🍌', '🥭', '🍍', '🥥'],
  cardBackColor: Colors.blue[400]!,
);

final animalTheme = CardTheme(
  name: '动物',
  emojis: ['🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼', '🐨', '🐯', '🦁', '🐮'],
  cardBackColor: Colors.green[400]!,
);

玩家可以选择喜欢的主题,增加趣味性。

小结

这篇讲了记忆翻牌的表情图案,核心知识点:

  • 表情符号: Unicode字符,不需要图片资源,跨平台兼容好
  • 级联操作符 : ..执行方法并返回对象本身,代码更紧凑
  • 展开运算符 : ...把列表元素展开,方便合并列表
  • 随机选择: shuffle打乱 + take取前N个,每局不同组合
  • 复制配对 : [...list, ...list]每种图案两张,形成配对
  • 双数组状态: revealed和matched分别记录临时和永久状态
  • AnimatedContainer: 属性变化自动动画,简单易用
  • 三元运算符嵌套: 根据多个条件决定颜色,层次分明
  • 问号背面: Icon显示未知状态,符合游戏主题
  • Text显示表情: 表情就是字符串,直接用Text渲染
  • 配对逻辑: firstIndex记录第一张,比较后决定配对或翻回
  • 延迟翻回: Future.delayed让玩家有时间看清图案

表情符号是记忆翻牌的简单方案,不需要图片资源,开发速度快,跨平台兼容好。配合配对逻辑,就是一个完整的记忆游戏。


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

相关推荐
爱吃大芒果2 小时前
Flutter for OpenHarmony前置知识:Dart 语法核心知识点总结(上)
开发语言·flutter·dart
2501_944424122 小时前
Flutter for OpenHarmony游戏集合App实战之数字拼图打乱排列
android·开发语言·flutter·游戏·harmonyos
运维行者_2 小时前
OpManager 对接 ERP 避坑指南,网络自动化提升数据同步效率
运维·服务器·开发语言·网络·microsoft·网络安全·php
pas1362 小时前
34-mini-vue 更新element的children-双端对比diff算法
javascript·vue.js·算法
爱编程的小庄2 小时前
Rust初识
开发语言·rust
23124_802 小时前
热身签到-ctfshow
开发语言·python
ashcn20012 小时前
websocket测试通信
前端·javascript·websocket
小白学大数据2 小时前
移动端Temu App数据抓包与商品爬取方案
开发语言·爬虫·python
吃吃喝喝小朋友2 小时前
JavaScript文件的操作方法
开发语言·javascript·ecmascript