Swing 是 Java 經典的桌面 GUI 工具包,雖然現代開發多轉向 JavaFX 或 Web 前端,但 Swing 輕量、成熟、零外部依賴,仍是學習**事件驅動程式設計、繪圖機制、遊戲迴圈**的最佳入門教具。本文帶你從零編寫一個「鍵盤控制四個方塊移動到目標區域」的小遊戲,涵蓋 JFrame、JPanel、自定義繪圖、鍵盤監聽、碰撞檢測、遊戲狀態管理完整流程。
src/
└── game/
├── BoxGame.java // 主程式入口
├── GameFrame.java // 視窗框架
├── GamePanel.java // 繪圖面板 + 遊戲邏輯
└── KeyHandler.java // 鍵盤事件處理
public class BoxGame {
public static void main(String[] args) {
// 在 EDT (Event Dispatch Thread) 啟動 GUI,確保執行緒安全
SwingUtilities.invokeLater(() -> {
GameFrame frame = new GameFrame();
frame.setVisible(true);
});
}
}
public class GameFrame extends JFrame {
public GameFrame() {
setTitle("Java Box Game - 方塊移動小遊戲");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setResizable(false);
// 添加遊戲面板
add(new GamePanel());
pack(); // 根據 PreferredSize 自動調整視窗大小
setLocationRelativeTo(null); // 居中顯示
}
}
public class GamePanel extends JPanel implements ActionListener {
// 遊戲常數
private static final int GRID_SIZE = 30;
private static final int COLS = 10;
private static final int ROWS = 10;
private static final int TARGET_X = 4; // 目標區域左上格子座標
private static final int TARGET_Y = 4;
private static final int TARGET_W = 2; // 2x2 目標區
private static final int TARGET_H = 2;
// 方塊狀態:4 個方塊的 (col, row)
private final int[][] boxes = { {0,0}, {9,0}, {0,9}, {9,9} };
private final boolean[] inTarget = new boolean[4];
private int selectedBox = -1; // 當前選中的方塊索引 (0-3)
// 定時器驅動重繪 (約 60 FPS)
private final Timer repaintTimer = new Timer(16, this);
public GamePanel() {
setPreferredSize(new Dimension(COLS * GRID_SIZE, ROWS * GRID_SIZE));
setBackground(Color.WHITE);
setFocusable(true); // 獲取鍵盤焦點
requestFocusInWindow();
addKeyListener(new KeyHandler(this));
repaintTimer.start();
}
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Graphics2D g2 = (Graphics2D) g;
g2.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
// 1. 繪製網格線 (可選)
g2.setColor(Color.LIGHT_GRAY);
for (int i = 0; i <= COLS; i++) g2.drawLine(i * GRID_SIZE, 0, i * GRID_SIZE, ROWS * GRID_SIZE);
for (int i = 0; i <= ROWS; i++) g2.drawLine(0, i * GRID_SIZE, COLS * GRID_SIZE, i * GRID_SIZE);
// 2. 繪製目標區域 (綠色半透明)
g2.setColor(new Color(0, 200, 0, 100));
g2.fillRect(TARGET_X * GRID_SIZE, TARGET_Y * GRID_SIZE, TARGET_W * GRID_SIZE, TARGET_H * GRID_SIZE);
g2.setColor(Color.GREEN);
g2.drawRect(TARGET_X * GRID_SIZE, TARGET_Y * GRID_SIZE, TARGET_W * GRID_SIZE, TARGET_H * GRID_SIZE);
// 3. 繪製 4 個方塊
for (int i = 0; i < 4; i++) {
int x = boxes[i][0] * GRID_SIZE;
int y = boxes[i][1] * GRID_SIZE;
// 顏色:已到達目標=藍色,選中=紅色邊框,普通=灰色
if (inTarget[i]) g2.setColor(Color.BLUE);
else g2.setColor(Color.DARK_GRAY);
g2.fillRect(x, y, GRID_SIZE, GRID_SIZE);
if (i == selectedBox) {
g2.setColor(Color.RED);
g2.setStroke(new BasicStroke(3));
g2.drawRect(x, y, GRID_SIZE, GRID_SIZE);
g2.setStroke(new BasicStroke(1));
} else {
g2.setColor(Color.BLACK);
g2.drawRect(x, y, GRID_SIZE, GRID_SIZE);
}
// 標號
g2.setColor(Color.WHITE);
g2.drawString(String.valueOf(i+1), x + 10, y + 20);
}
// 4. 勝利判定提示
if (checkWin()) {
g2.setFont(new Font("微軟雅黑", Font.BOLD, 30));
g2.setColor(new Color(255, 0, 0, 200));
String msg = "恭喜通關!按 R 重新開始";
FontMetrics fm = g2.getFontMetrics();
int msgX = (getWidth() - fm.stringWidth(msg)) / 2;
int msgY = getHeight() / 2;
g2.drawString(msg, msgX, msgY);
}
}
// 移動邏輯:嘗試移動 selectedBox 方向 (dx, dy) 格子
public void moveSelected(int dx, int dy) {
if (selectedBox == -1) return;
int newCol = boxes[selectedBox][0] + dx;
int newRow = boxes[selectedBox][1] + dy;
// 邊界檢查
if (newCol < 0 || newCol >= COLS || newRow < 0 || newRow >= ROWS) return;
// 碰撞檢查:不能移動到其他方塊佔用的格子
for (int i = 0; i < 4; i++) {
if (i != selectedBox && boxes[i][0] == newCol && boxes[i][1] == newRow) return;
}
// 更新位置
boxes[selectedBox][0] = newCol;
boxes[selectedBox][1] = newRow;
// 檢查是否進入目標區
checkTargetEntry(selectedBox);
repaint(); // 觸發重繪
}
private void checkTargetEntry(int idx) {
int col = boxes[idx][0];
int row = boxes[idx][1];
inTarget[idx] = (col >= TARGET_X && col < TARGET_X + TARGET_W &&
row >= TARGET_Y && row < TARGET_Y + TARGET_H);
}
private boolean checkWin() {
return inTarget[0] && inTarget[1] && inTarget[2] && inTarget[3];
}
public void resetGame() {
boxes[0] = new int[]{0,0};
boxes[1] = new int[]{9,0};
boxes[2] = new int[]{0,9};
boxes[3] = new int[]{9,9};
Arrays.fill(inTarget, false);
selectedBox = -1;
repaint();
}
// 選擇方塊 (1-4 鍵)
public void selectBox(int index) {
if (index >= 0 && index < 4) selectedBox = index;
}
@Override public void actionPerformed(ActionEvent e) { repaint(); } // 定時器觸發
}
public class KeyHandler extends KeyAdapter {
private final GamePanel panel;
public KeyHandler(GamePanel panel) { this.panel = panel; }
@Override
public void keyPressed(KeyEvent e) {
int code = e.getKeyCode();
// 數字鍵 1-4 選擇方塊 (支援主鍵盤與小鍵盤)
if (code >= KeyEvent.VK_1 && code <= KeyEvent.VK_4) {
panel.selectBox(code - KeyEvent.VK_1);
} else if (code >= KeyEvent.VK_NUMPAD1 && code <= KeyEvent.VK_NUMPAD4) {
panel.selectBox(code - KeyEvent.VK_NUMPAD1);
}
// 方向鍵移動
else if (code == KeyEvent.VK_UP) panel.moveSelected(0, -1);
else if (code == KeyEvent.VK_DOWN) panel.moveSelected(0, 1);
else if (code == KeyEvent.VK_LEFT) panel.moveSelected(-1, 0);
else if (code == KeyEvent.VK_RIGHT) panel.moveSelected(1, 0);
// R 重新開始
else if (code == KeyEvent.VK_R) panel.resetGame();
}
}
| 概念 | 代碼體現 |
|---|---|
| EDT (事件分派執行緒) | `SwingUtilities.invokeLater()` 啟動 GUI |
| 自定義繪圖 | 繼承 `JPanel` 覆寫 `paintComponent(Graphics g)` |
| 雙緩衝 | Swing 預設開啟雙緩衝,無閃爍 |
| 鍵盤焦點 | `setFocusable(true)` + `requestFocusInWindow()` |
| 事件監聽 | `KeyAdapter` 適配器模式,僅覆寫需要的方法 |
| 遊戲迴圈 | `javax.swing.Timer` 定時觸發 `repaint()` (約 60Hz) |
| 碰撞檢測 | 網格座標比對,移動前預判新位置是否佔用 |
| 狀態機 | `selectedBox`、`inTarget[]`、`checkWin()` 管理遊戲狀態 |
通過這個 200 行左右的小遊戲,你已完整體驗了 Java Swing 的 **視窗管理、自定義繪圖、事件驅動、遊戲迴圈、狀態管理** 核心流程。這些基礎技能可直接遷移至 JavaFX、LibGDX、甚至 Unity (C#) 等現代遊戲開發框架。建議下一步嘗試:將邏輯與渲染分離、引入圖片資源管理、實現簡單關卡系統,逐步構建完整遊戲原型。