貪吃蛇是經典程式設計練習題,涵蓋 GUI 事件處理、遊戲迴圈、資料結構(佇列/鏈表)、碰撞偵測等核心概念。本文提供基於 Java Swing 的完整實作,並解析四大核心類別職責與關鍵演算法。
snake/ ├── Frame.java // 主視窗、鍵盤監聽、選單列 ├── Launch.java // 遊戲主迴圈 (TimerTask)、邏輯判斷 ├── Panel.java // 繪圖面板 (JPanel paintComponent) └── SnakeBody.java // 蛇身/食物節點資料模型
package snake;
public class SnakeBody {
int x, y; // 格子座標
public SnakeBody() { this(0, 0); }
public SnakeBody(int x, int y) { this.x = x; this.y = y; }
public SnakeBody(SnakeBody other) { this(other.x, other.y); }
// 方向移動:直接修改座標 (可改為回傳新實例以符合不可變原則)
public void turnUp() { y--; }
public void turnDown() { y++; }
public void turnLeft() { x--; }
public void turnRight() { x++; }
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!(obj instanceof SnakeBody)) return false;
SnakeBody o = (SnakeBody) obj;
return x == o.x && y == o.y;
}
}
package snake;
import javax.swing.*;
import java.awt.*;
import java.util.LinkedList;
public class Panel extends JPanel {
static final int GRID_SIZE = 10; // 10x10 格子
LinkedList<SnakeBody> snake = new LinkedList<>();
SnakeBody food = new SnakeBody(-99, -99); // 初始隱藏
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
int w = getWidth(), h = getHeight();
int cw = w / GRID_SIZE, ch = h / GRID_SIZE;
// 繪製蛇身
for (SnakeBody b : snake) {
g.setColor(Color.ORANGE);
g.fillRect(b.x * cw, b.y * ch, cw, ch);
g.setColor(Color.BLACK);
g.drawRect(b.x * cw, b.y * ch, cw, ch);
}
// 繪製食物
g.setColor(Color.RED);
g.fillRect(food.x * cw, food.y * ch, cw, ch);
}
public void reset() { snake.clear(); }
}
package snake;
import javax.swing.*;
import java.awt.*;
import java.awt.event.*;
public class Frame extends JFrame implements KeyListener {
Panel panel = new Panel();
volatile char direction = 'd'; // 初始向右
volatile boolean alive = true, paused = false;
public Frame() {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 400);
setTitle("貪吃蛇 - Swing 版");
add(panel);
addKeyListener(this);
setJMenuBar(createMenuBar());
setLocationRelativeTo(null);
setVisible(true);
}
private JMenuBar createMenuBar() {
JMenuBar mb = new JMenuBar();
JMenu menu = new JMenu("遊戲");
JMenuItem newGame = new JMenuItem("新遊戲 (R)");
JMenuItem pause = new JMenuItem("暫停/繼續 (P)");
newGame.addActionListener(e -> resetGame());
pause.addActionListener(e -> paused = !paused);
menu.add(newGame); menu.add(pause);
mb.add(menu);
return mb;
}
public void resetGame() {
panel.reset();
alive = true;
direction = 'd';
// 重置邏輯由 Launch 初始化
}
@Override public void keyTyped(KeyEvent e) {}
@Override public void keyReleased(KeyEvent e) {}
@Override
public void keyPressed(KeyEvent e) {
// 防止 180 度反向導致瞬間自殺
switch (e.getKeyCode()) {
case KeyEvent.VK_UP: if (direction != 's') direction = 'w'; break;
case KeyEvent.VK_DOWN: if (direction != 'w') direction = 's'; break;
case KeyEvent.VK_LEFT: if (direction != 'd') direction = 'a'; break;
case KeyEvent.VK_RIGHT: if (direction != 'a') direction = 'd'; break;
case KeyEvent.VK_P: paused = !paused; break;
case KeyEvent.VK_R: resetGame(); break;
}
}
}
package snake;
import java.util.*;
public class Launch extends TimerTask {
Frame frame;
static final int DELAY_MS = 200; // 速度
public Launch(Frame frame) { this.frame = frame; }
@Override
public void run() {
if (frame.panel.snake.isEmpty()) initGame();
if (frame.alive && !frame.paused) {
boolean dead = moveSnake();
frame.alive = !dead;
frame.repaint(); // 觸發 Panel 重繪
}
if (checkWallCrash()) frame.alive = false;
}
private void initGame() {
frame.panel.snake.add(new SnakeBody(5, 5));
spawnFood();
}
private void spawnFood() {
Random r = new Random();
SnakeBody food;
do {
food = new SnakeBody(r.nextInt(Panel.GRID_SIZE - 2) + 1,
r.nextInt(Panel.GRID_SIZE - 2) + 1);
} while (frame.panel.snake.contains(food)); // 避免生成在蛇身上
frame.panel.food = food;
}
private boolean moveSnake() {
SnakeBody head = new SnakeBody(frame.panel.snake.getFirst());
frame.panel.snake.removeLast(); // 尾巴縮回 (未吃食物時)
// 依方向移動頭部
switch (frame.direction) {
case 'w': head.turnUp(); break;
case 's': head.turnDown(); break;
case 'a': head.turnLeft(); break;
case 'd': head.turnRight(); break;
}
// 自撞檢測
if (frame.panel.snake.contains(head)) return true;
frame.panel.snake.addFirst(head);
// 吃食物
if (head.equals(frame.panel.food)) {
frame.panel.snake.addFirst(new SnakeBody(head)); // 身體增長
spawnFood();
}
return false;
}
private boolean checkWallCrash() {
SnakeBody h = frame.panel.snake.getFirst();
return h.x < 0 || h.x >= Panel.GRID_SIZE || h.y < 0 || h.y >= Panel.GRID_SIZE;
}
public static void main(String[] args) {
Frame f = new Frame();
Timer timer = new Timer("GameLoop");
timer.schedule(new Launch(f), 0, DELAY_MS);
}
}
| 面向 | 現狀 | 建議改進 |
|---|---|---|
| 執行緒安全 | Swing 元件在 TimerTask 執行緒修改資料 | 使用 SwingUtilities.invokeLater 或改用 javax.swing.Timer |
| 狀態管理 | 方向、生死旗標為 volatile | 封裝為 GameState 物件,集中管理 |
| 擴充性 | 硬編碼 10x10 格子 | 支援動態格子大小、關卡、障礙物 |
| 渲染優化 | 全域重繪 | 僅重繪變動區域 (repaint(x,y,w,h)) |
此實作展示了 MVC 變體:Frame (Controller/View)、Launch (Model/Controller)、Panel (View)、SnakeBody (Model)。理解職責分離與遊戲迴圈設計,是進階開發 2D 遊戲的基石。歡迎 Fork 擴充計分、關卡、AI 對戰等功能!