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

Java Swing 遊戲開發入門實戰:從視窗架構到事件驅動完整教學 (第一部分)

     編輯:Java 相關知識

前言

Swing 是 Java 經典桌面 GUI 框架,雖然現代開發多轉向 JavaFX/Compose/Web,但 Swing 的事件分派執行緒 (EDT)、模型-視圖-控制器 (MVC)、佈局管理器、自定繪圖 等核心概念,是理解所有 GUI 框架的基石。本系列以「連連看」遊戲為例,分階段講解 Swing 遊戲開發完整流程。

一、頂層容器:視窗根節點

三大頂層容器

基本視窗骨架


import javax.swing.*;
import java.awt.*;

public class KyodaiGame extends JFrame {
    public KyodaiGame() {
        // 1. 基本屬性設定
        setTitle("連連看 - Java Swing 版");
        setSize(800, 600);                    // 視窗大小
        setDefaultCloseOperation(EXIT_ON_CLOSE); // 關閉時退出 JVM
        setResizable(false);                   // 固定大小 (遊戲常見)
        setLocationRelativeTo(null);           // 螢幕置中
        
        // 2. 初始化遊戲面板 (核心內容區)
        GamePanel gamePanel = new GamePanel();
        setContentPane(gamePanel);             // 直接設定為內容面板
        
        // 3. 可選:選單列
        setJMenuBar(createMenuBar());
    }
    
    private JMenuBar createMenuBar() {
        JMenuBar bar = new JMenuBar();
        JMenu gameMenu = new JMenu("遊戲");
        JMenuItem newGame = new JMenuItem("新遊戲 (F2)");
        JMenuItem exit = new JMenuItem("離開");
        gameMenu.add(newGame);
        gameMenu.addSeparator();
        gameMenu.add(exit);
        bar.add(gameMenu);
        return bar;
    }
    
    public static void main(String[] args) {
        // 關鍵:在 EDT 上啟動 GUI
        SwingUtilities.invokeLater(() -> {
            new KyodaiGame().setVisible(true);
        });
    }
}

二、元件層次與容器

容器 vs 非容器元件

  • 容器元件:可包含其他元件,繼承 Container。如 JPanel、JScrollPane、JTabbedPane、JSplitPane、JToolBar
  • 非容器元件:葉子節點,如 JButton、JLabel、JTextField、JProgressBar、JSlider

添加元件正確姿勢


// 頂層容器必須透過 getContentPane() 或 setContentPane()
frame.getContentPane().add(panel, BorderLayout.CENTER); // 正確
// frame.add(panel); // Java 5+ 自動轉發到 contentPane,但建議明確

// 一般容器直接 add()
JPanel panel = new JPanel();
panel.add(new JButton("按鈕")); // 直接加到 panel

三、佈局管理器:自適應 UI 核心

核心佈局比較

類別用途特徵
JFrame主應用視窗可獨立存在、有標題列/邊框/最小化/最大化/關閉、支援選單列
JDialog對話框依附於父視窗、可模態(阻塞父視窗)/非模態
JApplet瀏覽器嵌入小程式已淘汰,瀏覽器不再支援 Java Plugin
佈局特性適用場景關鍵約束
BorderLayout五大區域 (NORTH, SOUTH, EAST, WEST, CENTER)主視窗整體結構、對話框CENTER 自動填滿剩餘空間,每區域最多一個元件
GridLayout規則網格 (rows × cols)計算機鍵盤、棋盤、表單欄位所有格子等大、按序填入
FlowLayout左到右、換行流動工具列、按鈕群、簡單面板元件維持偏好大小、靠左/置中/靠右對齊
BoxLayout單向堆疊 (X_AXIS/Y_AXIS)表單垂直堆疊、工具列水平排列可配合 glue/strut/rigid area 控制間距
GridBagLayout最強彈性網格 (元件可跨行列)複雜表單、精確對齊介面GridBagConstraints 參數多、學習曲線陡
CardLayout卡片堆疊、一次顯示一張精靈對話框、分頁切換、遊戲狀態切換需手動切換顯示哪張卡片
GroupLayoutIDE 視覺化設計器首選 (Matisse)手寫極難維護不建議手寫
MigLayout (第三方)字串約束、極度簡潔強大需引入庫現代專案強烈推薦

遊戲主視窗佈局實戰


public class GamePanel extends JPanel {
    public GamePanel() {
        // 1. 使用 BorderLayout 分割三大區域
        setLayout(new BorderLayout());
        setBackground(Color.DARK_GRAY);
        
        // 2. 頂部工具列 (NORTH)
        JPanel toolBar = createToolBar();
        toolBar.setPreferredSize(new Dimension(800, 60));
        add(toolBar, BorderLayout.NORTH);
        
        // 3. 右側互動區 (EAST)
        JPanel sidePanel = createSidePanel();
        sidePanel.setPreferredSize(new Dimension(200, 540));
        add(sidePanel, BorderLayout.EAST);
        
        // 4. 中央遊戲區 (CENTER) - 自動填滿剩餘空間
        BoardPanel boardPanel = new BoardPanel();
        add(boardPanel, BorderLayout.CENTER);
    }
    
    private JPanel createToolBar() {
        JPanel bar = new JPanel(new FlowLayout(FlowLayout.LEFT, 10, 10));
        bar.setBackground(new Color(45, 45, 45));
        bar.setBorder(BorderFactory.createMatteBorder(0, 0, 2, 0, Color.GRAY));
        
        JButton newGameBtn = new JButton("新遊戲");
        JButton hintBtn = new JButton("提示");
        JLabel scoreLabel = new JLabel("分數: 0");
        scoreLabel.setForeground(Color.WHITE);
        
        bar.add(newGameBtn);
        bar.add(hintBtn);
        bar.add(Box.createHorizontalGlue()); // 推到右邊
        bar.add(scoreLabel);
        return bar;
    }
    
    private JPanel createSidePanel() {
        JPanel panel = new JPanel();
        panel.setLayout(new BoxLayout(panel, BoxLayout.Y_AXIS));
        panel.setBackground(new Color(35, 35, 35));
        panel.setBorder(BorderFactory.createEmptyBorder(20, 10, 20, 10));
        
        // 玩家資訊卡片
        JPanel infoCard = createInfoCard();
        panel.add(infoCard);
        panel.add(Box.createVerticalStrut(20));
        
        // 道具欄
        JPanel itemPanel = createItemPanel();
        panel.add(itemPanel);
        
        return panel;
    }
}

四、邊框與視覺美化

BorderFactory 靜態工廠方法


// 線條邊框
Border line = BorderFactory.createLineBorder(Color.GRAY, 2);

// 立體邊框 (凸起/凹陷)
Border raised = BorderFactory.createRaisedBevelBorder();
Border lowered = BorderFactory.createLoweredBevelBorder();
Border bevel = BorderFactory.createBevelBorder(BevelBorder.RAISED, 
    new Color(100,100,100), new Color(50,50,50)); // 高光/陰影色

// 空白邊框 (內距)
Border padding = BorderFactory.createEmptyBorder(10, 15, 10, 15); // 上右下左

// 標題邊框 (含標題文字)
Border titled = BorderFactory.createTitledBorder(
    BorderFactory.createLineBorder(Color.BLUE), 
    "玩家資訊", 
    TitledBorder.LEFT, TitledBorder.TOP);

// 組合邊框 (外層 + 內層)
Border compound = BorderFactory.createCompoundBorder(
    BorderFactory.createLineBorder(Color.GRAY),
    BorderFactory.createEmptyBorder(10, 10, 10, 10)
);

// 套用到元件
panel.setBorder(compound);

五、事件驅動程式設計

監聽器模式核心

  • 事件源 (Event Source):產生事件的元件 (按鈕、滑鼠、鍵盤、計時器)
  • 事件物件 (Event Object):封裝事件資訊 (ActionEvent, MouseEvent, KeyEvent)
  • 事件監聽器 (Event Listener):實作特定介面 (ActionListener, MouseListener)
  • 註冊:source.addActionListener(listener)

按鈕點擊事件實作


public class GamePanel extends JPanel implements ActionListener {
    private JButton newGameBtn;
    private JButton hintBtn;
    
    public GamePanel() {
        // ... 佈局代碼 ...
        
        newGameBtn = new JButton("新遊戲");
        newGameBtn.addActionListener(this); // 註冊監聽器
        hintBtn = new JButton("提示");
        hintBtn.addActionListener(this);
    }
    
