首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 相關知識

Java Swing 入門實戰:從零編寫可鍵盤控制移動方塊小遊戲

     編輯:Java 相關知識

前言

Swing 是 Java 經典的桌面 GUI 工具包,雖然現代開發多轉向 JavaFX 或 Web 前端,但 Swing 輕量、成熟、零外部依賴,仍是學習**事件驅動程式設計、繪圖機制、遊戲迴圈**的最佳入門教具。本文帶你從零編寫一個「鍵盤控制四個方塊移動到目標區域」的小遊戲,涵蓋 JFrame、JPanel、自定義繪圖、鍵盤監聽、碰撞檢測、遊戲狀態管理完整流程。

一、專案結構與主類

src/
└── game/
    ├── BoxGame.java          // 主程式入口
    ├── GameFrame.java        // 視窗框架
    ├── GamePanel.java        // 繪圖面板 + 遊戲邏輯
    └── KeyHandler.java       // 鍵盤事件處理

BoxGame.java

public class BoxGame {
    public static void main(String[] args) {
        // 在 EDT (Event Dispatch Thread) 啟動 GUI,確保執行緒安全
        SwingUtilities.invokeLater(() -> {
            GameFrame frame = new GameFrame();
            frame.setVisible(true);
        });
    }
}

二、視窗框架 GameFrame

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); // 居中顯示
    }
}

三、核心遊戲面板 GamePanel

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(); } // 定時器觸發
}

四、鍵盤事件處理 KeyHandler

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()` 管理遊戲狀態

六、常見問題與優化

  • 鍵盤無反應:確保 `GamePanel` 有焦點,未被其他組件搶佔。可在 `addNotify()` 中 `requestFocusInWindow()`。
  • 畫面閃爍:不要在 `paintComponent` 外繪圖,確保 `super.paintComponent(g)` 最先調用。
  • 移動卡頓:`Timer` 延遲設為 16ms (60FPS),邏輯與繪圖分離 (此處簡單合併)。複雜遊戲建議獨立邏輯執行緒 + `volatile` 狀態同步。
  • 擴展方向:添加關卡編輯器、記分板、音效 (Clip)、圖片素材 (ImageIcon)、敵人 AI (A*)、存檔/讀檔 (Preferences/JSON)。

七、結語

通過這個 200 行左右的小遊戲,你已完整體驗了 Java Swing 的 **視窗管理、自定義繪圖、事件驅動、遊戲迴圈、狀態管理** 核心流程。這些基礎技能可直接遷移至 JavaFX、LibGDX、甚至 Unity (C#) 等現代遊戲開發框架。建議下一步嘗試:將邏輯與渲染分離、引入圖片資源管理、實現簡單關卡系統,逐步構建完整遊戲原型。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved