Flutter随机迷宫生成和解迷宫小游戏功能的源码

时间:2022-07-27
本文章向大家介绍Flutter随机迷宫生成和解迷宫小游戏功能的源码,主要内容包括其使用实例、应用技巧、基本知识点总结和需要注意事项,具有一定的参考价值,需要的朋友可以参考一下。

此博客旨在帮助大家更好的了解图的遍历算法,通过Flutter移动端平台将图的遍历算法运用在迷宫生成和解迷宫上,让算法变成可视化且可以进行交互,最终做成一个可进行随机迷宫生成和解迷宫的APP小游戏。本人是应届毕业生,希望能与大家一起讨论和学习~

注:由于这是本人第一次写博客,难免排版或用词上有所欠缺,请大家多多包涵。 注:如需转载文章,请注明出处,谢谢。

一、项目介绍:

1.概述 项目名:方块迷宫 作者:沫小亮。 编程框架与语言:Flutter&Dart 开发环境:Android Studio 3.6.2 学习参考:慕课网-看得见的算法 项目完整源码地址:(待更新) 游戏截图:

2.迷宫生成原理 1.采用图的遍历进行迷宫生成,其本质就是生成一棵树,树中每个节点只能访问一次,且每个节点之间没有环路(迷宫的正确路径只有一条)。 2.初始化:设置起点和终点位置,并给所有行坐标为奇数且列坐标为奇数的位置设置为路。其余位置设置为墙。(坐标从0…开始算)

(如下图,蓝色位置为墙,橙色位置为路,橙色线条为可能即将打通的路,此图来源于慕课网-看得见的算法)

3.在遍历过程中,不断遍历每个位置,同时遍历过的位置设为已访问位置,结合迷宫生成算法(见迷宫特点第6点)让相邻某个墙变成路,使之路径联通。直至所有位置都遍历完成则迷宫生成结束(每个节点只能遍历一次)。

(如下图,蓝色位置为墙,橙色位置为路,橙色线条为可能即将打通的路,此图来源于慕课网-看得见的算法)

3.迷宫特点(可根据需求自行扩展) 1.迷宫只有一个起点、一个终点,且起点和终点的位置固定。 2.迷宫的正确路径只有一条。 3.迷宫的正确路径是连续的。 4.迷宫地图是正方形,且方块行数和列数都为奇数。 5.迷宫中每个方块占用一个单元格。 6.迷宫生成算法:图的深度优先遍历和广度优先遍历相结合 + 随机队列(入队和出队随机在队头或队尾)+ 随机方向遍历顺序(提高迷宫的随机性)。 7.迷宫自动求解算法:图的深度优先遍历(递归方法)。

4.玩法介绍(可根据需求自行扩展) 1.游戏共设置有10个关卡,到达终点可以进入下一关,随着关卡数的增加,迷宫地图大小(方块数)增加,但限定时间也会增加。 2.点击方向键可对玩家角色的位置进行控制。 2.每个关卡都有限定时间,超过限定时间仍未到达终点则闯关失败,可从本关继续挑战。 3.每个关卡都可以使用一次提示功能,可展示2秒的正确路径,便于小白玩家入门。 4. 颜色对应: 蓝灰色方块- 墙(不可经过) 蓝色方块- 玩家角色(可控制移动) 白色方块- 路(可经过) 深橘色- 终点(通关) 橙色- 正确路径(提示功能)

二、项目源码(主要部分):

pubspec.yaml //flutter配置清单

dependencies:
 flutter:
 sdk: flutter
 //toast库
 fluttertoast: ^3.1.3
 //Cupertino主题图标集
 cupertino_icons: ^0.1.2

maze_game_model.dart //迷宫游戏数据层

