基于JavaFX图形界面演示的迷宫创建与路径寻找
事情的起因是收到了一位网友的请求,他的java课设需要设计实现迷宫相关的程序——如标题概括。
我这边不方便透露相关信息,就只把任务要求写出来。
演示视频指路👉:
完整代码链接🔎:
- 网盘:https://pan.baidu.com/s/12CFCecCb6iLu8kgBWhaBwg?pwd=abcd 提取码:abcd
- Github:xiao-qi-w/Maze: 基于JavaFX图形界面演示的迷宫创建与路径寻找 (github.com)
开发工具:IDEA 2020.3.1,SceneBuilder
基础要求
(1)概述:用 java 设计和实现一电脑鼠走迷宫的软件程序。本综合实践分算法设计和实现和界面展现两部分。
(2)第一部分:算法设计和实现部分
迷宫地图生成算法的设计和实现
自动生成迷宫:根据迷宫生成算法自动生成一定复杂度的迷宫地图。
手动生成迷宫:根据文件中存储的固定数据生成迷宫地图。
单路径寻找算法的设计与实现:找出迷宫中一条单一的通路。
迷宫遍历算法的设计与实现:遍历迷宫中所有的可行路径。
最短路径计算算法的设计与实现:根据遍历结果,找出迷宫中所有通路中的最短通路。
(3)第二部分:界面展示部分
生成迷宫地图界面的设计与实现:根据生成的迷宫地图,用可视化的界面展现出来。
界面布局的设计与实现:根据迷宫程序的总体需求,设计和实现合理的界面布局。
相关迷宫生成过程和寻路算法在界面上的展现:将迷宫程序中的相关功能,跟界面合理结合,并采用一定的方法展现给用户,如通过动画展示等。
(4)总体任务要求
具有判断通路和障碍的功能;
走不通具备返回的能力(路径记忆);
能够寻找最短路径;
程序不仅要实现相关算法,还需要具备基本的界面操作功能。
(5)任务分解
迷宫的生成:手动生成或自动生成
寻路:从任意给定点走到另外给定点
遍历:遍历整个迷宫
寻优:计算最短路径(计算等高表,按路径行规定走)
相关界面设计和编程
看到这里相信各位已经对本程序有了初步的认知,而且上述要求中也对整体任务进行了分解,那么我们只需要挨个实现即可。实际上我们只需要做两件事,编写算法和使用图形界面展示算法。
有关图形界面的基础知识,推荐观看 B站UP蔡广 的视频(我基本是按照这个视频的知识点设计的):JavaFX 桌面软件 PC 软件开发 基础入门_哔哩哔哩_bilibili
我们先来完成第一件事——算法的实现:
假定观看文章的各位对本文出现的算法和数据结构有一定了解,所以这部分内容我并不对算法本身,如深度优先搜索DFS、广度优先搜索BFS以及所用到的数据结构,譬如栈、队列和链表做过多阐述,想要了解其原理与正确性的话请以加粗字体为关键词自行搜索。
为了方便算法实现,定义全局变量dirs数组表示右下左上四个方向:int[][] dirs = new int[][] {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
1. 迷宫的创建
这里由于我之前做过C语言的迷宫程序,不重复造轮子,上链接:C语言实现一个走迷宫小游戏(深度优先算法)。
当然我并没有全部照搬,只是采用了深度优先的思想。因为这几种生成算法都只能产生一条可行路径。为了体现遍历和寻优,我直接在迷宫中生成了一条大小合适的环路,并控制生成迷宫的复杂程度,这样一般情况下迷宫会有多条可行路径,示意图如下:
主要代码:
构造迷宫
// 修饰迷宫地图
public void initMap() {
//最外围层设为路径的原因,为了防止挖路时挖出边界,同时为了保护迷宫主体外的一圈墙体被挖穿
for (int i = 0; i < L; i++) {
map[i][0] = 1;
map[0][i] = 1;
map[i][L - 1] = 1;
map[L - 1][i] = 1;
}
// 创造迷宫, (2, 2)为起点
CreateMaze(inX, inY + 1);
// 画迷宫的入口和出口
for (int i = L - 3; i >= 0; i--) {
if (map[i][L - 3] == 1) {
map[i][L - 2] = 1;
this.outX = i;
break;
}
}
map[inX][inY] = map[outX][outY] = 1;
// 制造环路
for (int i = 10; i < 31; i++) {
map[i][10] = 1;
map[10][i] = 1;
map[i][30] = 1;
map[30][i] = 1;
}
// 创建迷宫时会打乱方向顺序,这里还原方向数组
dirs = new int[][]{{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
}
// 构造迷宫地图
public void CreateMaze(int x, int y) {
map[x][y] = ROUTE;
int i, j;
// 随机打乱方向顺序
for (i = 0; i < 4; i++) {
int r = random.nextInt(4);
int temp = dirs[0][0];
dirs[0][0] = dirs[r][0];
dirs[r][0] = temp;
temp = dirs[0][1];
dirs[0][1] = dirs[r][1];
dirs[r][1] = temp;
}
//向四个方向开挖
for (i = 0; i < 4; i++) {
int dx = x;
int dy = y;
//控制挖的距离,由rank来调整大小
int range = 1 + random.nextInt(rank);
while (range > 0) {
//计算出将要访问到的坐标
dx += dirs[i][0];
dy += dirs[i][1];
//排除掉回头路
if (map[dx][dy] == ROUTE) {
break;
}
//判断是否挖穿路径
int count = 0, k;
for (j = dx - 1; j < dx + 2; j++) {
for (k = dy - 1; k < dy + 2; k++) {
//abs(j - dx) + abs(k - dy) == 1 确保只判断九宫格的四个特定位置
if (Math.abs(j - dx) + Math.abs(k - dy) == 1 && map[j][k] == ROUTE) {
count++;
}
}
}
//count大于1表明墙体会被挖穿,停止
if (count > 1)
break;
//确保不会挖穿时,前进
range -= 1;
map[dx][dy] = ROUTE;
}
//没有挖穿危险,以此为节点递归
if (range <= 0) {
CreateMaze(dx, dy);
}
}
}
2. 单路径寻找算法
为了和最短路径算法有所区分,这里采用深度优先搜索(DFS)算法。核心思想为从迷宫某一点出发,依次向四个方向进行访问,对已经访问过的点进行标记。越界、迷宫墙体和已经访问过的点不会被访问,如此往复递归,直到找到出口或者给定可行坐标结束递归,记录路径,代码实现如下:
单路径寻找算法
// DFS寻找可行路径
public void findWay(boolean[][] visit, int x, int y) {
for (int k = 0; k < 4; ++k) {
int nx = x + dirs[k][0];
int ny = y + dirs[k][1];
if (nx < 2 || nx > L - 3 || ny < 1 || ny > L - 2 || visit[nx][ny] || map[nx][ny] != ROUTE)
continue;
//来到新位置后, 进行标记
map[nx][ny] = RIGHT;
visit[nx][ny] = true;
if (nx == outX && ny == outY) {
//走到出口则结束搜索, 记录路径并返回
LinkedList<Route> stack = new LinkedList<>();
for (int i = 0; i < L; ++i) {
for (int j = 0; j < L; ++j) {
if (map[i][j] > 1)
stack.push(new Route(i, j));
}
}
stacks.add(stack);
return;
} else {
//否则进行下一层递归
findWay(visit, nx, ny);
}
// 不正确的路径需要还原
map[nx][ny] = ROUTE;
}
}
3. 遍历迷宫算法
观察上述寻找单路经的算法,对其加以改造。由于visit数组的影响,在到达目标点后,目标点被设置为已访问过,不可能再次到达。所以我们去掉visit数组的限制,回溯所有可能的情况,一旦到达目标点我们就记录下这条路径,这样遍历算法也就完成了。由于受迷宫地图大小和环路的影响,实际要找到迷宫的所有可行路径是很耗时的,所以这部分演示时可以采取手动输入地图的方式,使迷宫的可行路径尽可能的少一些。下面给出具体实现:
遍历迷宫算法
// DFS遍历全部可行路径
public void findAllWay(int x, int y) {
for (int k = 0; k < 4; ++k) {
int nx = x + dirs[k][0];
int ny = y + dirs[k][1];
if (nx < 2 || nx > L - 3 || ny < 1 || ny > L - 2 || map[nx][ny] != ROUTE)
continue;
//来到新位置后,设置当前值为可行路径
map[nx][ny] = RIGHT;
if (nx == outX && ny == outY) {
//走到出口则结束搜索,记录路径并返回
LinkedList<Route> stack = new LinkedList<>();
for (int i = 0; i < L; ++i) {
for (int j = 0; j < L; ++j) {
if (map[i][j] > 1)
stack.push(new Route(i, j));
}
}
stacks.add(stack);
} else {
//否则进行下一层递归
findAllWay(nx, ny);
}
map[nx][ny] = ROUTE;
}
}
4. 最短路径算法
对于无向图两点间的最短路径问题,一般都是采用广度优先搜索(BFS)算法,正确性请自行了解。其思想为从起点出发,采用队列记录当前点能够访问到的点,将其标记为已访问,并不断重复这个过程至找到目标点,队列先进先出的特性保证了算法的正确性。为了记录最短路径,如果仍然采用标记的思想,那么由于算法的特性,最终记录的路径会多出来一些小分支,所以我采用自定义Route类记录坐标及其之间的联系。这里采用了链表的思想,即每个点指向他的上一步所在的点。具体实现如下:
最短路径算法
// BFS寻找最优路径
public void findBestWay() {
// 辅助队列
LinkedList<Route> queue = new LinkedList<>();
// 放入起点
queue.offer(new Route(inX, inY));
// 访问标记,用于判断当前坐标是否曾走到过
boolean[][] visit = new boolean[L][L];
visit[inX][inY] = true;
// 队列不为空 且 未找到终点
while (!queue.isEmpty() && !visit[outX][outY]) {
Route route = queue.poll();
int cx = route.getX(), cy = route.getY();
// 继续寻找
for (int i = 0; i < 4; i++) {
// 计算将要到达的坐标
int nx = cx + dirs[i][0];
int ny = cy + dirs[i][1];
// 判断可行性
if (nx > 1 && nx < L - 2 && ny > 0 && ny < L - 1 && map[nx][ny] == ROUTE && !visit[nx][ny]) {
visit[nx][ny] = true;
Route next = new Route(nx, ny, route);
queue.offer(next);
// 找到终点
if (nx == outX && ny == outY) {
LinkedList<Route> stack = new LinkedList<>();
for (Route p = next; p != null; p = p.getPre()) {
stack.push(p);
}
stacks.add(stack);
break;
}
}
}
}
}
接下来是第二件事——图形界面的实现:
算法已经实现的差不多了,现在进行界面的绘制。这里仍然假定各位通过上面提到的视频,已经对JavaFX有一定的了解。
回想我们要实现的功能,手动或自动生成迷宫地图,自动的上面算法已经实现,手动的就需要绘制界面供我们输入。顺着这个思路,我们可以先设计一下交互逻辑,进而确定需要哪些界面,每个界面又对应哪些功能,我的设计方案如下:
以初始界面为例,我们可以通过SceneBuilder软件设计界面,然后保存为fxml文件,如下:
开始界面
<?xml version="1.0" encoding="UTF-8"?>
<!-- 开始界面 -->
<?import javafx.scene.control.Button?>
<?import javafx.scene.control.Label?>
<?import javafx.scene.image.Image?>
<?import javafx.scene.image.ImageView?>
<?import javafx.scene.layout.AnchorPane?>
<?import javafx.scene.text.Font?>
<AnchorPane fx:id="rootStage"
xmlns:fx="http://javafx.com/fxml/1"
fx:controller="controllers.StartController"
prefHeight="600.0" prefWidth="600.0">
<children>
<Label fx:id="title" text='迷宫鼠演示程序' layoutX='150' layoutY='10' prefWidth="300" prefHeight="50"
alignment="CENTER">
<font>
<Font name="BOLD" size="40"/>
</font>
</Label>
<ImageView fx:id="icon" pickOnBounds="true" preserveRatio="true" layoutX="210" layoutY="100">
<image>
<Image url="@../images/maze.png"/>
</image>
</ImageView>
<Button fx:id='btn_manual' text='手动生成' layoutX='200' layoutY='350' onAction="#onManualClick" prefWidth="200"
prefHeight="50"/>
<Button fx:id='btn_auto' text='自动生成' layoutX='200' layoutY='450' onAction="#onAutoClick" prefWidth="200"
prefHeight="50"/>
</children>
</AnchorPane>
编写对应的控制器:
StartController.java
package controllers;
import javafx.fxml.FXML;
import javafx.fxml.FXMLLoader;
import javafx.scene.Parent;
import javafx.scene.Scene;
import javafx.scene.image.Image;
import javafx.scene.layout.AnchorPane;
import javafx.stage.Modality;
import javafx.stage.Stage;
import java.io.IOException;
/**
* @Author 郭小柒w
* @Date 2022/6/24 17:26
* @Description 开始界面逻辑控制
**/
public class StartController {
@FXML
private AnchorPane rootStage; // 父窗口面板
/**
* 手动生成按钮点击事件
*/
public void onManualClick() {
try {
// 加载手动输入界面布局文件
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("/fxmls/input.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root);
// 设置stage
Stage stage = new Stage();
stage.setResizable(false);
stage.getIcons().add(new Image("/images/maze.png"));
stage.setScene(scene);
// 设置父窗体
stage.initOwner(rootStage.getScene().getWindow());
// 设置除当前窗体外其他窗体均不可编辑
stage.initModality(Modality.WINDOW_MODAL);
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
/**
* 自动生成按钮点击事件
*/
public void onAutoClick() {
try {
// 加载迷宫主界面布局文件
FXMLLoader loader = new FXMLLoader();
loader.setLocation(getClass().getResource("/fxmls/menu.fxml"));
Parent root = loader.load();
Scene scene = new Scene(root);
// 获取Controller
MenuController controller = loader.getController();
// 进行迷宫初始化操作
controller.initialize(new int[42][42], MenuController.AUTO, null);
// 设置Stage
Stage stage = new Stage();
stage.setResizable(false);
stage.getIcons().add(new Image("/images/maze.png"));
stage.setScene(scene);
// 设置父窗体
stage.initOwner(rootStage.getScene().getWindow());
// 设置除当前窗体外其他窗体均不可编辑
stage.initModality(Modality.WINDOW_MODAL);
stage.show();
} catch (IOException e) {
e.printStackTrace();
}
}
public void initialize() {
// TODO: 如有需要初始化的内容,请在此方法内完成
}
}
下面进行界面展示。
开始界面:
手动输入界面:
迷宫主界面:
对于手动输入和迷宫展示功能,可以采用合适的JavaFX控件,不再贴出具体代码,控制器和界面的交互逻辑与上述一致。完整代码和实际演示视频见文章开头的链接。
—————————————————我———是———分———割———线————————————————
时间过得可真快呀!毕业后尝试工作了一段时间,这期间也有很多人来问那个C语言迷宫的问题。从那篇文章发布到现在已经两年整了,没想到最近还有机会把它翻新成图形界面表现出来。从我返校考试到放弃考研选择找工作,也已经是一年多以前。之前总是会觉得之后的人生会怎样怎样,设想过无数可能,觉得凭自己对这个专业的热爱总能在岗位上发光发热,,觉得工作是自己感兴趣的东西肯定不会苦闷,却未认识到现实跟想象的差距如此之大。找了份自以为绝对满意的工作,谁料想每天都重复着枯燥的单一工作内容。终于在深思熟虑后还是对之前的工作说拜拜啦,虽然跟老大说自己碰壁了还会回来,但心里不确定我是否真的愿意回去。再找到更心仪的工作之前,要更加努力啊。不放弃对未来的美好幻想,也不虚度了眼下的时光。勇敢的少年啊,快去创造奇迹吧!