通过Flutter实现在多端运行的扫雷游戏

简介: 当我们回忆起小时候的经典电脑游戏,扫雷一定是其中之一。这个简单而富有挑战的游戏不仅考验我们的智力和耐性,而且在完成后还会让我们感到一种无与伦比的成就感。现在,您可以使用Flutter来重新体验这个经典游戏,无论您是Flutter新手还是老手,都能通过本文,让您在Flutter的世界中开发出一个令人满意的扫雷游戏。

前言

当我们回忆起小时候的经典电脑游戏,扫雷一定是其中之一。这个简单而富有挑战的游戏不仅考验我们的智力和耐性,而且在完成后还会让我们感到一种无与伦比的成就感。现在,您可以使用Flutter来重新体验这个经典游戏,无论您是Flutter新手还是老手,都能通过本文,让您在Flutter的世界中开发出一个令人满意的扫雷游戏。

代码仓库:https://github.com/taxze6/flutter_game_collection/tree/main/mine_sweeping

注:本文demo未使用任何第三方插件,Flutter版本3.7.3

效果图

话不多说,先上效果图。(包含不同端、不同难度、不同游戏主题)

Windows端

网页端

Android端

iOS

macOS

Linux

暂无设备

暂无设备

暂无设备

开始实现

第一步:定义游戏设置

定义GameSetting单例类,确保扫雷程序中只有一个实例,并且该实例可以被全局访问,主要用于共享资源。

class GameSetting {

 GameSetting._();

}

然后定义一个私有的、静态的、不可变的 _default 对象,它是 GameSetting 类的默认实例,该实例在第一次使用时被创建。再定义一个 GameSetting 工厂构造函数,它通过返回 _default 对象实现了单例模式的实例化,该工厂构造函数是唯一可以实例化 GameSetting 对象的方法。

static final GameSetting _default = GameSetting._();

factory GameSetting() => _default;

完成了单例类的基本定义,现在再来定义与扫雷相关的,先定义游戏的难度。在扫雷中游戏的难度主要有两部分组成:

  • 棋盘格子的数量

///游戏的难度,默认为8*8

int difficulty = 8;

  • 雷的数量

///雷的数量 (格子总数 * 0.18 向下取整),通常扫雷的雷数在0.16-0.2之间。

int get mines => (difficulty * difficulty * 0.18).floor();

最后在定义一些游戏的颜色主题:

List<Color> c_5ADFD0 = [

 Color(0xFF299794),

 Color(0xFF2EC4C0),

 Color(0xFF2EC4C0)

];


List<Color> c_A0BBFF = [

 Color(0xFF5067C5),

 Color(0xFF838CFF),

 Color(0xFFA0BBFF),

];


///默认主题

Color themeColor = Color(0xFF5ADFD0);

第二步:定义游戏参数

在进行扫雷游戏的时候,需要记录棋盘格子上每个格子的参数,记录格子是否被标记为雷、是否被翻开。也需要记录游戏是否获胜、是否踩到了地雷。

late List<List<int>> board; // 棋盘

late List<List<bool>> revealed; // 记录格子是否被翻开

late List<List<bool>> flagged; // 记录格子是否被标记

late bool gameOver; // 游戏是否结束

late bool win; // 是否获胜

其他初始化参数:

late int numRows; // 行数

late int numCols; // 列数

late int numMines; // 雷数


//游戏时间

late int _playTime;

第三步:编写扫雷初始化游戏逻辑

定义了游戏的参数后,在游戏开始时,需要对其进行赋值。

numRows = gameSetting.difficulty;

numCols = gameSetting.difficulty;

numMines = gameSetting.mines;

// 初始化棋盘

board = List.generate(numRows, (_) => List.filled(numCols, 0));

// 初始化格子是否被翻开

revealed = List.generate(numRows, (_) => List.filled(numCols, false));

// 初始化格子是否被标记

flagged = List.generate(numRows, (_) => List.filled(numCols, false));

// 将游戏定义为未结束

gameOver = false;

// 将游戏定义为还未获胜

win = false;

通过while循环在棋盘上随机放置地雷,直到放置的地雷数量达到预定的 numMines

int numMinesPlaced = 0;

while (numMinesPlaced < numMines) {

...

}