class MazeGameModel {
int _rowSum; //迷宫行数
int _columnSum; //迷宫列数
int _startX, _startY; //迷宫入口坐标([startX,startY])
int _endX, _endY; //迷宫出口坐标([endX,endY])
static final int MAP_ROAD = 1; //1代表路
static final int MAP_WALL = 0; //0代表墙
List<List<int   mazeMap; //迷宫地形(1代表路,0代表墙)
List<List<bool   visited; //是否已经访问过
List<List<bool   path; //是否是正确解的路径
List<List<int   direction = [
[-1, 0],
[0, 1],
[1, 0],
[0, -1]
]; //迷宫遍历的方向顺序(迷宫趋势)
int spendStepSum = 0; //求解的总步数
int successStepLength = 0; //正确路径长度
int playerX, playerY; //当前玩家坐标
MazeGameModel(int rowSum, int columnSum) {
if (rowSum % 2 == 0 || columnSum % 2 == 0) {
throw "model_this- 迷宫行数和列数不能为偶数";
}
this._rowSum = rowSum;
this._columnSum = columnSum;
mazeMap = new List<List<int  ();
visited = new List<List<bool  ();
path = new List<List<bool  ();
//初始化迷宫起点与终点坐标
_startX = 1;
_startY = 0;
_endX = rowSum - 2;
_endY = columnSum - 1;
//初始化玩家坐标
playerX = _startX;
playerY = _startY;
//初始化迷宫遍历的方向(上、左、右、下)顺序(迷宫趋势)
//随机遍历顺序,提高迷宫生成的随机性(共12种可能性)
for (int i = 0; i < direction.length; i++) {
int random = Random().nextInt(direction.length);
List<int  temp = direction[random];
direction[random] = direction[i];
direction[i] = temp;
}
//初始化迷宫地图
for (int i = 0; i < rowSum; i++) {
List<int  mazeMapList = new List();
List<bool  visitedList = new List();
List<bool  pathList = new List();
for (int j = 0; j < columnSum; j++) {
//行和列都为基数则设置为路,否则设置为墙
if (i % 2 == 1 && j % 2 == 1) {
mazeMapList.add(1); //设置为路
} else {
mazeMapList.add(0); //设置为墙
}
visitedList.add(false);
pathList.add(false);
}
mazeMap.add(mazeMapList);
visited.add(visitedList);
path.add(pathList);
}
//初始化迷宫起点与终点位置
mazeMap[_startX][_startY] = 1;
mazeMap[_endX][_endY] = 1;
}
//返回迷宫行数
int getRowSum() {
return _rowSum;
}
//返回迷宫列数
int getColumnSum() {
return _columnSum;
}
//返回迷宫入口X坐标
int getStartX() {
return _startX;
}
//返回迷宫入口Y坐标
int getStartY() {
return _startY;
}
//返回迷宫出口X坐标
int getEndX() {
return _endX;
}
//返回迷宫出口Y坐标
int getEndY() {
return _endY;
}
//判断[i][j]是否在迷宫地图内
bool isInArea(int i, int j) {
return i  = 0 && i < _rowSum && j  = 0 && j < _columnSum;
}
}

position.dart //位置类(实体类) 注:x对应二维数组中的行下标,y对应二维数组中的列下标(往后也是)

class Position extends LinkedListEntry<Position {
int _x, _y; //X对应二维数组中的行下标,y对应二维数组中的列下标
Position _prePosition; //存储上一个位置
Position(int x, int y, { Position prePosition = null } ) {
this._x = x;
this._y = y;
this._prePosition = prePosition;
}
//返回X坐标()
int getX() {
return _x;
}
//返回Y坐标()
int getY() {
return _y;
}
//返回上一个位置
Position getPrePosition() {
return _prePosition;
}
}

random_queue.dart //随机队列 入队:头部或尾部(各50%的概率) 出队:头部或尾部(各50%的概率) 底层数据结构:LinkedList

class RandomQueue {
LinkedList<Position  _queue;
RandomQueue(){
_queue = new LinkedList();
}
//往随机队列里添加一个元素
void addRandom(Position position) {
if (Random().nextInt(100) < 50) {
//从头部添加
_queue.addFirst(position);
}
//从尾部添加 
else {
_queue.add(position);
}
}
//返回随机队列中的一个元素
Position removeRandom() {
if (_queue.length == 0) {
throw "数组元素为空";
}
if (Random().nextInt(100) < 50) {
//从头部移除
Position position = _queue.first;
_queue.remove(position);
return position;
} else {
//从尾部移除
Position position = _queue.last;
_queue.remove(position);
return position;
}
}
//返回随机队列元素数量
int getSize() {
return _queue.length;
}
//判断随机队列是否为空
bool isEmpty() {
return _queue.length == 0;
}
}

main.dart //迷宫游戏视图层和控制层

1. APP全局设置

