Flutter for OpenHarmony 游戏中心App实战:数字消除游戏实现
摘要:本文介绍了如何实现一个数字消除游戏,通过Dart语言和Flutter框架构建4x4数字网格。游戏随机生成1-9的数字和5-19的目标值,玩家需选择和等于目标的数字组合。文章详细讲解了游戏状态管理、随机数生成、选择逻辑和总和检查等核心功能实现,并展示了UI构建过程。该游戏考验数学计算和策略思维,通过实时反馈和动态更新提升游戏体验。(149字)

在前面的文章中,我们实现了拼图、记忆翻牌和知识问答三个游戏,学习了状态管理、定时器和数据结构等核心技能。这次我们要实现一个数字消除游戏,这是一个考验数学计算和策略思维的游戏。通过实现这个游戏,你将学习到如何处理数字运算、实现选择逻辑、动态生成游戏内容等技能。
数字消除游戏的玩法
数字消除游戏的玩法很有趣:屏幕上显示一个4x4的数字网格,每个格子里是1到9的随机数字。游戏会给出一个目标数字,玩家需要选择若干个数字,使它们的和等于目标数字。选对后,这些数字会被消除,生成新的数字和新的目标,得分增加。玩家需要快速计算和选择,找到正确的数字组合。
这种玩法简单但有挑战性,既考验玩家的数学计算能力,也考验策略思维。有时候可能有多种组合方式,玩家需要选择最优的方案。游戏的节奏紧凑,每次成功消除都会带来成就感,激发玩家继续挑战的欲望。
页面结构的搭建
NumberGamePage是一个有状态组件,需要管理数字网格和游戏进度:
class NumberGamePage extends StatefulWidget {
const NumberGamePage({super.key});
State<NumberGamePage> createState() => _NumberGamePageState();
}
使用StatefulWidget是因为游戏状态会随着玩家的操作不断变化。每次玩家选择数字,选中状态就会改变。每次成功消除,数字网格就会重新生成。这些变化都需要通过State类来管理,并通过setState方法触发UI更新。相比前面的游戏,数字消除的状态管理涉及到更多的数学计算。
游戏状态的定义
在State类中,我们需要定义游戏的核心状态:
class _NumberGamePageState extends State<NumberGamePage> {
List<int> numbers = [];
int target = 0;
int score = 0;
List<int> selected = [];
void initState() {
super.initState();
_generateNumbers();
}
numbers是一个包含16个整数的列表,表示4x4网格中每个位置的数字。初始为空列表,会在initState中生成。target是目标数字,玩家需要选择数字使其和等于这个值。score记录玩家的得分,每次成功消除增加10分。selected是一个索引列表,记录玩家选中的数字位置。
initState方法在页面创建时调用一次,我们在这里调用_generateNumbers方法生成初始的数字网格和目标。这种在initState中初始化游戏的做法,确保用户看到页面时游戏已经准备好了。与前面的游戏不同,这个游戏的初始状态需要通过计算生成,而不是固定的。
数字生成逻辑
游戏开始和每次成功消除后,都需要生成新的数字网格和目标:
void _generateNumbers() {
final random = Random();
setState(() {
numbers = List.generate(16, (_) => random.nextInt(9) + 1);
target = random.nextInt(15) + 5;
selected.clear();
});
}
使用Random类生成随机数。List.generate创建一个包含16个元素的列表,每个元素是1到9的随机整数。random.nextInt(9)生成0到8的随机数,加1后得到1到9。target设置为5到19的随机数,这个范围确保游戏有合适的难度,既不会太简单也不会太难。
selected.clear()清空选中列表,准备新一轮的选择。这里使用setState包裹状态更新,触发UI重新构建,新的数字会立即显示在屏幕上。每次生成的数字和目标都不同,确保游戏的可玩性和挑战性。
Random类的使用很简单,但背后的随机算法很复杂。Dart使用线性同余生成器,这是一个经典的伪随机数生成算法。虽然不是真正的随机,但对于游戏来说已经足够了。如果需要更高质量的随机数,可以使用Random.secure(),但性能会稍差。
数字选择逻辑
当玩家点击数字时,需要处理选择逻辑:
void _selectNumber(int index) {
if (selected.contains(index)) {
setState(() => selected.remove(index));
} else {
setState(() => selected.add(index));
}
_checkSum();
}
_selectNumber方法接收数字的索引作为参数。首先检查这个索引是否已经在selected列表中。如果在,说明这个数字已经被选中,点击是取消选择,从列表中移除。如果不在,说明这是新选择,添加到列表中。
这种切换选择状态的逻辑很常见,在很多应用中都会用到。使用contains方法检查元素是否存在,使用remove方法移除元素,使用add方法添加元素。这些都是List的基本操作,简单但实用。
每次选择改变后,立即调用_checkSum方法检查总和是否等于目标。这种实时检查的设计,让玩家可以立即看到结果,不需要额外的确认操作。如果总和正确,游戏会自动进入下一轮,保持流畅的游戏节奏。
总和检查逻辑
检查选中数字的总和是否等于目标是游戏的核心逻辑:
void _checkSum() {
int sum = selected.fold(0, (prev, i) => prev + numbers[i]);
if (sum == target) {
setState(() {
score += 10;
_generateNumbers();
});
}
}
使用fold方法计算总和。fold是一个归约操作,接收初始值和归约函数。归约函数接收累积值prev和当前元素i,返回新的累积值。这里我们将selected列表中的索引映射到numbers列表中的数字,然后累加。
fold方法虽然看起来复杂,但其实很强大。它可以将列表归约成单个值,不仅可以用于求和,还可以用于求积、求最大值、字符串拼接等。掌握fold方法,可以让代码更加简洁优雅。
如果总和等于目标,增加得分,调用_generateNumbers生成新的数字和目标。这里使用setState包裹状态更新,触发UI重新构建。玩家会看到得分增加,数字网格刷新,新的目标出现,可以继续下一轮游戏。
页面UI的构建
游戏页面的UI包括AppBar、得分显示、目标显示和数字网格:
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('数字消除'),
backgroundColor: const Color(0xFF16213e),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('得分: $score',
style: TextStyle(fontSize: 24.sp,
fontWeight: FontWeight.bold,
color: Colors.amber)),
SizedBox(height: 20.h),
AppBar的标题显示"数字消除",背景色使用深蓝色。Column垂直排列页面内容,mainAxisAlignment设置为center让内容居中显示。最上面显示得分,使用24号粗体琥珀色字体,非常醒目。琥珀色给人一种金色的感觉,象征着成就和奖励。
目标和选中总和的显示:
Text('目标数字: $target',
style: TextStyle(fontSize: 28.sp,
fontWeight: FontWeight.bold)),
SizedBox(height: 10.h),
Text('选中总和: ${selected.fold(0, (prev, i) => prev + numbers[i])}',
style: TextStyle(fontSize: 18.sp)),
SizedBox(height: 30.h),
目标数字使用28号粗体字显示,让玩家清楚地看到需要达到的目标。选中总和实时计算并显示,使用与_checkSum相同的fold方法。这种实时反馈让玩家可以随时知道当前选择的总和,不需要自己心算。
注意这里在build方法中直接计算选中总和,虽然每次重建都会计算,但计算量很小,不会影响性能。如果计算量很大,可以考虑将结果缓存到状态变量中,避免重复计算。
数字网格的实现
数字网格是游戏的核心部分,展示所有的数字:
Container(
width: 320.w,
height: 320.w,
padding: EdgeInsets.all(8.w),
child: GridView.builder(
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 4,
crossAxisSpacing: 8,
mainAxisSpacing: 8,
),
Container设置宽度和高度都为320个单位,创建一个正方形的游戏区域。padding设置8个单位的内边距。GridView.builder用于构建4x4的网格,physics设置为NeverScrollableScrollPhysics禁用滚动。gridDelegate定义网格布局:crossAxisCount为4表示每行4个数字,crossAxisSpacing和mainAxisSpacing设置数字之间的间距为8个单位。
这种4x4的布局可以容纳16个数字,在手机屏幕上显示效果很好。数字之间的间距让每个数字都有明确的边界,玩家不会误触。Container的尺寸经过精心设计,既不会太大占满整个屏幕,也不会太小看不清数字。
数字格子的构建:
itemCount: 16,
itemBuilder: (context, index) {
return GestureDetector(
onTap: () => _selectNumber(index),
child: Container(
decoration: BoxDecoration(
color: selected.contains(index)
? Colors.amber
: Colors.purpleAccent,
borderRadius: BorderRadius.circular(12.r),
),
child: Center(
child: Text('${numbers[index]}',
style: TextStyle(fontSize: 28.sp,
fontWeight: FontWeight.bold)),
),
),
);
},
),
),
],
),
);
}
itemCount设置为16,表示网格中有16个数字。itemBuilder负责构建每个数字格子,GestureDetector包裹格子,点击时调用_selectNumber方法。Container的颜色根据选中状态决定:如果被选中使用琥珀色,否则使用紫色。borderRadius设置12个单位的圆角,让格子看起来更加柔和。
数字使用28号粗体字显示,居中对齐。这种大号的数字在小小的格子中非常醒目,玩家可以清楚地看到每个数字。琥珀色的选中状态与紫色的未选中状态有明显的视觉区别,玩家可以清楚地看到自己选择了哪些数字。
fold方法的深入理解
fold是Dart中非常强大的列表操作方法,让我们深入了解一下它的用法:
int sum = selected.fold(0, (prev, i) => prev + numbers[i]);
fold方法接收两个参数:初始值和归约函数。初始值是0,表示从0开始累加。归约函数接收两个参数:prev是累积值,i是当前元素。函数返回新的累积值。
fold的执行过程是这样的:首先将初始值0作为prev,selected的第一个元素作为i,调用归约函数得到新的prev。然后将新的prev和selected的第二个元素作为i,再次调用归约函数。重复这个过程,直到处理完所有元素,最后的prev就是结果。
除了求和,fold还可以用于很多其他场景:
// 求积
int product = numbers.fold(1, (prev, n) => prev * n);
// 求最大值
int max = numbers.fold(numbers[0], (prev, n) => prev > n ? prev : n);
// 字符串拼接
String str = words.fold('', (prev, w) => prev + w);
// 构建Map
Map<int, int> map = numbers.fold({}, (prev, n) {
prev[n] = (prev[n] ?? 0) + 1;
return prev;
});
fold是函数式编程的核心概念之一,掌握它可以让代码更加简洁优雅。虽然也可以用for循环实现相同的功能,但fold更加声明式,表达了"归约"这个操作的本质。
随机数生成的策略
游戏的可玩性很大程度上取决于随机数生成的策略。让我们深入分析一下我们的策略:
numbers = List.generate(16, (_) => random.nextInt(9) + 1);
target = random.nextInt(15) + 5;
数字范围是1到9,这个范围既不会太小导致重复太多,也不会太大导致难以计算。目标范围是5到19,这个范围确保至少需要选择2个数字(最小的情况是1+1+1+1+1=5),最多可能需要选择多个数字(比如9+9+1=19)。
这种随机策略确保游戏有合适的难度。如果目标太小,比如2或3,玩家只需要选择一两个数字就能完成,太简单。如果目标太大,比如50,可能需要选择很多数字,甚至可能无解,太难。
可以根据玩家的得分动态调整难度:
void _generateNumbers() {
final random = Random();
final difficulty = min(score ~/ 50, 5); // 难度等级0-5
setState(() {
numbers = List.generate(16, (_) => random.nextInt(9 + difficulty) + 1);
target = random.nextInt(15 + difficulty * 3) + 5;
selected.clear();
});
}
随着得分增加,数字范围和目标范围都会增大,游戏难度逐渐提高。这种动态难度调整可以让游戏保持挑战性,避免玩家感到无聊。
游戏策略的思考
数字消除游戏不仅考验计算能力,还考验策略思维。有时候可能有多种组合方式达到目标,玩家需要选择最优的方案。
比如目标是10,网格中有[1,2,3,4,5,6,7,8,9,1,2,3,4,5,6,7]。可能的组合有:
- 1+9 = 10
- 2+8 = 10
- 3+7 = 10
- 4+6 = 10
- 1+2+3+4 = 10
- 1+2+7 = 10
- …
选择哪种组合呢?从策略角度,应该选择使用数字最少的组合,这样可以保留更多的数字用于下一轮。但从游戏设计角度,我们没有限制玩家的选择,任何正确的组合都可以。
可以添加奖励机制,鼓励玩家使用更少的数字:
void _checkSum() {
int sum = selected.fold(0, (prev, i) => prev + numbers[i]);
if (sum == target) {
setState(() {
score += 10 + (5 - selected.length) * 2; // 使用数字越少,奖励越多
_generateNumbers();
});
}
}
这种奖励机制可以增加游戏的策略性,让玩家不仅要找到正确的组合,还要找到最优的组合。
用户体验的优化
数字消除游戏的用户体验设计注重了几个方面。首先是实时反馈,选中总和实时显示,玩家可以随时知道当前的状态。其次是自动检查,不需要额外的确认按钮,选对后自动进入下一轮。
颜色的选择也很重要。琥珀色的选中状态与紫色的未选中状态有明显的对比,玩家可以清楚地看到自己的选择。得分使用琥珀色显示,给人一种成就感。
可以添加更多的反馈机制提升体验:
void _checkSum() {
int sum = selected.fold(0, (prev, i) => prev + numbers[i]);
if (sum == target) {
// 播放成功音效
// AudioPlayer.play('success.mp3');
// 显示成功动画
// showDialog(context: context, builder: (_) => SuccessDialog());
setState(() {
score += 10;
_generateNumbers();
});
} else if (sum > target) {
// 总和超过目标,给出提示
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('总和超过目标了!')),
);
}
}
这些反馈机制可以让游戏更加生动有趣,提高玩家的参与度。
性能优化的考虑
数字消除游戏的性能表现很好,因为游戏逻辑简单,UI更新频率不高。每次选择数字时,只有selected列表会改变,Flutter会精确地重建受影响的Widget。
GridView.builder只渲染16个数字,即使频繁更新也不会影响性能。fold方法虽然看起来复杂,但实际上只是简单的循环累加,计算量很小。
需要注意的是,我们在build方法中计算选中总和:
Text('选中总和: ${selected.fold(0, (prev, i) => prev + numbers[i])}')
这意味着每次重建都会计算一次。虽然计算量很小,但如果selected列表很长,可能会影响性能。可以将结果缓存到状态变量中:
int selectedSum = 0;
void _selectNumber(int index) {
setState(() {
if (selected.contains(index)) {
selected.remove(index);
} else {
selected.add(index);
}
selectedSum = selected.fold(0, (prev, i) => prev + numbers[i]);
});
_checkSum();
}
这样只在选择改变时计算一次,build方法中直接使用缓存的值。这是一个经典的空间换时间的优化策略。
扩展功能的思考
数字消除游戏还有很多可以扩展的功能。比如可以添加时间限制,增加游戏的紧张感。可以添加提示功能,帮助玩家找到正确的组合。可以添加难度选择,让玩家可以选择不同的挑战。
还可以添加特殊数字,比如乘法数字(选中后将其他数字乘以2)、减法数字(可以减少总和)等。可以添加连击系统,连续成功消除可以获得额外奖励。可以添加排行榜,让玩家可以与其他玩家比较成绩。
这些扩展功能的实现都不需要大幅修改现有代码。比如添加时间限制,只需要添加一个Timer和一个时间变量。添加特殊数字,只需要在numbers列表中添加特殊标记,在计算总和时特殊处理。良好的代码组织让扩展变得容易。
总结
本文详细介绍了数字消除游戏的实现。我们从游戏玩法设计开始,定义了游戏状态,实现了数字生成、选择和检查等核心逻辑,最后构建了游戏的UI。每个部分都有详细的代码和讲解,帮助你理解实现的原理。
数字消除游戏涉及到数学计算和策略思维,通过实现这个游戏,你学习到了如何使用Random生成随机数,如何使用fold方法进行列表归约,如何实现实时反馈的交互逻辑。我们还深入探讨了随机数生成策略、游戏平衡性设计、性能优化等高级话题。
这些技能不仅适用于数字消除游戏,也适用于其他需要数学计算和策略思维的应用。fold方法是函数式编程的核心概念,掌握它可以让代码更加简洁优雅。随机数生成在很多游戏中都会用到,了解其原理和策略很重要。
接下来的文章中,我们会继续实现其他游戏。每个游戏都有不同的玩法和实现方式,通过学习这些游戏,你将掌握更多的开发技巧,成为一名优秀的Flutter开发者。
欢迎加入开源鸿蒙跨平台社区:https://openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)