基於JavaFX圖形介面演示的迷宮建立與路徑尋找

郭小柒w發表於2022-06-24

事情的起因是收到了一位網友的請求,他的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語言實現一個走迷宮小遊戲(深度優先演算法)

  當然我並沒有全部照搬,只是採用了深度優先的思想。因為這幾種生成演算法都只能產生一條可行路徑。為了體現遍歷和尋優,我直接在迷宮中生成了一條大小合適的環路,並控制生成迷宮的複雜程度,這樣一般情況下迷宮會有多條可行路徑,示意圖如下:

基於JavaFX圖形介面演示的迷宮建立與路徑尋找

 

主要程式碼:

構造迷宮

// 修飾迷宮地圖
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有一定的瞭解。

回想我們要實現的功能,手動或自動生成迷宮地圖,自動的上面演算法已經實現,手動的就需要繪製介面供我們輸入。順著這個思路,我們可以先設計一下互動邏輯,進而確定需要哪些介面,每個介面又對應哪些功能,我的設計方案如下:

基於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語言迷宮的問題。從那篇文章釋出到現在已經兩年整了,沒想到最近還有機會把它翻新成圖形介面表現出來。從我返校考試到放棄考研選擇找工作,也已經是一年多以前。之前總是會覺得之後的人生會怎樣怎樣,設想過無數可能,覺得憑自己對這個專業的熱愛總能在崗位上發光發熱,,覺得工作是自己感興趣的東西肯定不會苦悶,卻未認識到現實跟想象的差距如此之大。找了份自以為絕對滿意的工作,誰料想每天都重複著枯燥的單一工作內容。終於在深思熟慮後還是對之前的工作說拜拜啦,雖然跟老大說自己碰壁了還會回來,但心裡不確定我是否真的願意回去。再找到更心儀的工作之前,要更加努力啊。不放棄對未來的美好幻想,也不虛度了眼下的時光。勇敢的少年啊,快去創造奇蹟吧!

相關文章