使用 Java Swing 實現隨機迷宮生成(深度優先搜索 + 棧回溯)、鍵盤控制小球移動、路徑顯示提示功能。核心數據結構:二維格子數組 + 父指針形成生成樹。
package maze;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
import java.util.*;
class Cell {
static final int UNVISITED = 0, VISITED = 1;
final int x, y;
int state = UNVISITED;
Cell parent = null;
Cell(int x, int y) { this.x = x; this.y = y; }
}
public class MazeGame extends JPanel {
private final int size, cellSize, padding;
private final Cell[][] grid;
private int ballX = 0, ballY = 0;
private boolean showPath = false;
public MazeGame(int size, int cellSize, int padding) {
this.size = size;
this.cellSize = cellSize;
this.padding = padding;
this.grid = new Cell[size][size];
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++)
grid[i][j] = new Cell(i, j);
generateMaze();
initInput();
}
// ===== 迷宮生成 =====
private void generateMaze() {
Random rand = new Random();
Stack stack = new Stack<>();
Cell start = grid[rand.nextInt(size)][rand.nextInt(size)];
stack.push(start);
int[] dx = {-1, 0, 1, 0}; // 上右下左
int[] dy = {0, 1, 0, -1};
while (!stack.isEmpty()) {
Cell cur = stack.pop();
if (cur.state == Cell.VISITED) continue;
cur.state = Cell.VISITED;
// 隨機打亂方向順序
List dirs = Arrays.asList(0, 1, 2, 3);
Collections.shuffle(dirs, rand);
for (int dir : dirs) {
int nx = cur.x + dx[dir], ny = cur.y + dy[dir];
if (nx < 0 || nx >= size || ny < 0 || ny >= size) continue;
Cell next = grid[nx][ny];
if (next.state == Cell.UNVISITED) {
next.parent = cur;
stack.push(next);
}
}
}
}
// ===== 繪製 =====
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
drawGrid(g);
drawWalls(g);
drawEntranceExit(g);
drawBall(g);
if (showPath) drawSolutionPath(g);
}
private void drawGrid(Graphics g) {
for (int i = 0; i <= size; i++) {
g.drawLine(padding + i * cellSize, padding,
padding + i * cellSize, padding + size * cellSize);
g.drawLine(padding, padding + i * cellSize,
padding + size * cellSize, padding + i * cellSize);
}
}
private void drawWalls(Graphics g) {
g.setColor(getBackground());
for (int i = size - 1; i >= 0; i--) {
for (int j = size - 1; j >= 0; j--) {
Cell p = grid[i][j].parent;
if (p != null) eraseWall(g, i, j, p.x, p.y);
}
}
}
private void eraseWall(Graphics g, int x1, int y1, int x2, int y2) {
int sx = padding + Math.max(y1, y2) * cellSize;
int sy = padding + Math.max(x1, x2) * cellSize;
int ex = (x1 == x2) ? sx + cellSize : sx;
int ey = (y1 == y2) ? sy + cellSize : sy;
if (sx != ex) { sx++; ex--; } else { sy++; ey--; }
g.drawLine(sx, sy, ex, ey);
}
private void drawEntranceExit(Graphics g) {
g.setColor(Color.BLACK);
g.drawLine(padding, padding + 1, padding, padding + cellSize - 1); // 入口
int end = padding + size * cellSize;
g.drawLine(end, end - 1, end, end - cellSize + 1); // 出口
}
private void drawBall(Graphics g) {
g.setColor(Color.RED);
int cx = padding + ballY * cellSize + cellSize / 2;
int cy = padding + ballX * cellSize + cellSize / 2;
g.fillOval(cx - cellSize / 3, cy - cellSize / 3, cellSize / 2, cellSize / 2);
}
private void drawSolutionPath(Graphics g) {
// 從終點回溯到起點繪製路徑
g.setColor(new Color(255, 165, 0)); // 橙色
Cell cur = grid[size - 1][size - 1];
while (cur.parent != null) {
drawPathSegment(g, cur, cur.parent);
cur = cur.parent;
}
}
private void drawPathSegment(Graphics g, Cell a, Cell b) {
int x1 = padding + a.y * cellSize + cellSize / 2;
int y1 = padding + a.x * cellSize + cellSize / 2;
int x2 = padding + b.y * cellSize + cellSize / 2;
int y2 = padding + b.x * cellSize + cellSize / 2;
g.drawLine(x1, y1, x2, y2);
}
// ===== 鍵盤控制 =====
private void initInput() {
setFocusable(true);
addKeyListener(new KeyAdapter() {
@Override
public void keyPressed(KeyEvent e) {
int nx = ballX, ny = ballY;
switch (e.getKeyCode()) {
case KeyEvent.VK_UP -> nx--;
case KeyEvent.VK_DOWN -> nx++;
case KeyEvent.VK_LEFT -> ny--;
case KeyEvent.VK_RIGHT -> ny++;
case KeyEvent.VK_SPACE -> { showPath = !showPath; repaint(); return; }
}
// 只能沿父子連接移動(即走通道)
if (inBounds(nx, ny) && connected(ballX, ballY, nx, ny)) {
ballX = nx; ballY = ny;
repaint();
checkWin();
}
}
});
}
private boolean inBounds(int x, int y) {
return x >= 0 && x < size && y >= 0 && y < size;
}
private boolean connected(int x1, int y1, int x2, int y2) {
Cell a = grid[x1][y1], b = grid[x2][y2];
return a.parent == b || b.parent == a;
}
private void checkWin() {
if (ballX == size - 1 && ballY == size - 1) {
JOptionPane.showMessageDialog(this, "恭喜通關!", "勝利", JOptionPane.INFORMATION_MESSAGE);
resetGame();
}
}
private void resetGame() {
ballX = ballY = 0;
showPath = false;
for (int i = 0; i < size; i++)
for (int j = 0; j < size; j++) {
grid[i][j].state = Cell.UNVISITED;
grid[i][j].parent = null;
}
generateMaze();
repaint();
}
// ===== 啟動入口 =====
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("迷宮遊戲 - 方向鍵移動,空格顯示路徑");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.add(new MazeGame(30, 20, 20));
frame.pack();
frame.setLocationRelativeTo(null);
frame.setVisible(true);
});
}
}
|
| 模塊 | 技術要點 |
|---|---|
| 迷宮生成 | 隨機 DFS + 顯式棧,非遞歸,避免棧溢出;父指針隱式存儲樹結構 |
| 牆壁繪製 | 僅擦除父子格子間共用邊,其餘保留,O(n²) 遍歷繪製 |
| 移動邏輯 | 僅允許父子相連格子間移動,天然防止穿牆 |
| 路徑提示 | 空格切換,從終點沿父指針回溯繪製 |
| 線程安全 | Swing 事件分發線程中操作狀態,repaint 觸發重繪 |
這個項目完美展示了圖論算法、Swing 繪製、事件驅動編程的綜合應用,是 Java 圖形界面入門的經典實戰。