    @Override
    public void actionPerformed(ActionEvent e) {
        Object source = e.getSource(); // 取得事件來源
        
        if (source == newGameBtn) {
            startNewGame();
        } else if (source == hintBtn) {
            showHint();
        }
    }
    
    private void startNewGame() {
        // 重置遊戲狀態、重新洗牌、重繪棋盤
        boardPanel.resetGame();
        score = 0;
        updateScoreLabel();
    }
    
    private void showHint() {
        // 尋找可消除配對、高亮顯示
        Pair hint = boardPanel.findHint();
        if (hint != null) {
            boardPanel.highlightPair(hint);
            // 2 秒後取消高亮
            Timer timer = new Timer(2000, evt -> boardPanel.clearHighlight());
            timer.setRepeats(false);
            timer.start();
        }
    }
}

Lambda 表達式簡化監聽器 (Java 8+)


newGameBtn.addActionListener(e -> startNewGame());
hintBtn.addActionListener(e -> showHint());

// 滑鼠事件範例
boardPanel.addMouseListener(new MouseAdapter() {
    @Override
    public void mousePressed(MouseEvent e) {
        if (SwingUtilities.isLeftMouseButton(e)) {
            boardPanel.handleTileClick(e.getX(), e.getY());
        }
    }
});

六、對話框與訊息提示

JOptionPane 靜態便利方法


// 資訊對話框
JOptionPane.showMessageDialog(parent, "遊戲說明內容...", "關於", JOptionPane.INFORMATION_MESSAGE);

// 確認對話框
int result = JOptionPane.showConfirmDialog(parent, "確定要離開嗎?", "確認", JOptionPane.YES_NO_OPTION);
if (result == JOptionPane.YES_OPTION) System.exit(0);

// 輸入對話框
String name = JOptionPane.showInputDialog(parent, "請輸入玩家名稱:", "新玩家", JOptionPane.QUESTION_MESSAGE);

// 選項對話框
Object[] options = {"簡單", "普通", "困難"};
int choice = JOptionPane.showOptionDialog(parent, "選擇難度", "難度設定", 
    JOptionPane.DEFAULT_OPTION, JOptionPane.QUESTION_MESSAGE, null, options, options[1]);

七、執行緒安全:EDT 鐵律

Swing 單執行緒規則:所有 GUI 元件的建立、修改、查詢必須在事件分派執行緒 (EDT) 上進行。違反會導致畫面凍結、資料不一致、難以重現的 Bug。

正確模式


// 1. 程式進入點:在 EDT 上啟動
public static void main(String[] args) {
    SwingUtilities.invokeLater(() -> new KyodaiGame().setVisible(true));
}

// 2. 背景執行緒完成後更新 UI:SwingUtilities.invokeLater()
new Thread(() -> {
    // 耗時操作:載入資源、網路請求、複雜計算
    ImageIcon background = loadBackgroundImage();
    
    // 回到 EDT 更新 UI
    SwingUtilities.invokeLater(() -> {
        backgroundLabel.setIcon(background);
        loadingLabel.setVisible(false);
    });
}).start();

// 3. 定時任務:javax.swing.Timer (自動在 EDT 執行)
Timer gameTimer = new Timer(1000, e -> {
    timeRemaining--;
    timeLabel.setText("時間: " + timeRemaining);
    if (timeRemaining <= 0) gameOver();
});
gameTimer.start();

錯誤模式


// ❌ 錯誤:在背景執行緒直接修改 UI
new Thread(() -> {
    scoreLabel.setText("分數: " + score); // 危險!
}).start();

// ❌ 錯誤:在 EDT 執行耗時操作導致介面凍結
public void actionPerformed(ActionEvent e) {
    Thread.sleep(3000); // 整個 UI 凍結 3 秒
    doSomething();
}

八、自定義繪圖:遊戲渲染核心

繼承 JPanel 覆寫 paintComponent


public class BoardPanel extends JPanel {
    private Tile[][] board; // 遊戲邏輯資料模型
    private int tileSize = 48;
    
    public BoardPanel() {
        setPreferredSize(new Dimension(12 * tileSize, 10 * tileSize));
        setBackground(Color.BLACK);
        setFocusable(true); // 可接收鍵盤焦點
    }
    
