單執行緒 `paintComponent` 同時負責:
**衝突**:籃子移動頻繁 `repaint` 會打斷蘋果下落的固定時間間隔,導致蘋果速度忽快忽慢。
┌─────────────────────────────────────┐
│ Game Model (非 GUI) │
│ - Apple: x, y, vy, active │
│ - Basket: x, width │
│ - score, state │
│ - update(deltaTime) ← 邏輯更新 │
└──────────────┬──────────────────────┘
│ 狀態查詢 (線程安全)
▼
┌─────────────────────────────────────┐
│ View (JPanel) │
│ - paintComponent(g) ← 只讀模型繪製 │
│ - 固定幀率 ~60FPS 重繪 │
└─────────────────────────────────────┘
// 1. 遊戲模型 (獨立類,無 Swing 依賴)
class GameModel {
Apple apple = new Apple();
Basket basket = new Basket();
int score = 0;
final Object lock = new Object(); // 簡單同步
void update(double dt) {
synchronized(lock) {
apple.fall(dt);
if (basket.catches(apple)) {
score++;
apple.reset();
}
}
}
// 給 View 讀取的快照方法
Apple getApple() { synchronized(lock) { return apple.copy(); } }
Basket getBasket() { synchronized(lock) { return basket.copy(); } }
int getScore() { synchronized(lock) { return score; } }
}
// 2. 邏輯執行緒 (固定時間步)
class GameLoop implements Runnable {
GameModel model;
volatile boolean running = true;
public void run() {
long last = System.nanoTime();
while (running) {
long now = System.nanoTime();
double dt = (now - last) / 1e9;
last = now;
model.update(dt);
try { Thread.sleep(16); } catch (InterruptedException ignored) {}
}
}
}
// 3. 視圖面板 (只讀模型繪製)
class GamePanel extends JPanel {
GameModel model;
public GamePanel(GameModel m) { this.model = m; }
@Override
protected void paintComponent(Graphics g) {
super.paintComponent(g);
Apple a = model.getApple(); // 獲取快照
Basket b = model.getBasket();
g.setColor(Color.RED);
g.fillOval(a.x, a.y, a.size, a.size);
g.setColor(Color.BLUE);
g.fillRect(b.x, getHeight()-50, b.width, 30);
g.drawString("Score: " + model.getScore(), 10, 20);
}
}
// 4. 主程式啟動
public class AppleGame {
public static void main(String[] args) {
GameModel model = new GameModel();
GamePanel panel = new GamePanel(model);
JFrame frame = new JFrame("Catch Apple");
frame.add(panel);
frame.setSize(400, 600);
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setVisible(true);
// 啟動邏輯執行緒
new Thread(new GameLoop(model)).start();
// 視圖固定幀率重繪 (Swing Timer 在 EDT 執行)
new Timer(16, e -> panel.repaint()).start();
// 鍵盤控制籃子 (更新模型)
frame.addKeyListener(new KeyAdapter() {
public void keyPressed(KeyEvent e) {
if (e.getKeyCode() == KeyEvent.VK_LEFT) model.getBasket().moveLeft();
if (e.getKeyCode() == KeyEvent.VK_RIGHT) model.getBasket().moveRight();
}
});
}
}
| 要點 | 說明 |
|---|---|
| 模型無 Swing 依賴 | 可單元測試、可移植、職責單一 |
| 固定時間步 `dt` | 物理模擬穩定,不受幀率波動影響 |
| View 只讀模型快照 | 避免併發修改異常,`synchronized` 或 `copy()` 保證一致性 |
| Swing Timer 驅動重繪 | 在 EDT 執行,符合 Swing 線程規則 |
| 輸入事件更新模型 | 鍵盤/滑鼠盡快更新模型,視圖下一幀自動反映 |