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();
                    },
                  )
                ],
              );
            });
      }
    }

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

原文地址:https://blog.csdn.net/moxiaoliang123456/article/details/105538221

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


简介 java中使用jar包来封装有用的功能,然后将其分发到maven仓库中,供其他人使用。同样的在dart中也有类似的概念叫做packages。packages就是可以用来共享的软件包,可以包含libraries和tools。 你可以在pub.dev网站中查到dart中所有的共享packages的
简介 flutter是google在2015年dart开发者峰会上推出的一种开源的移动UI构建框架,使用flutter可以非常方便的编译成运行在原始android,ios,web等移动平台上的移动应用。 flutter是使用dart来编写的,最新的flutter版本是2.5.3,而最新的Dart语言
简介 dart作为一种面向对象的语言,class是必不可少的。dart中所有的class,除了Null都继承自Object class。 要想使用dart中的类就要构造类的实例,在dart中,一个类的构造函数有两种方式,一起来看看吧。 传统的构造函数 和JAVA一样,dart中可以使用和class名
简介 Exception是程序中的异常情况,在JAVA中exception有checked Exception和unchecked Exception。那么在dart中的情况是不是一样的呢?一起来看看吧。 Exception和Error Dart中表示异常的类有两个,分别是Exception和Err
简介 虽然dart中的类只能有一个父类,也就是单继承的,但是dart提供了mixin语法来绕过这样限制。 今天,和大家一起来探讨一下dart类中的继承。 使用extends 和JAVA一样,dart中可以定义一个父类,然后使用extends来继承他,得到一个子类,如下所示: class Studen
简介 pubspec.yaml是所有dart项目的灵魂,它包含了所有dart项目的依赖信息和其他元信息,所以pubspec.yaml就是dart项目的meta! pubspec.yaml支持的字段 根据dart的定义,pubspec.yaml中可以包含下面的字段: 字段名 是否必须字段 描述 nam
dart系列之:dart语言中的特殊操作符 简介 有运算就有操作符,dart中除了普通的算术运算的操作符之外,还有自定义的非常特殊的操作符,今天带大家一起来探索一下dart中的特殊操作符。 普通操作符 普通操作符就很好解释了,就是加减乘除,逻辑运算符,比较运算符和位运算符等。 这些操作符和其他语言的
简介 在dart系统中,有pubspec.yaml文件的应用就可以被成为一个package。而Libray package是一类特殊的package,这种包可以被其他的项目所依赖. 也就是通常所说的库。 如果你也想你写的dart程序可以上传到pub.dev上,或者提供给别人使用,则来看看这篇文章吧。
简介 和所有的编程语言一样,dart有他内置的语言类型,这些内置类型都继承自Object,当然这些内置类型是dart语言的基础,只有掌握了这些内置类型才能够在使用dart语言的时候得心应手。 今天就给大家讲解一下dart语言的内置类型。 Null 在dart中用null来表示空。那么null和Nul
简介 函数是所有编程语言都有的内容,不管是面向对象还是面向过程,函数都是非常重要的一部分。dart中的函数和java中的函数有什么区别呢? dart作为一种面向对象的编程语言,它的函数也是一个对象,用Function来表示。先看下函数的定义: abstract class Function { ex
简介 熟悉JAVA的朋友可能知道,JAVA在8中引入了泛型的概念。什么是泛型呢?泛型就是一种通用的类型格式,一般用在集合中,用来指定该集合中应该存储的对象格式。 有了泛型可以简化我们的编程,并且可以减少错误的产生,非常的方便。 dart语言中也有泛型。一起来看看吧。 为什么要用泛型 使用泛型的主要目
简介 熟悉javascript的朋友应该知道,在ES6中引入了await和async的语法,可以方便的进行异步编程,从而摆脱了回调地狱。dart作为一种新生的语言,没有理由不继承这种优秀的品质。很自然的,dart中也有await和async语言,一起来看看吧。 为什么要用异步编程 那么为什么要用异步
简介 要想熟悉一种语言,最简单的做法就是熟悉dart提供的各种核心库。dart为我们提供了包括dart:core,dart:async,dart:math,dart:convert,dart:html和dart:io这几种常用的库。 今天给大家介绍一下dart:core中的数字和字符串的使用。 数字
简介 ES6中在引入异步编程的同时,也引入了Generators,通过yield关键词来生成对应的数据。同样的dart也有yield关键词和生成器的概念。 什么时候生成器呢?所谓生成器就是一个能够持续产生某些数据的装置,也叫做generator。 两种返回类型的generator 根据是同步生成还是
简介 Flutter的基础是widget,根据是否需要跟用户进行交互,widget则可以分为StatelessWidget和StatefulWidget。StatelessWidget只能根据传入的状态进行简单的初始化widget,如果要实现跟用户交互这种复杂的功能,则需要用到StatefulWid
简介 时间和日期是我们经常会在程序中使用到的对象。但是对时间和日期的处理因为有不同时区的原因,所以一直以来都不是很好用。就像在java中,为时间和日期修改和新增了多次API,那么作为新生的语言dart而言,会有什么不一样的地方吗? dart中关于日期和时间的两个非常重要的类是DateTime和Dur
简介 Library是dart用来组织代码的一种非常有用的方式,通过定义不同的Library,可以将非常有用的dart代码进行封装,从而提供给其他的项目使用。虽然我们可以自由使用import或者export来对library进行导入和导入。但是什么样的用法才是最合适的用法呢? 一起来看看吧。 使用p
简介 dart中的集合有三个,分别是list,set和map。dart在dart:core包中提供了对于这三种集合非常有用的方法,一起来看看吧。 List的使用 首先是list的创建,可以创建空的list或者带值的list: var emptyList =[]; var nameList = [&#
简介 dart:html包为dart提供了构建浏览器客户端的一些必须的组件,之前我们提到了HTML和DOM的操作,除了这些之外,我们在浏览器端另一个常用的操作就是使用XMLHttpRequest去做异步HTTP资源的请求,也就是AJAX请求。 dart同样提供了类似JS中XMLHttpRequest
简介 Flutter是google开发的一个跨平台的UI构建工具,flutter目前最新的版本是3.0.5。使用flutter你可以使用一套代码搭建android,IOS,web和desktop等不同平台的应用。做到一次编写到处运行的目的。 说到一次编写处处运行,大家可能会想到java。那么flut