    @Override
    protected void paintComponent(Graphics g) {
        super.paintComponent(g); // 必須先呼叫父類 (清除背景)
        Graphics2D g2d = (Graphics2D) g;
        
        // 啟用抗鋸齒
        g2d.setRenderingHint(RenderingHints.KEY_ANTIALIASING, RenderingHints.VALUE_ANTIALIAS_ON);
        g2d.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, RenderingHints.VALUE_TEXT_ANTIALIAS_ON);
        
        // 繪製棋盤格
        drawBoard(g2d);
        
        // 繪製方塊
        drawTiles(g2d);
        
        // 繪製選中/提示高亮
        drawHighlights(g2d);
    }
    
    private void drawBoard(Graphics2D g) {
        g.setColor(new Color(30, 30, 30));
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                int x = c * tileSize;
                int y = r * tileSize;
                g.fillRect(x, y, tileSize - 1, tileSize - 1); // -1 留出格線
            }
        }
    }
    
    private void drawTiles(Graphics2D g) {
        for (int r = 0; r < ROWS; r++) {
            for (int c = 0; c < COLS; c++) {
                Tile tile = board[r][c];
                if (tile != null && tile.getImage() != null) {
                    int x = c * tileSize;
                    int y = r * tileSize;
                    g.drawImage(tile.getImage(), x, y, tileSize, tileSize, this);
                }
            }
        }
    }
}

雙重緩衝:消除閃爍


public class BoardPanel extends JPanel {
    public BoardPanel() {
        // Swing 預設開啟雙重緩衝,但可明確確認
        setDoubleBuffered(true);
    }
    
    // 若需手動控制 (極少見):
    // BufferStrategy strategy = getBufferStrategy();
    // if (strategy == null) { createBufferStrategy(2); return; }
    // Graphics g = strategy.getDrawGraphics();
    // try { paintComponent(g); } finally { g.dispose(); }
    // strategy.show();
}

九、資源管理:圖片/音效載入


public class ResourceManager {
    private static final Map imageCache = new ConcurrentHashMap<>();
    private static final Map audioCache = new ConcurrentHashMap<>();
    
    public static Image getImage(String path) {
        return imageCache.computeIfAbsent(path, p -> {
            try {
                // 從 classpath 載入 (JAR 內也適用)
                URL url = ResourceManager.class.getResource("/resources/" + p);
                if (url == null) throw new IllegalArgumentException("Resource not found: " + p);
                return ImageIO.read(url);
            } catch (IOException e) {
                throw new UncheckedIOException(e);
            }
        });
    }
    
    public static AudioClip getSound(String path) {
        return audioCache.computeIfAbsent(path, p -> {
            URL url = ResourceManager.class.getResource("/sounds/" + p);
            return Applet.newAudioClip(url); // 簡單音效足夠
        });
    }
    
    public static void playSound(String path) {
        AudioClip clip = getSound(path);
        if (clip != null) clip.play();
    }
}

十、本章小結與練習

核心概念回顧

  1. JFrame 為頂層視窗,JPanel 為通用容器,透過 LayoutManager 自動佈局
  2. BorderLayout 適合主視窗結構,BoxLayout/FlowLayout 適合區域內部
  3. BorderFactory 提供豐富邊框裝飾,CompoundBorder 組合內外邊距
  4. 事件處理採用 監聽器模式,Java 8+ 推薦 Lambda 簡化代碼
  5. EDT 鐵律:SwingUtilities.invokeLater() 啟動、javax.swing.Timer 定時、背景執行緒回 UI 用 invokeLater()
  6. 自定繪圖覆寫 paintComponent(Graphics),必須呼叫 super.paintComponent(),善用 Graphics2D 進階渲染

練習題

  1. 修改工具列佈局,將「新遊戲」按鈕靠左,「分數」標籤靠右,中間留白自動延展
  2. 為側邊欄加上 TitledBorder 分組:「玩家資訊」、「遊戲道具」、「系統設定」
  3. 實作一個「暫停」對話框,包含「繼續」、「重新開始」、「返回主選單」三個選項
  4. 在棋盤面板加入滑鼠進入/離開事件,滑鼠懸停時顯示格子座標提示
  5. 嘗試使用 MigLayout 重寫主視窗佈局,體驗字串約束的簡潔性

下一章預告

第二部分將深入:遊戲邏輯模型設計 (MVC 分離)、棋盤演算法 (連連看尋路)、動畫系統、狀態機管理、持久化存檔、打包部署。敬請期待!

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