「連連看」規則簡單卻極具深度:在 10×10 棋盤上成對消除相同圖案,連線不可超過兩個轉彎且不穿越其他方塊。本文完整拆解一個採用 MVC 分層 的 J2ME 實現:LinkCanvas(視圖/控制)+ GameEngine(模型/邏輯),重點剖析地圖打亂、直線/一轉/兩轉連線判定、連線路徑記錄三大核心演算法。
LinkCanvas (extends Canvas, implements Runnable)
├── paint() → 呼叫 engine.paintMap / paintSelectArea / paintLinkLine
├── keyPressed() → 方向鍵移動游標,FIRE 鍵呼叫 engine.fire()
├── run() → 固定 100ms 幀率,呼叫 engine.action() + repaint()
└── GameEngine
├── map[10][10] → 0=空,1-10=圖案類型
├── 選取狀態 → firstRow/Col, secondRow/Col, cRow/cCol (游標)
├── 連線類型 → NO_LINK / H_LINK / V_LINK / ONE_CORNER_X/Y / TWO_CORNER
└── 核心演算法 → isEmptyRow/Col/Cell, isOneCornerLink, isTwoCornerLink, logic()
private void initMap() {
// 1. 依行填入 1~10,每行相同 → 保證每種圖案出現偶數次
for (int r=0; r<MAX_ROW; r++)
for (int c=0; c<MAX_COL; c++)
map[r][c] = r + 1;
// 2. 重複 10 次隨機交換兩個非相同格子
for (int i=0; i<10; i++)
for (int r=0; r<MAX_ROW; r++)
for (int c=0; c<MAX_COL; c++) {
int tr = Math.abs(ran.nextInt() % 10);
int tc = Math.abs(ran.nextInt() % 10);
if (tr != r || tc != c) swap(map[r][c], map[tr][tc]);
}
}
關鍵點: 初始按行填充確保每種圖案恰好 10 個(偶數),隨機交換保持總數不變,理論上必有解(但未檢查連通性,極少數情況可能無解,商業版需加入「可解性驗證 + 重洗」邏輯)。
protected void paint(Graphics g) {
clearScreen(g); // 白底
engine.paintMap(g); // 方塊與數字
engine.paintSelectArea(g); // 綠框游標 + 紅框已選
engine.paintLinkLine(g); // 消除連線動畫(閃現後清除)
}
paintMap:雙層迴圈繪製非零格子,drawRect + drawString。paintSelectArea:依 selectTileNum (0/1/2) 繪製游標與已選高亮。paintLinkLine:依 linkType 繪製直線/折線,繪製完立即重置 linkType = NO_LINK,形成「閃一下」視覺回饋。// 檢查 (r1,c1) 到 (r2,c2) 之間(不含端點)同行是否全為 0
private boolean isEmptyRow(int r1, int c1, int r2, int c2) {
if (r1 != r2) return false;
if (c1 > c2) for (int c=c1-1; c>c2; c--) if (map[r1][c]!=0) return false;
else for (int c=c2-1; c>c1; c--) if (map[r1][c]!=0) return false;
return true;
}
// isEmptyCol 同理,檢查同列
private boolean isEmptyCell(int r, int c) { return map[r][c] == 0; }
在 logic() 中優先判斷:
if (isEmptyRow(fr, fc, sr, sc)) return H_LINK;
if (isEmptyCol(fr, fc, sr, sc)) return V_LINK;
轉折點必須是 (fr, sc) 或 (sr, fc) 且為空,且兩段皆為直線通路:
private int isOneCornerLink(int r1, int c1, int r2, int c2) {
// 情況 A:先橫後豎,轉折點 (r1, c2)
if (isEmptyCell(r1, c2) && isEmptyRow(r1, c1, r1, c2) && isEmptyCol(r1, c2, r2, c2))
return ONE_CORNER_FIRSTX; // 先 X(橫)
// 情況 B:先豎後橫,轉折點 (r2, c1)
if (isEmptyCell(r2, c1) && isEmptyCol(r1, c1, r2, c1) && isEmptyRow(r2, c1, r2, c2))
return ONE_CORNER_FIRSTY; // 先 Y(豎)
return NO_LINK;
}
private boolean isTwoCornerLink(int r1, int c1, int r2, int c2) {
// 1. 向上掃描
for (int r=r1-1; r>=0; r--) {
if (map[r][c1]!=0) break; // 撞牆停止
p[0]=r; p[1]=c1; // 記錄第一轉折點
if (isOneCornerLink(r, c1, r2, c2) != NO_LINK) { // 能一轉到達終點?
// 記錄第二轉折點
switch(result) { case ONE_CORNER_FIRSTX: p[2]=r; p[3]=c2; break; ... }
return true;
}
}
// 2. 向下、向左、向右 同理...
// 3. 特例:繞過邊界(視棋盤外圍為空)
if (isEmptyCol(r1,c1,-1,c1) && isEmptyCol(r2,c2,-1,c2)) { p[0]=-1; p[1]=c1; p[2]=-1; p[3]=c2; return true; }
// 左/下/右 邊界同理...
return false;
}
演算法複雜度: O(Rows + Cols) 級,10×10 棋盤極快,無效能壓力。
logic() 與 action()private int logic() {
if (map[fr][fc] != map[sr][sc]) return NO_LINK; // 圖案不同
if (isEmptyRow(...)) return H_LINK;
if (isEmptyCol(...)) return V_LINK;
int r = isOneCornerLink(...); if (r != NO_LINK) return r;
if (isTwoCornerLink(...)) return TWO_CORNER;
return NO_LINK;
}
public boolean action() {
if (selectTileNum != 2) return false;
linkType = logic();
if (linkType != NO_LINK) {
map[fr][fc] = map[sr][sc] = 0; // 消除
selectTileNum = 0; return true;
}
selectTileNum = 0; return false;
}
public void keyPressed(int keyCode) {
int action = getGameAction(keyCode); // MIDP 內建映射
switch(action) {
case UP: engine.moveUP(); break;
case DOWN: engine.moveDown(); break;
case LEFT: engine.moveLeft(); break;
case RIGHT: engine.moveRight(); break;
case FIRE: engine.fire(); break; // 選取/確認
}
}
游標移動邊界檢查在 GameEngine.moveXxx() 內完成,cRow/cCol 限制在 [0, MAX-1]。
| 模組 | 教學版現狀 | 商業級增強 |
|---|---|---|
| 地圖生成 | 隨機交換 10 次 | 演算法生成保證可解 + 難度分級 + 關卡編輯器匯入 |
| 圖形資源 | drawString 數字佔位 | 載入 PNG 圖塊,支援動畫方塊、特效粒子 |
| 連線動畫 | 單幀閃現 | 漸顯/漸隱、粒子流、聲音同步、連擊加分 |
| 狀態管理 | 單一 Canvas | 狀態機:選單/遊戲/暫停/設定/成就/排行榜 |
| 存檔 | 無 | RMS 存檔:進度、最高分、音樂開關、自訂鍵位 |
| 效能 | 全量重繪 | 髒矩形更新、雙緩衝(GameCanvas)、物件池 |
本實現以 約 300 行核心代碼 完整呈現連連看核心邏輯,展示了在 MIDP 1.0 受限環境下:
map[][] 單一真實來源,繪製邏輯純函數化。selectTileNum、first/second、cRow/cCol、linkType 驅動整個遊戲流程。此代碼可直接作為 J2ME 遊戲開發教學範例,或作為現代平台(Android/LibGDX/Unity)移植時的演算法參考實現。