Flutter for OpenHarmony游戏集合App实战之数字拼图滑动交换

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

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

前言

上一篇讲了数字拼图的打乱,这篇来聊聊滑动交换的逻辑。

数字拼图的玩法是点击空格旁边的数字,数字就会滑到空格位置。只有和空格相邻的数字才能移动。

点击处理

dart 复制代码
void _tap(int index) {
  int empty = tiles.indexOf(0);
  bool canSwap = (index == empty - 1 && empty % size > 0) ||
                 (index == empty + 1 && empty % size < size - 1) ||
                 (index == empty - size) || (index == empty + size);
  if (canSwap) {
    setState(() {
      tiles[empty] = tiles[index];
      tiles[index] = 0;
      moves++;
      if (_checkWin()) _showWinDialog();
    });
  }
}

这是点击处理的核心方法,接收点击的格子索引作为参数。让我逐段解释:

找到空格

dart 复制代码
int empty = tiles.indexOf(0);

indexOf方法在列表中查找元素,返回第一个匹配元素的索引。

这里找到0(空格)的位置。因为每次移动后空格位置会变,所以每次点击都要重新找。

判断能否交换

dart 复制代码
bool canSwap = (index == empty - 1 && empty % size > 0) ||
               (index == empty + 1 && empty % size < size - 1) ||
               (index == empty - size) || (index == empty + size);

这是一个复合布尔表达式,用||连接四个条件,任一条件为真就可以交换。

点击的位置必须和空格相邻才能交换。让我详细解释四个条件:

左边相邻

dart 复制代码
index == empty - 1 && empty % size > 0

这个条件判断点击位置是否在空格左边:

  • index == empty - 1:点击位置的索引比空格小1
  • empty % size > 0:空格不在最左列(列号大于0)

为什么要第二个条件?看这个例子:

复制代码
索引布局:
0  1  2  3
4  5  6  7
8  9  10 11
12 13 14 15

如果空格在索引4(第二行第一个),索引3(第一行最后一个)虽然比4小1,但它们不相邻!

empty % size得到空格的列号,如果是0说明在最左列,左边没有格子。

右边相邻

dart 复制代码
index == empty + 1 && empty % size < size - 1

同样的逻辑:

  • index == empty + 1:点击位置的索引比空格大1
  • empty % size < size - 1:空格不在最右列(列号小于3)

如果空格在索引3(第一行最后一个),索引4虽然比3大1,但它们不相邻。

上边相邻

dart 复制代码
index == empty - size

点击位置的索引比空格小size(4),说明在上一行同一列。

这个不需要额外的边界检查,因为如果空格在第一行,empty - size会是负数,不可能等于任何有效索引。

下边相邻

dart 复制代码
index == empty + size

点击位置的索引比空格大size(4),说明在下一行同一列。

同样不需要额外检查,如果空格在最后一行,empty + size会超出数组范围。

为什么左右要额外检查而上下不用

一维数组里,索引连续但位置不一定相邻。

复制代码
索引3和4的位置:
0  1  2  [3]
[4] 5  6  7

索引3和4相差1,但在棋盘上一个在右边缘,一个在左边缘,不相邻。

而索引0和4相差4(size),在棋盘上是上下关系,一定相邻。

所以左右移动要检查empty % size,确保不跨行;上下移动不需要额外检查。

执行交换

dart 复制代码
setState(() {
  tiles[empty] = tiles[index];
  tiles[index] = 0;
  moves++;
  if (_checkWin()) _showWinDialog();
});

如果可以交换,在setState里执行:

  1. 把点击位置的数字移到空格位置
  2. 把点击位置设为0(变成新的空格)
  3. 步数加1
  4. 检查是否胜利

为什么用setState

setState通知Flutter框架状态变了,需要重新build。

如果不用setState,tiles数组虽然变了,但UI不会更新,玩家看不到变化。

胜利检查

dart 复制代码
bool _checkWin() {
  for (int i = 0; i < tiles.length - 1; i++) {
    if (tiles[i] != i + 1) return false;
  }
  return tiles.last == 0;
}

这个方法检查拼图是否完成。

检查前15个位置

dart 复制代码
for (int i = 0; i < tiles.length - 1; i++) {
  if (tiles[i] != i + 1) return false;
}

遍历索引0到14(tiles.length - 1 = 15,不包括15):

  • 索引0应该是1
  • 索引1应该是2
  • ...
  • 索引14应该是15

如果任何一个位置的值不等于索引+1,立即返回false。

检查最后一个位置

dart 复制代码
return tiles.last == 0;

tiles.last是列表的最后一个元素,应该是0(空格)。

正确顺序是[1, 2, 3, ..., 15, 0]。

为什么分开检查

可以合并成一个循环:

dart 复制代码
bool _checkWin() {
  for (int i = 0; i < tiles.length; i++) {
    int expected = (i + 1) % tiles.length;  // 0,1,2,...,15 -> 1,2,3,...,0
    if (tiles[i] != expected) return false;
  }
  return true;
}

但分开写更清晰,而且性能差不多。

点击事件绑定

dart 复制代码
return GestureDetector(
  onTap: () => _tap(i),
  child: Container(...),
);

