记忆翻牌大冒险:用 Flutter 与三方库在鸿蒙上打造单人挑战卡片记忆游戏
文章摘要 本项目基于鸿蒙6.0平台开发一款Flutter记忆翻牌游戏,包含以下核心功能:随机排列的emoji卡片配对、计时计分系统、多难度选择及最佳成绩记录。开发环境需配置Flutter 3.22+、DevEco Studio及鸿蒙SDK。项目采用模块化结构,包含游戏状态管理、卡片动画组件和双页面路由设计,支持第三方库扩展。代码提供详细注释,可直接在鸿蒙设备运行,适合初学者学习Flutter在鸿蒙
欢迎加入开源鸿蒙跨平台社区: https://openharmonycrossplatform.csdn.net
本项目从零开始,在鸿蒙 6.0 设备上构建一款单人记忆翻牌游戏。你将学习 Flutter 页面路由传参、数据模型、计时器与动画的基础用法,并了解如何将三方库(如 audioplayers、shared\_preferences)无缝扩展进你的项目。所有代码均带详细注释,可直接在鸿蒙平台运行。
一、你将构建什么
一款单人记忆翻牌游戏,核心功能如下:
-
屏幕上排列若干对 emoji 卡片,所有卡片默认背面朝上;
-
玩家每次翻开两张卡片,图案相同则配对成功并保持翻开状态,否则短暂显示后自动翻回;
-
游戏目标:在最短时间内找出所有配对卡片;
-
游戏结束后返回主页,本地记录各难度最佳时间(本文采用内存存储,可轻松扩展至
shared\_preferences实现持久化); -
支持多难度选择,界面配备细腻的缩放动画,提升交互体验。
二、环境与工具
搭建项目需准备以下环境与工具,避免后续编译报错:
-
鸿蒙 6.0 真机或模拟器;
-
DevEco Studio(内置 OpenHarmony SDK);
-
Flutter 3.22 及以上版本(已集成鸿蒙平台支持);
-
基本网络环境(用于下载项目依赖库)。
2.1 检查 Flutter 与鸿蒙环境
在终端依次执行以下命令,确保每一步输出符合预期:
1. 确认 Flutter 版本
flutter --version
输出示例:Flutter 3\.22\.0 • channel stable …,要求版本 ≥ 3.22,建议使用稳定版。
2. 启用鸿蒙平台支持
flutter config --enable-ohos
flutter config --list | grep enable-ohos
应显示 enable\-ohos: true,表示鸿蒙平台已启用。
3. 检查 DevEco Studio 与 OHOS SDK 路径
flutter doctor -v
输出中需找到以下类似段落,确认 SDK 配置正确:
[✓] OpenHarmony toolchain - develop for OpenHarmony devices
• DevEco Studio at /Applications/DevEco-Studio.app
• OpenHarmony SDK at /Users/xxx/ohos-sdk
• OpenHarmony SDK version 6.0.0
若出现 ✗ 提示,需按以下方式修复:
-
在 DevEco Studio 中下载对应 API 6.0 的系统镜像或 SDK;
-
设置环境变量
OHOS\_SDK\_HOME,指向你的 SDK 路径。
4. 检查已连接的鸿蒙设备
flutter devices
列表中需至少有一个 ohos 设备,示例如下:
ohos (mobile) • 1234567890ABCDEF • ohos • OpenHarmony 6.0
若无设备,需确认:
-
真机已开启“开发者模式”并信任连接的电脑;
-
模拟器已在 DevEco Studio 中启动,且能被
hdc list targets识别。
三、创建项目并配置鸿蒙平台
打开终端,依次执行以下命令,完成项目创建与鸿蒙平台配置:
mkdir -p ~/HarmonyProjects && cd ~/HarmonyProjects
flutter create memory_flip_ohos
cd memory_flip_ohos
若项目未自动生成鸿蒙平台支持文件,执行以下命令添加:
flutter create --platforms ohos .
配置完成后,项目根目录下会出现 ohos/ 文件夹,说明鸿蒙平台配置成功。
四、项目结构
为保证代码清晰可维护,所有逻辑文件均放在 lib/ 目录下,具体结构如下:
lib/
├── main.dart # 应用入口,初始化根组件
├── models/
│ └── game_state.dart # 游戏数据模型与全局状态管理
├── pages/
│ ├── home_page.dart # 主页,负责难度选择与最佳时间展示
│ └── game_page.dart # 游戏页面,实现核心翻牌、计时、匹配逻辑
└── widgets/
└── memory_card.dart # 单张卡片组件,包含翻开动画效果
在终端执行以下命令,创建对应目录:
mkdir -p lib/models lib/pages lib/widgets
创建完成后,用编辑器依次创建并写入以下 Dart 文件(具体代码见下一节)。
五、代码实现详解
以下所有文件均包含详细注释,可按顺序复制到项目对应目录中,直接使用。
5.1 应用入口 —— lib/main\.dart
import 'package:flutter/material.dart';
import 'pages/home_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
runApp(MemoryFlipApp());
}
/// 应用根组件,设定全局主题样式
class MemoryFlipApp extends StatelessWidget {
Widget build(BuildContext context) {
return MaterialApp(
title: '记忆翻牌大冒险',
debugShowCheckedModeBanner: false, // 隐藏调试横幅
theme: ThemeData(
brightness: Brightness.light,
primarySwatch: Colors.teal,
scaffoldBackgroundColor: Colors.teal.shade50,
),
home: HomePage(), // 应用启动后直接跳转主页
);
}
}
说明:采用纯白天蓝色调主题,简洁清爽;直接跳转至 HomePage,未添加主题切换逻辑,可自行扩展为日/夜间模式。
5.2 游戏数据模型 —— lib/models/game\_state\.dart
import 'dart:math';
/// 单张卡片的数据模型,存储卡片核心信息
class CardModel {
final int id; // 配对唯一标识,相同id为一对卡片
final String emoji; // 卡片显示的图案
bool isFlipped; // 卡片是否已翻开
bool isMatched; // 卡片是否已匹配成功
CardModel({
required this.id,
required this.emoji,
this.isFlipped = false,
this.isMatched = false,
});
}
/// 游戏全局状态管理,控制游戏流程与数据
class GameState {
late List<CardModel> cards; // 所有卡片列表
int flippedCardCount = 0; // 当前已翻开但未匹配的卡片数
int matchedPairs = 0; // 已成功配对的对数
bool isGameOver = false; // 游戏是否结束
int moveCount = 0; // 尝试次数(翻开两张卡片算一次)
Stopwatch stopwatch = Stopwatch(); // 游戏计时器
// 卡片图案库,可自由扩展更多图案
static const List<String> emojiSet = [
'🐶', '🐱', '🐭', '🐹', '🐰', '🦊', '🐻', '🐼',
'🐨', '🐯', '🦁', '🐮', '🐷', '🐸', '🐵', '🐔',
'🐧', '🐦', '🐤', '🦆', '🦅', '🦉', '🦇', '🐺',
];
/// 初始化一局游戏,pairCount 为当前难度的配对对数
void initGame(int pairCount) {
// 1. 从图案库随机选取 pairCount 个图案,每种图案生成两张卡片
final shuffled = emojiSet.toList()..shuffle();
final selected = shuffled.take(pairCount).toList();
final cardList = <CardModel>[];
for (int i = 0; i < selected.length; i++) {
cardList.add(CardModel(id: i, emoji: selected[i]));
cardList.add(CardModel(id: i, emoji: selected[i])); // 生成成对卡片
}
// 2. 打乱卡片顺序,保证每次游戏布局不同
cardList.shuffle(Random());
cards = cardList;
// 3. 重置游戏状态,准备新一局游戏
flippedCardCount = 0;
matchedPairs = 0;
isGameOver = false;
moveCount = 0;
stopwatch.reset();
stopwatch.start(); // 启动计时器
}
}
要点:
-
id是卡片配对的唯一标识,匹配逻辑基于id对比,比直接比对 emoji 更可靠; -
emojiSet提供 24 种不同图案,可支撑最高 10 对卡片的难度需求。
5.3 卡片组件 —— lib/widgets/memory\_card\.dart
import 'package:flutter/material.dart';
import '../models/game_state.dart';
class MemoryCard extends StatelessWidget {
final CardModel card;
final VoidCallback onTap; // 卡片点击回调函数
const MemoryCard({super.key, required this.card, required this.onTap});
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
// 已翻开或已匹配的卡片,不响应点击事件
if (card.isFlipped || card.isMatched) return;
onTap();
},
child: AnimatedScale(
scale: 1.0,
duration: Duration(milliseconds: 300),
curve: Curves.easeOutBack, // 弹性动画效果,提升交互质感
child: AnimatedSwitcher(
duration: Duration(milliseconds: 300),
// 根据卡片状态,切换显示正面或背面
child: card.isFlipped || card.isMatched
? _buildFront() // 显示卡片正面(图案)
: _buildBack(), // 显示卡片背面(问号)
),
),
);
}
// 构建卡片背面(默认显示状态)
Widget _buildBack() {
return Container(
key: ValueKey('back_${card.id}'),
decoration: BoxDecoration(
color: Colors.teal,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(blurRadius: 4, color: Colors.black26, offset: Offset(2, 2)),
],
),
child: Center(
child: Text('?', style: TextStyle(fontSize: 32, color: Colors.white70)),
),
);
}
// 构建卡片正面(翻开后显示状态)
Widget _buildFront() {
return Container(
key: ValueKey('front_${card.id}'),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
boxShadow: [
BoxShadow(blurRadius: 4, color: Colors.black26, offset: Offset(2, 2)),
],
// 匹配成功的卡片,添加金色边框标识
border: card.isMatched
? Border.all(color: Colors.amber, width: 3)
: null,
),
child: Center(
child: Text(card.emoji, style: TextStyle(fontSize: 36)),
),
);
}
}
动画说明:
-
AnimatedScale提供缩放动画,curve: Curves\.easeOutBack让卡片翻开时有弹性效果,提升交互体验; -
AnimatedSwitcher实现卡片正面与背面的淡入淡出切换,两个组件的duration均设为 300 毫秒,反馈及时。
5.4 主页 —— lib/pages/home\_page\.dart
import 'package:flutter/material.dart';
import 'game_page.dart';
class HomePage extends StatefulWidget {
const HomePage({super.key});
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
// 三个难度的最佳时间(单位:秒),初始值 999 表示“暂无记录”
int _best6 = 999, _best8 = 999, _best10 = 999;
/// 将秒数格式化为 mm:ss 格式,用于显示最佳时间
String _formatSeconds(int seconds) {
if (seconds == 999) return '--:--';
final m = seconds ~/ 60; // 分钟数
final s = seconds % 60; // 秒数
return '${m.toString().padLeft(2, '0')}:${s.toString().padLeft(2, '0')}';
}
/// 根据游戏页面返回的结果,更新对应难度的最佳时间
void _updateBestTime(int pairs, int seconds) {
setState(() {
if (pairs == 6 && seconds < _best6) _best6 = seconds;
if (pairs == 8 && seconds < _best8) _best8 = seconds;
if (pairs == 10 && seconds < _best10) _best10 = seconds;
});
}
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('记忆翻牌大冒险'),
),
body: Center(
child: SingleChildScrollView(
padding: EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('选择难度', style: Theme.of(context).textTheme.headlineSmall),
SizedBox(height: 30),
_buildDifficultyCard('简单', 6, _best6),
SizedBox(height: 12),
_buildDifficultyCard('中等', 8, _best8),
SizedBox(height: 12),
_buildDifficultyCard('困难', 10, _best10),
],
),
),
),
);
}
/// 构建单个难度选择卡片,点击跳转至对应难度的游戏页面
Widget _buildDifficultyCard(String label, int pairs, int bestSeconds) {
return Card(
elevation: 4,
child: ListTile(
title: Text(label, style: TextStyle(fontSize: 18)),
subtitle: Text('翻开 $pairs 对卡片'),
trailing: Text('最佳 ${_formatSeconds(bestSeconds)}'),
onTap: () async {
// 跳转游戏页面,并等待游戏结束后返回的结果
final result = await Navigator.push(
context,
MaterialPageRoute(
builder: (_) => GamePage(pairCount: pairs),
),
);
// 若返回有效结果,更新对应难度的最佳时间
if (result != null && result is Map) {
_updateBestTime(result['pairs'], result['seconds']);
}
},
),
);
}
}
关键设计:
-
最佳时间仅保存在内存中,应用退出后数据会丢失;若需持久化,可引入
shared\_preferences将数据写入本地; -
通过
Navigator\.push跳转游戏页面,并等待GamePage通过Navigator\.pop返回游戏结果,实现页面间数据传递。
5.5 游戏主页面 —— lib/pages/game\_page\.dart
import 'dart:async';
import 'package:flutter/material.dart';
import '../models/game_state.dart';
import '../widgets/memory_card.dart';
class GamePage extends StatefulWidget {
final int pairCount; // 从主页传入的配对数量(决定游戏难度)
const GamePage({super.key, required this.pairCount});
State<GamePage> createState() => _GamePageState();
}
class _GamePageState extends State<GamePage> {
final GameState _gameState = GameState();
List<CardModel?> _selectedCards = []; // 暂存当前翻开的两张未匹配卡片
Timer? _timer; // 用于每秒刷新计时显示
late Duration _elapsedTime = Duration.zero; // 已流逝的游戏时间
void initState() {
super.initState();
_gameState.initGame(widget.pairCount); // 初始化游戏
// 每秒刷新一次计时显示
_timer = Timer.periodic(Duration(seconds: 1), _updateTimer);
}
/// 每秒更新一次已流逝时间,刷新界面显示
void _updateTimer(Timer timer) {
if (!_gameState.isGameOver) {
setState(() {
_elapsedTime = _gameState.stopwatch.elapsed;
});
}
}
/// 卡片点击处理逻辑,控制卡片翻开、匹配判断
void _onCardTap(CardModel card) async {
if (_gameState.isGameOver) return; // 游戏结束后,不响应点击
if (_selectedCards.length == 2) return; // 已有两张未处理卡片,不允许再翻开
// 翻开当前卡片,更新游戏状态
setState(() {
card.isFlipped = true;
_gameState.flippedCardCount++;
_selectedCards.add(card);
});
// 当翻开两张卡片时,进行匹配判断
if (_selectedCards.length == 2) {
_gameState.moveCount++; // 增加尝试次数
final first = _selectedCards[0]!;
final second = _selectedCards[1]!;
if (first.id == second.id) {
// 配对成功:标记卡片为已匹配,重置暂存列表
setState(() {
first.isMatched = true;
second.isMatched = true;
_gameState.matchedPairs++;
_gameState.flippedCardCount -= 2;
_selectedCards.clear();
// 所有卡片配对完成,游戏结束
if (_gameState.matchedPairs == widget.pairCount) {
_gameState.isGameOver = true;
_gameState.stopwatch.stop();
_returnBestTime(); // 向主页返回游戏结果
}
});
} else {
// 配对失败:延迟 800ms 后,将两张卡片翻回背面
await Future.delayed(Duration(milliseconds: 800));
setState(() {
first.isFlipped = false;
second.isFlipped = false;
_gameState.flippedCardCount -= 2;
_selectedCards.clear();
});
}
}
}
/// 游戏结束后,向主页返回当前难度和用时,用于更新最佳时间
void _returnBestTime() {
Navigator.pop(context, {
'pairs': widget.pairCount,
'seconds': _elapsedTime.inSeconds,
});
}
/// 将 Duration 格式化为 mm:ss,用于显示游戏用时
String formatDuration(Duration d) {
String twoDigits(int n) => n.toString().padLeft(2, '0');
final minutes = twoDigits(d.inMinutes.remainder(60));
final seconds = twoDigits(d.inSeconds.remainder(60));
return '$minutes:$seconds';
}
void dispose() {
_timer?.cancel(); // 页面销毁时,取消计时器,避免内存泄漏
super.dispose();
}
Widget build(BuildContext context) {
final cardCount = _gameState.cards.length;
// 根据卡片总数,自动计算网格列数,优化显示效果
final crossAxisCount = (cardCount <= 8) ? 4 : (cardCount <= 15 ? 5 : 6);
return Scaffold(
appBar: AppBar(
title: Text('记忆翻牌'),
actions: [
Center(
child: Padding(
padding: EdgeInsets.only(right: 16),
child: Text(
'⏱ ${formatDuration(_elapsedTime)}',
style: TextStyle(fontSize: 18),
),
),
),
],
),
// 游戏结束时显示结束界面,否则显示卡片网格
body: _gameState.isGameOver
? _buildGameOverScreen()
: GridView.builder(
padding: const EdgeInsets.all(12),
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: crossAxisCount,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
),
itemCount: cardCount,
itemBuilder: (context, index) {
final card = _gameState.cards[index];
return MemoryCard(
card: card,
onTap: () => _onCardTap(card),
);
},
),
);
}
/// 游戏结束界面,显示用时、尝试次数,提供重新游戏和返回主页选项
Widget _buildGameOverScreen() {
return AnimatedOpacity(
opacity: 1.0,
duration: Duration(milliseconds: 600),
child: Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('🎉 恭喜完成!', style: TextStyle(fontSize: 32)),
SizedBox(height: 16),
Text('用时:${formatDuration(_elapsedTime)}'),
Text('尝试次数:${_gameState.moveCount}'),
SizedBox(height: 24),
ElevatedButton.icon(
icon: Icon(Icons.replay),
label: Text('再来一局'),
onPressed: () {
// 重置游戏状态,开始新一局
setState(() {
_gameState.initGame(widget.pairCount);
_selectedCards.clear();
});
},
),
SizedBox(height: 8),
TextButton(
onPressed: () => Navigator.pop(context), // 返回主页
child: Text('返回首页'),
),
],
),
),
);
}
}
核心逻辑:
-
\_selectedCards暂存当前翻开的两张未匹配卡片,当数量达到 2 时,立即进行匹配判断; -
匹配成功:两张卡片标记为
isMatched = true,保持翻开状态;当所有配对完成,游戏结束并停止计时; -
匹配失败:延迟 0.8 秒后,将两张卡片翻回背面,清空暂存列表;
-
计时器每秒刷新一次 AppBar 上的时间显示,确保用时实时更新;
-
游戏结束后,通过
Navigator\.pop将当前难度和用时传回主页,主页据此更新对应难度的最佳时间。
六、运行与测试
按以下步骤运行项目,验证功能是否正常:
-
确保所有 Dart 文件已按上述代码保存至对应目录;
-
在项目根目录(
memory\_flip\_ohos)执行以下命令,下载依赖并运行项目:flutter pub get flutter run \-d ohos -
应用启动后,进行功能测试:
-
点击任一难度卡片,进入游戏页面;
-
卡片默认显示背面(问号),点击可翻开,显示 emoji 图案;
-
翻出两张相同图案的卡片,即配对成功,卡片会锁定并显示金色边框;
-
全部卡片配对完成后,弹出游戏结束界面,显示用时和尝试次数;
-
点击“返回首页”,若当前成绩优于该难度的最佳时间,主页会自动更新最佳时间显示。