使用 Random().nextInt 方法生成两个随机数 i 和 j,分别用于表示棋盘中的行和列。

int i = Random().nextInt(numRows);

int j = Random().nextInt(numCols);

通过 board[i][j] != -1 的判断语句,检查这个位置是否已经放置了地雷。如果没有则将 board[i][j] 的值设置为 -1,表示在这个位置放置了地雷,并将numMinesPlaced 的值加 1。

if (board[i][j] != -1) {

 board[i][j] = -1;

 numMinesPlaced++;

}

放完了地雷,那么就到了扫雷的核心逻辑,计算每个非地雷格子周围的地雷数量,然后将计算得到的地雷数量保存在对应的格子上。具体实现的逻辑为:通过两个嵌套的 for 循环遍历整个棋盘,内层的两个嵌套循环会计算这个格子周围的所有格子中地雷的数量,并将这个数量保存在 count 变量中。

for (int i = 0; i < numRows; i++) {

 for (int j = 0; j < numCols; j++) {

   ...

 }

}

循环中具体的逻辑是:在每个单元格上,如果它不是地雷(值不为为-1)则内部嵌套两个循环遍历当前单元格周围的所有单元格,计算地雷数量并存储在当前单元格中。

if (board[i][j] != -1) {

 int count = 0;

 for (int i2 = max(0, i - 1); i2 <= min(numRows - 1, i + 1); i2++) {

   for (int j2 = max(0, j - 1);

       j2 <= min(numCols - 1, j + 1);

       j2++) {

     if (board[i2][j2] == -1) {

       count++;

     }

   }

 }

 board[i][j] = count;

}

第四步:编写用户交互游戏逻辑

只要用户点击了,就要将格子设置为翻开了。

void reveal(int i, int j) {

revealed[i][j] = true;

}

当用户点击了一个格子后,我们需要判断以下几点:

  • 如果翻开的是地雷

if (board[i][j] == -1) {

 //将所有的地雷翻开,告诉用户所有的地雷位置

 for (int i2 = 0; i2 < numRows; i2++) {

   for (int j2 = 0; j2 < numCols; j2++) {

     if (board[i2][j2] == -1) {

       revealed[i2][j2] = true;

     }

   }

 }

 //游戏结束

 gameOver = true;

//结束动画

...

}

  • 如果点击的格子周围都没有雷就自动翻开相邻的空格

if (board[i][j] == 0) {

 for (int i2 = max(0, i - 1); i2 <= min(numRows - 1, i + 1); i2++) {

   for (int j2 = max(0, j - 1); j2 <= min(numCols - 1, j + 1); j2++) {

     if (!revealed[i2][j2]) {

       reveal(i2, j2);

     }

   }

 }

}

  • 检查是否胜利

///它会遍历整个棋盘,检查每一个未被翻开的格子是否都是地雷,

bool checkWin() {

 for (int i = 0; i < numRows; i++) {

   for (int j = 0; j < numCols; j++) {

     if (board[i][j] != -1 && !revealed[i][j]) {

       return false;

     }

   }

 }

 return true;

}



if (checkWin()) {

 win = true;

 gameOver = true;

 _timer?.cancel();

 //获胜动画

 ...

}

第五步:封装格子

定义枚举类BlockType,用于判断不同的状态下显示不同的格子样式。

enum BlockType {

 //数字

 figure,

 //雷

 mine,

 //标记

 label,

 //未标记(未被翻开)

 unlabeled,

}

封装格子的代码其实很简单,根据不同的状态封装即可,这里就不过多展示了。

第六步:游戏布局

此处只分析游戏棋盘的布局。

通过GridView.builder构建棋盘,使用SliverGridDelegateWithFixedCrossAxisCount实现每一行具有相同数量的列。

GridView.builder(

 gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(

   crossAxisCount: numCols,

   childAspectRatio: 1.0,

 ),

 itemBuilder: (BuildContext context, int index) {

  ...

 }

)

通过对index的整除和取模,得到行和列,然后根据每个格子的当前状态对封装好的格子布局传入不同的参数。

