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

相关推荐
草履虫建模1 天前
力扣算法 1768. 交替合并字符串
java·开发语言·算法·leetcode·职场和发展·idea·基础
讯方洋哥1 天前
HarmonyOS App开发——职前通应用App开发(下)
华为·harmonyos
Rainman博1 天前
WMS-窗口relayout&FinishDrawing
android
naruto_lnq1 天前
分布式系统安全通信
开发语言·c++·算法
学嵌入式的小杨同学1 天前
【Linux 封神之路】信号编程全解析:从信号基础到 MP3 播放器实战(含核心 API 与避坑指南)
java·linux·c语言·开发语言·vscode·vim·ux
Re.不晚1 天前
Java入门17——异常
java·开发语言
精彩极了吧1 天前
C语言基本语法-自定义类型:结构体&联合体&枚举
c语言·开发语言·枚举·结构体·内存对齐·位段·联合
摘星编程1 天前
React Native鸿蒙版:Image图片占位符
react native·react.js·harmonyos
南极星10051 天前
蓝桥杯JAVA--启蒙之路(十)class版本 模块
java·开发语言
baidu_247438611 天前
Android ViewModel定时任务
android·开发语言·javascript