-
七、引入三方库的扩展建议
当前项目为简化新手入门流程,未强制使用第三方插件。可根据需求,添加以下三方库丰富功能,扩展方式如下:
7.1 音效反馈 —— audioplayers
为翻牌、配对成功等操作添加音效,提升交互体验,步骤如下:
-
在
pubspec\.yaml中添加依赖:audioplayers: ^6\.1\.0 \# 或使用鸿蒙适配版 audioplayers\_ohos -
在项目根目录创建
assets/audio/文件夹,放置flip\.mp3(翻牌音效)和match\.mp3(配对成功音效); -
在
game\_page\.dart中初始化AudioPlayer,并在翻牌(\_onCardTap)和配对成功时,调用play\(AssetSource\(\.\.\.\)\)播放对应音效。
提示:若鸿蒙平台无官方版 audioplayers,可查找 audioplayers\_ohos 或通过 git 依赖接入适配版本。
7.2 最佳时间持久化 —— shared\_preferences
将各难度最佳时间保存至本地,避免应用退出后数据丢失,步骤如下:
-
在
pubspec\.yaml中添加依赖:shared\_preferences: ^2\.3\.3 \# 或使用鸿蒙适配版 shared\_preferences\_ohos -
在主页
\_updateBestTime方法中,将更新后的最佳时间写入SharedPreferences; -
在主页
initState方法中,读取本地保存的最佳时间,赋值给\_best6、\_best8、\_best10,实现数据持久化。
八、总结
通过本项目,你已掌握在鸿蒙 6.0 上使用 Flutter 构建完整交互式应用的核心流程,具体包括:
-
鸿蒙开发环境的搭建与核心配置检查;
-
Flutter 项目的合理结构组织、数据模型定义、页面路由与数据回传;
-
动画组件(
AnimatedScale、AnimatedSwitcher)的实际运用,提升界面交互质感; -
计时器
Timer与Stopwatch的组合使用,实现游戏计时功能; -
三方库的集成思路,为后续功能扩展打下基础。
后续可自行扩展功能,例如:替换卡片图案为本地图片、增加更多难度档位、接入云端排行榜、添加日/夜间模式等,让这款“记忆翻牌大冒险”更加完善出彩。
更多推荐


所有评论(0)