前语

当咱们回忆起小时候的经典电脑游戏,扫雷一定是其间之一。这个简单而富有应战的游戏不只检测咱们的智力和耐性,而且在完成后还会让咱们感到一种无与伦比的成就感。现在,您能够运用Flutter来重新体会这个经典游戏,不管您是Flutter新手仍是老手,都能经过本文,让您在Flutter的世界中开发出一个令人满意的扫雷游戏。

代码仓库:github.com/taxze6/flut…

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

效果图

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

Windows端 网页端 Android端
通过Flutter实现一个能在多端运行的扫雷游戏
通过Flutter实现一个能在多端运行的扫雷游戏
通过Flutter实现一个能在多端运行的扫雷游戏
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,假如您觉得文章对您有价值,希望您能给我的文章点个❤️,有问题需求联络我的话:我在这儿,也能够经过的新的私信功用联络到我。假如您觉得文章还差了那么点东西,也请经过重视督促我写出更好的文章~万一哪天我进步了呢?

本文正在参与「金石计划」