草庐IT

基于JavaFX图形界面演示的迷宫创建与路径寻找

郭小柒w 2023-03-28 原文

事情的起因是收到了一位网友的请求,他的java课设需要设计实现迷宫相关的程序——如标题概括。

我这边不方便透露相关信息,就只把任务要求写出来。

演示视频指路?:

完整代码链接?:

开发工具: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语言迷宫的问题。从那篇文章发布到现在已经两年整了,没想到最近还有机会把它翻新成图形界面表现出来。从我返校考试到放弃考研选择找工作,也已经是一年多以前。之前总是会觉得之后的人生会怎样怎样,设想过无数可能,觉得凭自己对这个专业的热爱总能在岗位上发光发热,,觉得工作是自己感兴趣的东西肯定不会苦闷,却未认识到现实跟想象的差距如此之大。找了份自以为绝对满意的工作,谁料想每天都重复着枯燥的单一工作内容。终于在深思熟虑后还是对之前的工作说拜拜啦,虽然跟老大说自己碰壁了还会回来,但心里不确定我是否真的愿意回去。再找到更心仪的工作之前,要更加努力啊。不放弃对未来的美好幻想,也不虚度了眼下的时光。勇敢的少年啊,快去创造奇迹吧!

有关基于JavaFX图形界面演示的迷宫创建与路径寻找的更多相关文章

  1. ruby - 如何在 Ruby 中顺序创建 PI - 2

    出于纯粹的兴趣,我很好奇如何按顺序创建PI,而不是在过程结果之后生成数字,而是让数字在过程本身生成时显示。如果是这种情况,那么数字可以自行产生,我可以对以前看到的数字实现垃圾收集,从而创建一个无限系列。结果只是在Pi系列之后每秒生成一个数字。这是我通过互联网筛选的结果:这是流行的计算机友好算法,类机器算法:defarccot(x,unity)xpow=unity/xn=1sign=1sum=0loopdoterm=xpow/nbreakifterm==0sum+=sign*(xpow/n)xpow/=x*xn+=2sign=-signendsumenddefcalc_pi(digits

  2. python - 如何使用 Ruby 或 Python 创建一系列高音调和低音调的蜂鸣声? - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我想在固定时间创建一系列低音和高音调的哔哔声。例如:在150毫秒时发出高音调的蜂鸣声在151毫秒时发出低音调的蜂鸣声200毫秒时发出低音调的蜂鸣声250毫秒的高音调蜂鸣声有没有办法在Ruby或Python中做到这一点?我真的不在乎输出编码是什么(.wav、.mp3、.ogg等等),但我确实想创建一个输出文件。

  3. ruby - 使用 Vim Rails,您可以创建一个新的迁移文件并一次性打开它吗? - 2

    使用带有Rails插件的vim,您可以创建一个迁移文件,然后一次性打开该文件吗?textmate也可以这样吗? 最佳答案 你可以使用rails.vim然后做类似的事情::Rgeneratemigratonadd_foo_to_bar插件将打开迁移生成的文件,这正是您想要的。我不能代表textmate。 关于ruby-使用VimRails,您可以创建一个新的迁移文件并一次性打开它吗?,我们在StackOverflow上找到一个类似的问题: https://sta

  4. ruby-on-rails - 无法使用 Rails 3.2 创建插件? - 2

    我对最新版本的Rails有疑问。我创建了一个新应用程序(railsnewMyProject),但我没有脚本/生成,只有脚本/rails,当我输入ruby./script/railsgeneratepluginmy_plugin"Couldnotfindgeneratorplugin.".你知道如何生成插件模板吗?没有这个命令可以创建插件吗?PS:我正在使用Rails3.2.1和ruby​​1.8.7[universal-darwin11.0] 最佳答案 随着Rails3.2.0的发布,插件生成器已经被移除。查看变更日志here.现在

  5. ruby - 如何使用 RSpec::Core::RakeTask 创建 RSpec Rake 任务? - 2

    如何使用RSpec::Core::RakeTask初始化RSpecRake任务?require'rspec/core/rake_task'RSpec::Core::RakeTask.newdo|t|#whatdoIputinhere?endInitialize函数记录在http://rubydoc.info/github/rspec/rspec-core/RSpec/Core/RakeTask#initialize-instance_method没有很好的记录;它只是说:-(RakeTask)initialize(*args,&task_block)AnewinstanceofRake

  6. ruby - 为什么 SecureRandom.uuid 创建一个唯一的字符串? - 2

    关闭。这个问题需要detailsorclarity.它目前不接受答案。想改进这个问题吗?通过editingthispost添加细节并澄清问题.关闭8年前。Improvethisquestion为什么SecureRandom.uuid创建一个唯一的字符串?SecureRandom.uuid#=>"35cb4e30-54e1-49f9-b5ce-4134799eb2c0"SecureRandom.uuid方法创建的字符串从不重复?

  7. ruby - 有人可以帮助解释类创建的 post_initialize 回调吗 (Sandi Metz) - 2

    我正在阅读SandiMetz的POODR,并且遇到了一个我不太了解的编码原则。这是代码:classBicycleattr_reader:size,:chain,:tire_sizedefinitialize(args={})@size=args[:size]||1@chain=args[:chain]||2@tire_size=args[:tire_size]||3post_initialize(args)endendclassMountainBike此代码将为其各自的属性输出1,2,3,4,5。我不明白的是查找方法。当一辆山地自行车被实例化时,因为它没有自己的initialize方法

  8. ruby - 寻找通过阅读代码确定编程语言的ruby gem? - 2

    几个月前,我读了一篇关于ruby​​gem的博客文章,它可以通过阅读代码本身来确定编程语言。对于我的生活,我不记得博客或gem的名称。谷歌搜索“ruby编程语言猜测”及其变体也无济于事。有人碰巧知道相关gem的名称吗? 最佳答案 是这个吗:http://github.com/chrislo/sourceclassifier/tree/master 关于ruby-寻找通过阅读代码确定编程语言的rubygem?,我们在StackOverflow上找到一个类似的问题:

  9. ruby - 使用多个数组创建计数 - 2

    我正在尝试按0-9和a-z的顺序创建数字和字母列表。我有一组值value_array=['0','1','2','3','4','5','6','7','8','9','a','b','光盘','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','','u','v','w','x','y','z']和一个组合列表的数组,按顺序,这些数字可以产生x个字符,比方说三个list_array=[]和一个当前字母和数字组合的数组(在将它插入列表数组之前我会把它变成一个字符串,]current_combo['0','0','0']

  10. 世界前沿3D开发引擎HOOPS全面讲解——集3D数据读取、3D图形渲染、3D数据发布于一体的全新3D应用开发工具 - 2

    无论您是想搭建桌面端、WEB端或者移动端APP应用,HOOPSPlatform组件都可以为您提供弹性的3D集成架构,同时,由工业领域3D技术专家组成的HOOPS技术团队也能为您提供技术支持服务。如果您的客户期望有一种在多个平台(桌面/WEB/APP,而且某些客户端是“瘦”客户端)快速、方便地将数据接入到3D应用系统的解决方案,并且当访问数据时,在各个平台上的性能和用户体验保持一致,HOOPSPlatform将帮助您完成。利用HOOPSPlatform,您可以开发在任何环境下的3D基础应用架构。HOOPSPlatform可以帮您打造3D创新型产品,HOOPSSDK包含的技术有:快速且准确的CAD

随机推荐