此博客旨在帮助大家更好的了解图的遍历算法,通过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> mazeMap; //迷宫地形(1代表路,0代表墙) List
> visited; //是否已经访问过 List
> path; //是否是正确解的路径 List
> 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
>(); visited = new List
>(); path = new List
>(); //初始化迷宫起点与终点坐标 _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
temp = direction[random]; direction[random] = direction[i]; direction[i] = temp; } //初始化迷宫地图 for (int i = 0; i < rowSum; i++) { List mazeMapList = new List(); List visitedList = new List(); List 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{ 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_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{ 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:[ //游戏地图区域 _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:[ 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: [ 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迷宫小游戏内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!