Flutter 框架跨平台鸿蒙开发 - 虚拟戳戳乐:打造趣味抽奖游戏体验
Flutter虚拟戳戳乐:打造趣味抽奖游戏体验
项目概述
虚拟戳戳乐是一款基于Flutter开发的趣味抽奖游戏应用,完美复现了传统戳戳乐的游戏体验。应用集成了奖品系统、戳洞动画、概率控制、游戏记录、统计分析等核心功能,通过精美的动画效果和直观的用户界面,为用户提供充满惊喜的游戏体验。
运行效果图




应用特色
- 真实游戏体验:5×4网格布局,完美还原传统戳戳乐
- 丰富奖品系统:四个稀有度等级,12种不同奖品
- 精美动画效果:戳洞动画、奖品展示动画、特效动画
- 智能概率控制:科学的奖品分配算法,保证游戏平衡
- 完整记录系统:详细的游戏记录和统计分析
- 成就系统:最佳奖品记录,激励持续游戏
技术架构
核心技术栈
dependencies:
flutter:
sdk: flutter
cupertino_icons: ^1.0.8
项目结构
lib/
├── main.dart # 应用入口和主要逻辑
├── models/ # 数据模型(集成在main.dart中)
│ ├── prize.dart # 奖品模型
│ ├── scratch_hole.dart # 戳戳乐洞位模型
│ ├── game_record.dart # 游戏记录模型
│ └── user_stats.dart # 用户统计模型
├── screens/ # 页面组件(集成在main.dart中)
│ ├── game_page.dart # 游戏页面
│ ├── prize_page.dart # 奖品页面
│ ├── records_page.dart # 记录页面
│ └── stats_page.dart # 统计页面
├── widgets/ # 自定义组件(集成在main.dart中)
│ ├── scratch_hole_widget.dart # 戳戳乐洞位组件
│ ├── prize_card.dart # 奖品卡片
│ └── game_board.dart # 游戏板组件
└── utils/ # 工具类(集成在main.dart中)
├── prize_distributor.dart # 奖品分配器
├── animation_helper.dart # 动画辅助
└── stats_calculator.dart # 统计计算
数据模型设计
奖品模型(Prize)
奖品模型是游戏的核心数据结构,定义了所有可能获得的奖品:
class Prize {
final String id; // 奖品唯一标识
final String name; // 奖品名称
final String description; // 奖品描述
final String emoji; // 奖品图标
final int value; // 奖品价值
final String rarity; // 稀有度等级
final Color color; // 奖品颜色
final bool isSpecial; // 是否为特殊奖品
Prize({
required this.id,
required this.name,
required this.description,
required this.emoji,
required this.value,
required this.rarity,
required this.color,
this.isSpecial = false,
});
}
奖品模型包含稀有度文本转换功能:
String get rarityText {
switch (rarity) {
case 'common':
return '普通';
case 'rare':
return '稀有';
case 'epic':
return '史诗';
case 'legendary':
return '传说';
default:
return '未知';
}
}
戳戳乐洞位模型(ScratchHole)
洞位模型管理每个戳戳乐位置的状态:
class ScratchHole {
final int index; // 洞位索引
Prize? prize; // 包含的奖品
bool isScratched; // 是否已戳开
bool isAnimating; // 是否正在动画
double scratchProgress; // 戳开进度
ScratchHole({
required this.index,
this.prize,
this.isScratched = false,
this.isAnimating = false,
this.scratchProgress = 0.0,
});
}
洞位模型提供状态复制功能:
ScratchHole copyWith({
Prize? prize,
bool? isScratched,
bool? isAnimating,
double? scratchProgress,
}) {
return ScratchHole(
index: index,
prize: prize ?? this.prize,
isScratched: isScratched ?? this.isScratched,
isAnimating: isAnimating ?? this.isAnimating,
scratchProgress: scratchProgress ?? this.scratchProgress,
);
}
游戏记录模型(GameRecord)
游戏记录模型存储每局游戏的详细信息:
class GameRecord {
final String id; // 记录唯一标识
final DateTime playTime; // 游戏时间
final List<Prize> wonPrizes; // 获得的奖品列表
final int totalValue; // 总价值
final int holesScratched; // 戳开的洞位数
GameRecord({
required this.id,
required this.playTime,
required this.wonPrizes,
required this.totalValue,
required this.holesScratched,
});
}
用户统计模型(UserStats)
用户统计模型管理玩家的游戏统计数据:
class UserStats {
int totalGames; // 总游戏次数
int totalPrizes; // 总获得奖品数
int totalValue; // 总价值
Map<String, int> rarityCount; // 稀有度统计
Prize? bestPrize; // 最佳奖品
UserStats({
this.totalGames = 0,
this.totalPrizes = 0,
this.totalValue = 0,
Map<String, int>? rarityCount,
this.bestPrize,
}) : rarityCount = rarityCount ?? {};
}
应用主体结构
应用入口
class MyApp extends StatelessWidget {
const MyApp({super.key});
Widget build(BuildContext context) {
return MaterialApp(
title: '虚拟戳戳乐',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.orange),
useMaterial3: true,
),
home: const ScratchGameHomePage(),
);
}
}
应用采用橙色作为主题色,营造温暖活泼的游戏氛围。
主页面结构
主页面使用底部导航栏实现四个核心功能模块:
class ScratchGameHomePage extends StatefulWidget {
const ScratchGameHomePage({super.key};
State<ScratchGameHomePage> createState() => _ScratchGameHomePageState();
}
class _ScratchGameHomePageState extends State<ScratchGameHomePage>
with TickerProviderStateMixin {
int _selectedIndex = 0;
List<ScratchHole> _holes = []; // 戳戳乐洞位列表
List<Prize> _availablePrizes = []; // 可用奖品列表
List<GameRecord> _gameRecords = []; // 游戏记录列表
UserStats _userStats = UserStats(); // 用户统计数据
bool _isGameActive = false; // 游戏是否进行中
int _remainingHoles = 0; // 剩余洞位数
List<Prize> _currentWonPrizes = []; // 当前获得奖品
// 动画控制器
late AnimationController _scratchAnimationController;
late AnimationController _prizeAnimationController;
late Animation<double> _scratchAnimation;
late Animation<double> _prizeScaleAnimation;
late Animation<double> _prizeRotationAnimation;
}
动画系统设计
动画控制器初始化
应用使用多个动画控制器实现丰富的动画效果:
void _setupAnimations() {
// 戳洞动画控制器
_scratchAnimationController = AnimationController(
duration: const Duration(milliseconds: 800),
vsync: this,
);
// 奖品展示动画控制器
_prizeAnimationController = AnimationController(
duration: const Duration(milliseconds: 1200),
vsync: this,
);
// 戳洞动画
_scratchAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _scratchAnimationController,
curve: Curves.easeOutBack,
));
// 奖品缩放动画
_prizeScaleAnimation = Tween<double>(
begin: 0.0,
end: 1.0,
).animate(CurvedAnimation(
parent: _prizeAnimationController,
curve: const Interval(0.0, 0.6, curve: Curves.elasticOut),
));
// 奖品旋转动画(仅特殊奖品)
_prizeRotationAnimation = Tween<double>(
begin: 0.0,
end: 2 * pi,
).animate(CurvedAnimation(
parent: _prizeAnimationController,
curve: const Interval(0.4, 1.0, curve: Curves.easeInOut),
));
}
动画效果实现
戳洞动画和奖品展示动画的协调配合:
void _scratchHole(ScratchHole hole) {
if (!_isGameActive || hole.isScratched) return;
setState(() {
hole.isScratched = true;
hole.isAnimating = true;
_remainingHoles--;
});
// 开始戳洞动画
_scratchAnimationController.forward().then((_) {
_scratchAnimationController.reset();
});
// 如果有奖品,开始奖品动画
if (hole.prize != null) {
_currentWonPrizes.add(hole.prize!);
_prizeAnimationController.forward().then((_) {
_prizeAnimationController.reset();
setState(() {
hole.isAnimating = false;
});
});
// 显示获奖提示
_showPrizeDialog(hole.prize!);
} else {
setState(() {
hole.isAnimating = false;
});
}
}
游戏页面实现
游戏状态展示
游戏页面顶部展示当前游戏状态:
Widget _buildGamePage() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 游戏状态卡片
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.orange.shade400, Colors.red.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'🎯 戳戳乐游戏',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 8),
Text(
_isGameActive ? '选择一个洞位戳开,看看有什么惊喜!' : '点击"开始新游戏"开始游戏',
style: const TextStyle(
fontSize: 16,
color: Colors.white70,
),
),
const SizedBox(height: 16),
Row(
children: [
_buildGameStatCard('剩余洞位', '$_remainingHoles'),
const SizedBox(width: 16),
_buildGameStatCard('已获奖品', '${_currentWonPrizes.length}'),
],
),
],
),
),
],
),
);
}
戳戳乐游戏板
游戏板采用5×4网格布局,完美还原传统戳戳乐:
Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.brown.shade100,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.brown.shade300, width: 3),
),
child: Column(
children: [
const Text(
'戳戳乐',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.brown,
),
),
const SizedBox(height: 16),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 5,
childAspectRatio: 1.0,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
itemCount: _holes.length,
itemBuilder: (context, index) {
return _buildScratchHole(_holes[index]);
},
),
],
),
),
戳戳乐洞位组件
每个洞位的视觉效果和交互逻辑:
Widget _buildScratchHole(ScratchHole hole) {
return GestureDetector(
onTap: () => _scratchHole(hole),
child: AnimatedBuilder(
animation: _scratchAnimation,
builder: (context, child) {
return Container(
decoration: BoxDecoration(
color: hole.isScratched
? (hole.prize?.color ?? Colors.grey.shade300)
: Colors.brown.shade400,
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: hole.isAnimating
? Colors.yellow
: Colors.brown.shade600,
width: hole.isAnimating ? 3 : 2,
),
boxShadow: hole.isAnimating
? [
BoxShadow(
color: Colors.yellow.withValues(alpha: 0.5),
blurRadius: 10,
spreadRadius: 2,
),
]
: null,
),
child: hole.isScratched
? _buildRevealedPrize(hole)
: _buildUnscratched(hole),
);
},
),
);
}
奖品展示效果
戳开后的奖品展示,包含动画效果:
Widget _buildRevealedPrize(ScratchHole hole) {
if (hole.prize == null) return const SizedBox.shrink();
return AnimatedBuilder(
animation: _prizeAnimationController,
builder: (context, child) {
return Transform.scale(
scale: _prizeScaleAnimation.value,
child: Transform.rotate(
angle: hole.prize!.isSpecial ? _prizeRotationAnimation.value : 0,
child: Container(
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(6),
gradient: LinearGradient(
colors: [
hole.prize!.color.withValues(alpha: 0.3),
hole.prize!.color.withValues(alpha: 0.7),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text(
hole.prize!.emoji,
style: const TextStyle(fontSize: 20),
),
const SizedBox(height: 2),
Text(
'${hole.prize!.value}',
style: const TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
),
);
},
);
}
奖品系统设计
奖品数据初始化
应用包含12种不同的奖品,分为四个稀有度等级:
void _initializeData() {
_availablePrizes = [
// 普通奖品 (50%概率)
Prize(
id: 'common_1',
name: '小糖果',
description: '甜甜的小糖果',
emoji: '🍬',
value: 1,
rarity: 'common',
color: Colors.pink,
),
Prize(
id: 'common_2',
name: '小饼干',
description: '香脆的小饼干',
emoji: '🍪',
value: 2,
rarity: 'common',
color: Colors.brown,
),
// ... 更多普通奖品
// 稀有奖品 (30%概率)
Prize(
id: 'rare_1',
name: '巧克力',
description: '丝滑的巧克力',
emoji: '🍫',
value: 10,
rarity: 'rare',
color: Colors.orange,
),
// ... 更多稀有奖品
// 史诗奖品 (15%概率)
Prize(
id: 'epic_1',
name: '生日蛋糕',
description: '精美的生日蛋糕',
emoji: '🎂',
value: 50,
rarity: 'epic',
color: Colors.green,
),
// ... 更多史诗奖品
// 传说奖品 (5%概率)
Prize(
id: 'legendary_1',
name: '黄金糖果',
description: '传说中的黄金糖果',
emoji: '⭐',
value: 200,
rarity: 'legendary',
color: Colors.amber,
isSpecial: true,
),
Prize(
id: 'legendary_2',
name: '钻石蛋糕',
description: '闪闪发光的钻石蛋糕',
emoji: '💎',
value: 500,
rarity: 'legendary',
color: Colors.blue,
isSpecial: true,
),
];
}
奖品分配算法
科学的概率分配算法确保游戏平衡:
void _distributePrizes() {
final random = Random();
final shuffledHoles = List<ScratchHole>.from(_holes);
shuffledHoles.shuffle();
// 分配奖品概率
final prizeDistribution = <String, double>{
'common': 0.5, // 50% 普通
'rare': 0.3, // 30% 稀有
'epic': 0.15, // 15% 史诗
'legendary': 0.05, // 5% 传说
};
for (int i = 0; i < shuffledHoles.length; i++) {
final hole = shuffledHoles[i];
final randomValue = random.nextDouble();
String selectedRarity = 'common';
double cumulativeRate = 0.0;
// 累积概率计算
for (final entry in prizeDistribution.entries) {
cumulativeRate += entry.value;
if (randomValue <= cumulativeRate) {
selectedRarity = entry.key;
break;
}
}
// 从对应稀有度中随机选择奖品
final availablePrizes = _availablePrizes
.where((prize) => prize.rarity == selectedRarity)
.toList();
if (availablePrizes.isNotEmpty) {
final selectedPrize = availablePrizes[random.nextInt(availablePrizes.length)];
hole.prize = selectedPrize;
}
}
}
奖品展示页面
奖品页面按稀有度分组展示所有奖品:
Widget _buildPrizePage() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 奖品展示头部
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.purple.shade400, Colors.pink.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: const Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
'🎁 奖品大全',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
SizedBox(height: 8),
Text(
'查看所有可能获得的奖品',
style: TextStyle(
fontSize: 16,
color: Colors.white70,
),
),
],
),
),
const SizedBox(height: 24),
// 按稀有度分组展示奖品
...['legendary', 'epic', 'rare', 'common'].map((rarity) {
final prizes = _availablePrizes.where((p) => p.rarity == rarity).toList();
if (prizes.isEmpty) return const SizedBox.shrink();
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Container(
width: 4,
height: 20,
decoration: BoxDecoration(
color: _getRarityColor(rarity),
borderRadius: BorderRadius.circular(2),
),
),
const SizedBox(width: 8),
Text(
'${_getRarityText(rarity)} (${prizes.length})',
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
],
),
const SizedBox(height: 12),
GridView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.2,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: prizes.length,
itemBuilder: (context, index) {
return _buildPrizeCard(prizes[index]);
},
),
const SizedBox(height: 24),
],
);
}).toList(),
],
),
);
}
获奖提示系统
获奖对话框
当玩家获得奖品时,显示精美的获奖对话框:
void _showPrizeDialog(Prize prize) {
showDialog(
context: context,
barrierDismissible: false,
builder: (context) => Dialog(
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
child: Container(
padding: const EdgeInsets.all(24),
decoration: BoxDecoration(
borderRadius: BorderRadius.circular(20),
gradient: LinearGradient(
colors: [
prize.color.withValues(alpha: 0.1),
prize.color.withValues(alpha: 0.3),
],
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
),
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'🎉 恭喜获得 🎉',
style: TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: prize.color,
),
),
const SizedBox(height: 20),
// 奖品展示容器
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
color: prize.color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(20),
border: Border.all(color: prize.color, width: 3),
),
child: Center(
child: Text(
prize.emoji,
style: const TextStyle(fontSize: 48),
),
),
),
const SizedBox(height: 16),
Text(
prize.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 8),
Text(
prize.description,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
// 奖品信息标签
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6),
decoration: BoxDecoration(
color: prize.color,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(Icons.star, color: Colors.white, size: 16),
const SizedBox(width: 4),
Text(
'价值:${prize.value}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
],
),
),
const SizedBox(width: 12),
Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: _getRarityColor(prize.rarity).withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: _getRarityColor(prize.rarity)),
),
child: Text(
prize.rarityText,
style: TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: _getRarityColor(prize.rarity),
),
),
),
],
),
const SizedBox(height: 20),
SizedBox(
width: double.infinity,
child: ElevatedButton(
onPressed: () => Navigator.of(context).pop(),
style: ElevatedButton.styleFrom(
backgroundColor: prize.color,
foregroundColor: Colors.white,
),
child: const Text('太棒了!'),
),
),
],
),
),
),
);
}
游戏记录系统
记录页面设计
游戏记录页面展示所有游戏历史:
Widget _buildRecordsPage() {
if (_gameRecords.isEmpty) {
return const Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(Icons.history, size: 64, color: Colors.grey),
SizedBox(height: 16),
Text(
'暂无游戏记录',
style: TextStyle(fontSize: 18, color: Colors.grey),
),
SizedBox(height: 8),
Text(
'完成一局游戏后会显示在这里',
style: TextStyle(fontSize: 14, color: Colors.grey),
),
],
),
);
}
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(
'游戏记录 (${_gameRecords.length})',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
TextButton(
onPressed: _clearRecords,
child: const Text('清空记录'),
),
],
),
const SizedBox(height: 16),
ListView.builder(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
itemCount: _gameRecords.length,
itemBuilder: (context, index) {
final record = _gameRecords[index];
return Container(
margin: const EdgeInsets.only(bottom: 12),
child: Card(
child: ExpansionTile(
leading: Container(
width: 48,
height: 48,
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.blue.shade400, Colors.purple.shade400],
),
borderRadius: BorderRadius.circular(8),
),
child: Center(
child: Text(
'${index + 1}',
style: const TextStyle(
color: Colors.white,
fontWeight: FontWeight.bold,
),
),
),
),
title: Text('游戏 ${index + 1}'),
subtitle: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('获得 ${record.wonPrizes.length} 个奖品,总价值 ${record.totalValue}'),
Text(
_formatDateTime(record.playTime),
style: const TextStyle(fontSize: 12),
),
],
),
children: [
if (record.wonPrizes.isNotEmpty)
Container(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'获得奖品:',
style: TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
Wrap(
spacing: 8,
runSpacing: 8,
children: record.wonPrizes.map((prize) {
return Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: prize.color.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: prize.color),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(prize.emoji, style: const TextStyle(fontSize: 12)),
const SizedBox(width: 4),
Text(
prize.name,
style: const TextStyle(fontSize: 10),
),
const SizedBox(width: 4),
Text(
'(${prize.value})',
style: TextStyle(
fontSize: 10,
color: prize.color,
fontWeight: FontWeight.bold,
),
),
],
),
);
}).toList(),
),
],
),
),
],
),
),
);
},
),
],
),
);
}
统计分析系统
统计页面设计
统计页面提供详细的游戏数据分析:
Widget _buildStatsPage() {
return SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// 统计概览
Container(
width: double.infinity,
padding: const EdgeInsets.all(20),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [Colors.green.shade400, Colors.teal.shade400],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
),
borderRadius: BorderRadius.circular(16),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
'📊 游戏统计',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
const SizedBox(height: 16),
Row(
children: [
_buildStatsCard('游戏次数', '${_userStats.totalGames}'),
const SizedBox(width: 16),
_buildStatsCard('获得奖品', '${_userStats.totalPrizes}'),
],
),
const SizedBox(height: 12),
Row(
children: [
_buildStatsCard('总价值', '${_userStats.totalValue}'),
const SizedBox(width: 16),
_buildStatsCard('平均价值', _getAverageValue()),
],
),
],
),
),
const SizedBox(height: 24),
// 稀有度统计
const Text(
'稀有度统计',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
...['legendary', 'epic', 'rare', 'common'].map((rarity) {
final count = _userStats.rarityCount[rarity] ?? 0;
final color = _getRarityColor(rarity);
final percentage = _userStats.totalPrizes > 0
? (count / _userStats.totalPrizes * 100).toStringAsFixed(1)
: '0.0';
return Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: color.withValues(alpha: 0.1),
borderRadius: BorderRadius.circular(8),
border: Border.all(color: color.withValues(alpha: 0.3)),
),
child: Row(
children: [
Container(
width: 12,
height: 12,
decoration: BoxDecoration(
color: color,
shape: BoxShape.circle,
),
),
const SizedBox(width: 12),
Expanded(
child: Text(
_getRarityText(rarity),
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
Text(
'$count ($percentage%)',
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
),
),
],
),
);
}).toList(),
],
),
);
}
最佳奖品展示
统计页面还展示玩家获得的最佳奖品:
// 最佳奖品
if (_userStats.bestPrize != null) ...[
const Text(
'最佳奖品',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Container(
width: double.infinity,
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: LinearGradient(
colors: [
_userStats.bestPrize!.color.withValues(alpha: 0.2),
_userStats.bestPrize!.color.withValues(alpha: 0.4),
],
),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: _userStats.bestPrize!.color, width: 2),
),
child: Row(
children: [
Text(
_userStats.bestPrize!.emoji,
style: const TextStyle(fontSize: 48),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
_userStats.bestPrize!.name,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
Text(
_userStats.bestPrize!.description,
style: TextStyle(
fontSize: 14,
color: Colors.grey.shade600,
),
),
const SizedBox(height: 8),
Row(
children: [
Container(
padding: const EdgeInsets.symmetric(
horizontal: 8,
vertical: 4,
),
decoration: BoxDecoration(
color: _userStats.bestPrize!.color,
borderRadius: BorderRadius.circular(12),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
const Icon(
Icons.star,
size: 12,
color: Colors.white,
),
const SizedBox(width: 4),
Text(
'${_userStats.bestPrize!.value}',
style: const TextStyle(
fontSize: 12,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
],
),
),
const SizedBox(width: 8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: 6,
vertical: 2,
),
decoration: BoxDecoration(
color: _getRarityColor(_userStats.bestPrize!.rarity)
.withValues(alpha: 0.2),
borderRadius: BorderRadius.circular(8),
border: Border.all(
color: _getRarityColor(_userStats.bestPrize!.rarity),
),
),
child: Text(
_userStats.bestPrize!.rarityText,
style: TextStyle(
fontSize: 10,
fontWeight: FontWeight.bold,
color: _getRarityColor(_userStats.bestPrize!.rarity),
),
),
),
],
),
],
),
),
],
),
),
],
游戏结束处理
游戏结束逻辑
当所有洞位都被戳开或玩家主动结束游戏时:
void _endCurrentGame() {
if (!_isGameActive) return;
final gameRecord = GameRecord(
id: 'game_${DateTime.now().millisecondsSinceEpoch}',
playTime: DateTime.now(),
wonPrizes: List.from(_currentWonPrizes),
totalValue: _currentWonPrizes.fold(0, (sum, prize) => sum + prize.value),
holesScratched: 20 - _remainingHoles,
);
setState(() {
_isGameActive = false;
_gameRecords.insert(0, gameRecord);
// 更新统计
_userStats.totalGames++;
_userStats.totalPrizes += _currentWonPrizes.length;
_userStats.totalValue += gameRecord.totalValue;
// 更新稀有度统计
for (final prize in _currentWonPrizes) {
_userStats.rarityCount[prize.rarity] =
(_userStats.rarityCount[prize.rarity] ?? 0) + 1;
// 更新最佳奖品
if (_userStats.bestPrize == null || prize.value > _userStats.bestPrize!.value) {
_userStats.bestPrize = prize;
}
}
});
// 显示游戏结束对话框
_showGameEndDialog(gameRecord);
}
游戏结束对话框
游戏结束时显示本轮游戏总结:
void _showGameEndDialog(GameRecord record) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('🎮 游戏结束'),
content: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('本轮获得 ${record.wonPrizes.length} 个奖品'),
Text('总价值:${record.totalValue}'),
Text('戳开洞位:${record.holesScratched}/20'),
if (record.wonPrizes.isNotEmpty) ...[
const SizedBox(height: 12),
const Text('获得奖品:', style: TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
...record.wonPrizes.map((prize) => Text('${prize.emoji} ${prize.name} (${prize.value})')),
],
],
),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: const Text('查看记录'),
),
ElevatedButton(
onPressed: () {
Navigator.of(context).pop();
_initializeNewGame();
},
child: const Text('再来一局'),
),
],
),
);
}
工具函数
稀有度相关函数
处理稀有度颜色和文本转换:
Color _getRarityColor(String rarity) {
switch (rarity) {
case 'common':
return Colors.grey;
case 'rare':
return Colors.blue;
case 'epic':
return Colors.purple;
case 'legendary':
return Colors.orange;
default:
return Colors.grey;
}
}
String _getRarityText(String rarity) {
switch (rarity) {
case 'common':
return '普通';
case 'rare':
return '稀有';
case 'epic':
return '史诗';
case 'legendary':
return '传说';
default:
return '未知';
}
}
统计计算函数
计算平均价值和格式化时间:
String _getAverageValue() {
if (_userStats.totalPrizes == 0) return '0';
return (_userStats.totalValue / _userStats.totalPrizes).toStringAsFixed(1);
}
String _formatDateTime(DateTime dateTime) {
return '${dateTime.month}-${dateTime.day} ${dateTime.hour.toString().padLeft(2, '0')}:${dateTime.minute.toString().padLeft(2, '0')}';
}
项目总结
虚拟戳戳乐应用成功实现了完整的抽奖游戏体验,通过精心设计的概率系统、丰富的动画效果和完善的数据管理,为用户提供了充满惊喜和乐趣的游戏体验。
技术亮点
- 复杂动画系统:多层次动画效果,包括戳洞、奖品展示、特效动画
- 科学概率算法:平衡的奖品分配系统,确保游戏公平性
- 完整数据管理:游戏记录、统计分析、用户数据持久化
- 响应式UI设计:适配不同屏幕尺寸,优秀的用户体验
- 状态管理:复杂的游戏状态管理和动画协调
功能特色
- 20个洞位的完整戳戳乐游戏板
- 12种不同奖品,四个稀有度等级
- 精美的获奖动画和提示系统
- 详细的游戏记录和统计分析
- 最佳奖品记录和成就系统
- 完整的数据清理和重置功能
扩展方向
- 社交功能:好友系统、排行榜、分享功能
- 更多游戏模式:限时挑战、特殊活动、主题活动
- 奖品系统扩展:更多奖品类型、季节性奖品、限定奖品
- 音效系统:戳洞音效、获奖音效、背景音乐
- 数据持久化:本地存储、云端同步、跨设备数据
- 付费功能:购买游戏次数、特殊道具、VIP特权
通过本教程的学习,你已经掌握了Flutter游戏开发的核心技能,包括复杂动画、概率算法、状态管理和数据分析。这些技能可以应用到各种游戏和娱乐应用的开发中。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)