void main() =  runApp(MyApp());
class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
if (Platform.isAndroid) {
// 以下两行 设置android状态栏为透明的沉浸。写在组件渲染之后,是为了在渲染后进行set赋值,覆盖状态栏,写在渲染之前MaterialApp组件会覆盖掉这个值。
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(statusBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
}
return MaterialApp(
title: '方块迷宫', //应用名
theme: ThemeData(
primarySwatch: Colors.blue, //主题色
),
debugShowCheckedModeBanner: false, //不显示debug标志
home: MyHomePage(), //主页面
);
}
}

2.界面初始化

class MyHomePage extends StatefulWidget {
MyHomePage({Key key}) : super(key: key);
@override
_MyHomePageState createState() =  _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage  {
int gameWidth, gameHeight; //游戏地图宽度和高度
double itemWidth, itemHeight; //每个小方块的宽度和高度
int level = 1;  //当前关卡数(共10关)
int rowSum = 15; //游戏地图行数
int columnSum = 15; //游戏地图列数
int surplusTime; //游戏剩余时间
bool isTip = false; //是否使用提示功能
Timer timer;  //计时器
MazeGameModel _model; //迷宫游戏数据层
//初始化状态
@override
void initState() {
super.initState();
_model = new MazeGameModel(rowSum, columnSum);
//新建一个事件循环队列,确保不堵塞主线程
new Future(() {
//生成一个迷宫
_doGenerator(_model.getStartX(), _model.getStartY() + 1);
});
//设置倒计时
_setSurplusTime(level);
}

3.界面整体结构

@override
Widget build(BuildContext context) {
//获取手机屏幕宽度,并让屏幕高度等于屏幕宽度(确保形成正方形迷宫区域)
//结果向下取整,避免出现实际地图宽度大于手机屏幕宽度的情况
gameHeight = gameWidth = MediaQuery.of(context).size.width.floor();
//每一个小方块的宽度和长度(屏幕宽度/列数)
itemHeight = itemWidth = (gameWidth / columnSum);
return Scaffold(
appBar: PreferredSize(
//设置标题栏高度
preferredSize: Size.fromHeight(40),
//标题栏区域
child: _appBarWidget()),
body: ListView(
children: <Widget [
//游戏地图区域
_gameMapWidget(),
//游戏提示与操作栏区域
_gameTipWidget(),
//游戏方向控制区域
_gameControlWidget(),
],
),
);
}

4.游戏地图区域

注:由于游戏提示与操作栏区域、游戏方向键控制区域不是本文章要讲的重点,故不详细介绍,有兴趣的朋友可以到完整项目源码地址中查看。

//游戏地图区域
Widget _gameMapWidget(){
return Container(
width: gameHeight.toDouble(),
height: gameHeight.toDouble(),
color: Colors.white,
child: Center(
//可堆叠布局(配合Positioned绝对布局使用)
child: Stack(
//按行遍历
children: List.generate(_model.mazeMap.length, (i) {
return Stack(
//按列遍历
children: List.generate(_model.mazeMap[i].length, (j) {
//绝对布局
return Positioned(
//每个方块的位置
left: j * itemWidth.toDouble(),
top: i * itemHeight.toDouble(),
//每个方块的大小和颜色
child: Container(
width: itemWidth.toDouble(),
height: itemHeight.toDouble(),
//位于顶层的颜色应放在前面进行判断,避免被其他颜色覆盖
//墙- 蓝灰色
//路- 白色
//玩家角色- 蓝色
//迷宫终点-  深橘色
//迷宫正确路径- 橙色
color: _model.mazeMap[i][j] == 0
? Colors.blueGrey
: (_model.playerX == i && _model.playerY == j)
? Colors.blue
: (_model.getEndX() == i && _model.getEndY() == j)
? Colors.deepOrange
: _model.path[i][j] ? Colors.orange : Colors.white));
}));
}),
),
));
}

5.生成迷宫

//开始生成迷宫地图
void _doGenerator(int x, int y) {
RandomQueue queue = new RandomQueue();
//设置起点
Position start = new Position(x, y);
//入队
queue.addRandom(start);
_model.visited[start.getX()][start.getY()] = true;
while (queue.getSize() != 0) {
//出队
Position curPosition = queue.removeRandom();
//对上、下、左、右四个方向进行遍历,并获得一个新位置
for (int i = 0; i < 4; i++) {
int newX = curPosition.getX() + _model.direction[i][0] * 2;
int newY = curPosition.getY() + _model.direction[i][1] * 2;
//如果新位置在地图范围内且该位置没有被访问过
if (_model.isInArea(newX, newY) && !_model.visited[newX][newY]) {
//入队
queue.addRandom(new Position(newX, newY, prePosition: curPosition));
//设置该位置为已访问
_model.visited[newX][newY] = true;
//设置该位置为路
_setModelWithRoad(curPosition.getX() + _model.direction[i][0], curPosition.getY() + _model.direction[i][1]);
}
}
}
}

6.自动解迷宫(提示功能)

//自动解迷宫(提示功能)
//从起点位置开始(使用递归的方式)求解迷宫,如果求解成功则返回true,否则返回false
bool _doSolver(int x, int y) {
if (!_model.isInArea(x, y)) {
throw "坐标越界";
}
//设置已访问
_model.visited[x][y] = true;
//设置该位置为正确路径
_setModelWithPath(x, y, true);
//如果该位置为终点位置,则返回true
if (x == _model.getEndX() && y == _model.getEndY()) {
return true;
}
//对四个方向进行遍历,并获得一个新位置
for (int i = 0; i < 4; i++) {
int newX = x + _model.direction[i][0];
int newY = y + _model.direction[i][1];
//如果该位置在地图范围内,且该位置为路,且该位置没有被访问过,则继续从该点开始递归求解
if (_model.isInArea(newX, newY) &&
_model.mazeMap[newX][newY] == MazeGameModel.MAP_ROAD &&
!_model.visited[newX][newY]) {
if (_doSolver(newX, newY)) {
return true;
}
}
}
//如果该位置不是正确的路径,则将该位置设置为非正确路径所途径的位置
_setModelWithPath(x, y, false);
return false;
}

7.控制玩家角色移动

移动到新位置

//控制玩家角色移动
void _doPlayerMove(String direction) {
switch (direction) {
case "上":
//如果待移动的目标位置在迷宫地图内,且该位置是路,则进行移动
if (_model.isInArea(_model.playerX - 1, _model.playerY) && _model.mazeMap[_model.playerX - 1][_model.playerY] == 1) {
setState(() {
_model.playerX--;
});
}
break;
//省略其他三个方向的代码

玩家到达终点位置

//如果玩家角色到达终点位置
if (_model.playerX == _model.getEndX() && _model.playerY == _model.getEndY()) {
isTip = false; //刷新可提示次数
timer.cancel(); //取消倒计时
//如果当前关是第10关
if (level == 10) {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text("骚年,你已成功挑战10关,我看你骨骼惊奇,适合玩迷宫(狗头"),
actions: <Widget [
new FlatButton(
child: new Text('继续挑战第10关(新地图)', style: TextStyle(fontSize: 16)),
onPressed: () {
setState(() {
_model.playerX = _model.getStartX();
_model.playerY = _model.getStartY();
});
//重新初始化数据
_model = new MazeGameModel(rowSum, columnSum);
//生成迷宫和设置倒计时
_doGenerator(_model.getStartX(), _model.getStartY() + 1);
_setSurplusTime(level);
Navigator.of(context).pop();
},
)
],
);
});
}
//如果当前关不是第10关
else {
showDialog(
barrierDismissible: false,
context: context,
builder: (BuildContext context) {
return AlertDialog(
content: Text("恭喜闯关成功"),
actions: <Widget [
new FlatButton(
child: new Text('挑战下一关', style: TextStyle(fontSize: 16)),
onPressed: () {
setState(() {
//关卡数+1,玩家角色回到起点
level++;
_model.playerX = _model.getStartX();
_model.playerY = _model.getStartY();
});
//重新初始化数据
_model = new MazeGameModel(rowSum = rowSum + 4, columnSum = columnSum + 4);
//生成迷宫和设置倒计时
_doGenerator(_model.getStartX(), _model.getStartY() + 1);
_setSurplusTime(level);
Navigator.of(context).pop();
},
)
],
);
});
}
}

注:其他与控制逻辑相关的方法不在此文中详细介绍,有兴趣的朋友可以到完整项目源码地址中浏览。

总结

到此这篇关于Flutter随机迷宫生成和解迷宫小游戏功能的源码的文章就介绍到这了,更多相关Flutter迷宫小游戏内容请搜索ZaLou.Cn以前的文章或继续浏览下面的相关文章希望大家以后多多支持ZaLou.Cn!