Java AWT/Swing 提供了豐富的圖像處理 API (Image, Graphics, MediaTracker) 與內建多執行緒支援。結合兩者,可實現**靜態圖像動態化**效果:將一張完整圖片分割為多塊拼圖,利用執行緒控制動畫幀率、處理用戶交互 (拖拽、點擊)、實現拼圖遊戲邏輯。本文深度解析圖像分割算法、MediaTracker 異步載入監控、雙緩衝繪圖、遊戲主迴路執行緒模型、交互事件處理等核心技術。
final int ROWS = 4; // 行數
final int COLS = 5; // 列數
final int TOTAL = ROWS * COLS; // 20 片
final int EMPTY_INDEX = TOTAL - 1; // 最後一片設為空白 (用於移動空位)
class Piece {
Image image; // 該片段圖像
int startX, startY; // 原始正確位置 (網格座標)
int currX, currY; // 當前顯示位置 (像素座標)
int index; // 0 ~ TOTAL-1
Piece(Image img, int x, int y, int idx) {
this.image = img;
this.startX = x; this.startY = y;
this.currX = x; this.currY = y; // 初始位置 = 正確位置 (或打亂)
this.index = idx;
}
boolean isInCorrectPosition() {
return currX == startX && currY == startY;
}
}
// 假設 sourceImage 已載入完整圖片
int pieceW = sourceImage.getWidth(null) / COLS;
int pieceH = sourceImage.getHeight(null) / ROWS;
Piece[] pieces = new Piece[TOTAL];
for (int row = 0; row < ROWS; row++) {
for (int col = 0; col < COLS; col++) {
int idx = row * COLS + col;
// 使用 Graphics 繪製子區域到新 Image (或用 PixelGrabber/CropImageFilter)
Image pieceImg = createImage(new FilteredImageSource(
sourceImage.getSource(),
new CropImageFilter(col * pieceW, row * pieceH, pieceW, pieceH)
));
int x = col * pieceW;
int y = row * pieceH;
pieces[idx] = new Piece(pieceImg, x, y, idx);
}
}
// 將最後一片設為空白 (可創建透明圖像或標記)
pieces[EMPTY_INDEX].image = null; // 或透明圖
圖像載入 (網路/磁碟) 是異步的。MediaTracker 確保繪圖前所有片段**完全載入**:
MediaTracker tracker = new MediaTracker(this); // Component 參數
for (int i = 0; i < TOTAL; i++) {
if (pieces[i].image != null) {
tracker.addImage(pieces[i].image, i); // ID = 索引
}
}
try {
tracker.waitForAll(); // 阻塞直到全部載入完成或出錯
// 檢查錯誤
for (int i = 0; i < TOTAL; i++) {
if (tracker.isErrorID(i)) throw new RuntimeException("圖像 " + i + " 載入失敗");
}
} catch (InterruptedException e) { Thread.currentThread().interrupt(); }
class GameThread extends Thread {
private volatile boolean running = true;
private final int TARGET_FPS = 30;
private final long FRAME_TIME = 1000 / TARGET_FPS;
@Override
public void run() {
while (running) {
long start = System.currentTimeMillis();
// 1. 更新遊戲狀態 (動畫、自動移動等)
updateGameState();
// 2. 繪圖 (雙緩衝)
renderOffscreen();
// 3. 翻頁顯示
repaint(); // 觸發 paint() -> 繪製 offscreen 圖像
// 4. 幀率控制
long elapsed = System.currentTimeMillis() - start;
long sleep = FRAME_TIME - elapsed;
if (sleep > 0) {
try { Thread.sleep(sleep); } catch (InterruptedException e) { running = false; }
}
}
}
void stopGame() { running = false; }
}
private Image offscreenImage;
private Graphics offscreenGraphics;
public void update(Graphics g) { // 避免預設 update 清屏導致閃爍
paint(g);
}
public void paint(Graphics g) {
if (offscreenImage == null) {
offscreenImage = createImage(getWidth(), getHeight());
offscreenGraphics = offscreenImage.getGraphics();
}
// 1. 離屏清屏
offscreenGraphics.setColor(getBackground());
offscreenGraphics.fillRect(0, 0, getWidth(), getHeight());
// 2. 繪製所有拼圖片段
for (Piece p : pieces) {
if (p.image != null) {
offscreenGraphics.drawImage(p.image, p.currX, p.currY, this);
}
}
// 3. 繪製網格線、提示文字等 UI
drawGrid(offscreenGraphics);
drawUI(offscreenGraphics);
// 4. 一次性繪製到螢幕
g.drawImage(offscreenImage, 0, 0, this);
}
addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent e) {
int clickX = e.getX();
int clickY = e.getY();
// 找到點擊位置的片段 (非空白)
for (Piece p : pieces) {
if (p.image != null &&
clickX >= p.currX && clickX < p.currX + pieceW &&
clickY >= p.currY && clickY < p.currY + pieceH) {
// 檢查是否與空白片相鄰 (上下左右)
Piece empty = pieces[EMPTY_INDEX];
if (isAdjacent(p, empty)) {
// 交換位置 (動畫或瞬間)
animateSwap(p, empty); // 或直接 swapPosition(p, empty)
repaint();
}
break;
}
}
}
});
boolean isAdjacent(Piece a, Piece b) {
return (Math.abs(a.currX - b.currX) == pieceW && a.currY == b.currY) ||
(Math.abs(a.currY - b.currY) == pieceH && a.currX == b.currX);
}
void swapPosition(Piece a, Piece b) {
int tx = a.currX, ty = a.currY;
a.currX = b.currX; a.currY = b.currY;
b.currX = tx; b.currY = ty;
}
boolean checkWin() {
for (Piece p : pieces) {
if (p.image != null && !p.isInCorrectPosition()) return false;
}
return true; // 所有非空白片段均在正確位置
}
// 在 updateGameState 或交互後調用
if (checkWin()) {
gameThread.stopGame();
showVictoryDialog(); // 彈窗、計時、重新開始按鈕
}
public class PuzzleGame extends Frame implements Runnable {
// 常數、Piece 內部類、pieces 陣列、MediaTracker
// 雙緩衝 offscreenImage/Graphics
// Thread gameThread
public PuzzleGame() {
// 載入圖片 -> 分割 -> MediaTracker 等待 -> 打亂順序 -> 啟動 gameThread
}
// run() 遊戲主迴路
// update/paint 雙緩衝繪圖
// mousePressed 交互邏輯
// checkWin 勝利判定
// main() 啟動
}
animateSwap 使用補間算法 (線性/緩動) 在數幀內平滑過渡位置。BufferedImage 片段,避免運行時 CropImageFilter 開銷。JPanel、使用 SwingWorker 載入、Timer 驅動幀、paintComponent 繪圖、MouseListener 交互。ImageView + TranslateTransition 動畫、AnimationTimer 遊戲迴路、CSS 樣式化。Java 多執行緒控制圖像分割重組,展示了 **AWT 圖形 API、MediaTracker 異步載入、雙緩衝繪圖、固定幀率遊戲迴路、事件驅動交互** 等核心客戶端技術的有機結合。從拼圖遊戲這一經典案例出發,掌握的圖像分割算法、座標映射、碰撞檢測、狀態機設計、動畫插值等技能,可直接應用於圖片編輯器、視覺化儀表板、教學軟體、休閒遊戲等廣泛領域。隨著 JavaFX、Swing、甚至 Web 技術 (Canvas/WebGL) 的發展,核心邏輯 (分割、狀態、渲染、交互) 永遠是圖形應用開發的不變基石。