itemBuilder: (BuildContext context, int index) {

 int i = index ~/ numCols;

 int j = index % numCols;

 BlockType blockType;

//格子被翻开

if (revealed[i][j]) {

  //是地雷

  if (board[i][j] == -1) {

    blockType = BlockType.mine;

  } else {

    blockType = BlockType.figure;

  }

} else {

  //被用户标记

  if (flagged[i][j]) {

    blockType = BlockType.label;

  } else {

    blockType = BlockType.unlabeled;

  }

}

 return GestureDetector(

   onTap: () => reveal(i, j),

   onDoubleTap: () => toggleFlag(i, j),

   child: BlockContainer(

     backColor: gameSetting.themeColor,

     value: revealed[i][j] && board[i][j] != 0 ? board[i][j] : 0,

     blockType: blockType,

   ),

 );

},

其中,如果双击格子代表标记或取消标记,定义了一个方法toggleFlag

///标记雷

void toggleFlag(int i, int j) {

 if (!gameOver) {

   setState(() {

     flagged[i][j] = !flagged[i][j];

   });

 }

}

到这里,就完成了对扫雷这款游戏的实现。更改游戏的主题状态或游戏难度,只需更改不同的初始化参数即可。

优化-第七步:游戏时间

有一个计时器,会大大提高用户玩算法类、解谜类这样游戏的乐趣,例如魔方。在Flutter中通过Timer.periodic去实现计时器是很简答的,就不过多讲述了,主要看下如何格式化为时钟的形式:

String get playTime {

 int minutes = (_playTime ~/ 60); // 计算分钟数

 int seconds = (_playTime % 60); // 计算秒数

 //padLeft方法用于补齐不足两位的数字,第一个参数是补齐后的字符串总长度,第二个参数是用于补齐的字符。

 return '${minutes.toString().padLeft(2, '0')}:${seconds.toString().padLeft(2, '0')}';

}

关于我

Hello,我是Taxze,如果您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需要联系我的话:我在这里 ,也可以通过掘金的新的私信功能联系到我。如果您觉得文章还差了那么点东西,也请通过关注督促我写出更好的文章~万一哪天我进步了呢?😝

相关文章
|
缓存 Java 开发工具
Flutter— 第一次运行Flutter工程时的Bug总结
Flutter— 第一次运行Flutter工程时的Bug总结
 Flutter— 第一次运行Flutter工程时的Bug总结
Doodle Jump — 使用Flutter&Flame开发游戏真不错!
用Flutter&Flame开发游戏是一种什么体验?最近网上冲浪的时候,我偶然发现了一个国外的游戏网站,类似于国内的4399。在浏览时,我遇到了一款经典的小游戏:Doodle Jump...
112862 12
|
3月前
|
开发工具 iOS开发
解决Flutter运行报错Could not run build/ios/iphoneos/Runner.app
解决Flutter运行报错Could not run build/ios/iphoneos/Runner.app
133 2
|
3月前
|
iOS开发
解决Flutter运行IOS报错:Podfile is out of date
解决Flutter运行IOS报错:Podfile is out of date
69 1
|
3月前
|
Dart 开发工具 Android开发
Flutter学习:从搭建环境到运行
Flutter学习:从搭建环境到运行
42 0
|
4月前
|
Dart Android开发 iOS开发
flutter 创建项目、运行项目、项目目录
flutter 创建项目、运行项目、项目目录
157 0
|
5月前
|
Dart Serverless Android开发
Flutter 单线程模型保证UI运行流畅
Flutter 单线程模型保证UI运行流畅
70 0
|
6月前
|
机器学习/深度学习 Java Android开发
记录一个Flutter运行的异常FAILURE: Build failed with an exception. What went wrong: A problem occurred config
记录一个Flutter运行的异常FAILURE: Build failed with an exception. What went wrong: A problem occurred config
196 0
|
Dart Android开发
Flutter | vscode运行Flutter疑难杂症
本来今天更新Stable Diffusion最后一个教程的,但是今天在开发中遇到了一个问题。
162 0
Flutter | vscode运行Flutter疑难杂症
|
存储 Dart 前端开发
原来Flutter代码是这样运行在原生系统的!快来了解Flutter标准模板,感受原生系统中Flutter的魅力!
原来Flutter代码是这样运行在原生系统的!快来了解Flutter标准模板,感受原生系统中Flutter的魅力!
84 0