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