每个数字块都用GestureDetector包裹,检测点击事件。

  • onTap:点击回调
  • () => _tap(i):箭头函数,调用_tap并传入当前索引i

为什么用箭头函数

dart 复制代码
// 正确
onTap: () => _tap(i),

// 错误!
onTap: _tap(i),  // 这会立即调用_tap,而不是等点击时调用

onTap需要一个函数,不是函数的返回值。

() => _tap(i)创建一个新函数,点击时才执行。

空格不绑定事件

dart 复制代码
if (value == 0) return Container(...);  // 没有GestureDetector
return GestureDetector(...);

空格的渲染代码在前面,直接return了Container,没有包裹GestureDetector。

点击空格没有意义,所以不需要处理。

步数显示

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

顶部显示当前步数。

  • $moves:字符串插值,把moves变量的值插入字符串
  • fontSize: 20:字号20,比较醒目

玩家可以追求更少步数完成。15拼图的最优解通常在50-80步之间。

提示文字

dart 复制代码
const Padding(padding: EdgeInsets.all(16), child: Text('点击数字移动到空位', style: TextStyle(color: Colors.grey))),

底部灰色提示,告诉玩家怎么操作。

  • color: Colors.grey:灰色,不抢眼
  • const:整个Widget是编译时常量,性能更好

胜利对话框

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('再来一局'))],
  ));
}

游戏胜利时弹出对话框。

showDialog

dart 复制代码
showDialog(context: context, builder: (_) => AlertDialog(...))

showDialog是Flutter显示对话框的方法:

  • context:BuildContext,用于定位对话框
  • builder:构建对话框的函数,参数是context(这里用_忽略)

AlertDialog

dart 复制代码
AlertDialog(
  title: const Text('恭喜!'),
  content: Text('完成拼图! 步数: $moves'),
  actions: [...],
)

Material Design的标准对话框:

  • title:标题
  • content:内容,显示步数
  • actions:底部按钮

再来一局按钮

dart 复制代码
TextButton(
  onPressed: () { Navigator.pop(context); setState(_initGame); },
  child: const Text('再来一局'),
)

点击按钮时:

  1. Navigator.pop(context):关闭对话框
  2. setState(_initGame):重新初始化游戏

动画效果

当前实现是瞬间交换,没有动画。如果想加滑动动画,可以用AnimatedPositioned:

dart 复制代码
AnimatedPositioned(
  duration: Duration(milliseconds: 200),
  left: (index % size) * cellSize,
  top: (index ~/ size) * cellSize,
  child: ...,
)

但这需要改用Stack布局,每个数字块用Positioned定位,比较复杂。

另一种方案是用AnimatedSwitcher:

dart 复制代码
AnimatedSwitcher(
  duration: Duration(milliseconds: 200),
  child: Container(
    key: ValueKey(tiles[i]),  // key变化时触发动画
    ...
  ),
)

当前简化处理,没有动画也能玩。

索引和坐标的转换

一维索引和二维坐标的转换公式:

dart 复制代码
// 索引 -> 坐标
int row = index ~/ size;  // 整除得到行号
int col = index % size;   // 取余得到列号

// 坐标 -> 索引
int index = row * size + col;

举例,size=4时:

  • 索引5:row = 5 ~/ 4 = 1,col = 5 % 4 = 1,即第2行第2列
  • 第3行第2列:index = 2 * 4 + 1 = 9

这个转换在很多地方用到,比如判断边界、计算相邻位置、渲染时计算坐标。

完整的游戏流程

  1. 初始化:生成有序数组,打乱
  2. 渲染:GridView显示16个格子
  3. 点击:判断是否相邻,交换
  4. 检查:每次移动后检查是否胜利
  5. 胜利:弹出对话框,可以重新开始

小结

这篇讲了数字拼图的滑动交换,核心知识点:

  • indexOf找空格: 每次点击都要重新找0的位置
  • 相邻判断: 四个方向的条件,左右要额外检查边界
  • 边界检查 : empty % size判断列号,防止跨行
  • 数组交换: 两行代码完成,先移数字再设空格
  • setState: 通知Flutter更新UI
  • 胜利条件: [1,2,3,...,15,0]
  • 步数统计: 每次有效移动加1
  • GestureDetector: 检测点击事件
  • 箭头函数 : () => _tap(i)延迟执行

滑动交换是数字拼图的核心操作,理解了相邻判断的逻辑,游戏就完成了。


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

相关推荐
偷星星的贼112 小时前
C++中的访问者模式实战
开发语言·c++·算法
a3158238062 小时前
Android编码规范(修订版)
android·代码规范
灵感菇_2 小时前
Android OkHttp框架全解析
android·java·okhttp·网络编程
w***76552 小时前
快速上手DCAT-Admin开发指南
android
莫问前路漫漫2 小时前
Java Runtime Environment(JRE)全解析:Java 程序跨平台运行的核心基石
java·开发语言
进阶小白猿2 小时前
Java技术八股学习Day22
java·开发语言·学习
技术摆渡人2 小时前
专题二:【驱动进阶】打破 Linux 驱动开发的黑盒:从 GPIO 模拟到 DMA 陷阱全书
android·linux·驱动开发
蒟蒻的贤2 小时前
操作系统复习
java·开发语言·数据库
汤姆yu2 小时前
基于android的个人健康系统
android