MIDP 2.0 Game API 提供 TiledLayer / LayerManager 內建瓦片地圖捲動。MIDP 1.0 需自行實現:**瓦片資料結構、可視區域裁剪、座標轉換、高效繪製**。本文提供完整可運行的瓦片地圖引擎核心代碼 (含 30×40 地圖測試數據)。
| 模組 | 職責 | 關鍵變數 |
|---|---|---|
| MapData | 地圖邏輯數據 | byte[][] tiles 瓦片索引陣列、mapW/mapH 像素尺寸 |
| TileSet | 圖塊圖集 | Image[] tileImages 從大圖切割得來 |
| Camera | 可視窗口 | screenX/Y 左上角世界座標、screenW/H 螢幕尺寸 |
| Renderer | 繪製邏輯 | 計算可視瓦片範圍 → 逐塊 drawImage |
// 螢幕像素座標 → 瓦片索引範圍
int startCol = screenX / tileW;
int endCol = startCol + screenCols + ((screenX % tileW) > 0 ? 1 : 0); // 含部分顯示邊緣
if (endCol > mapCols) endCol = mapCols;
int startRow = screenY / tileH;
int endRow = startRow + screenRows + ((screenY % tileH) > 0 ? 1 : 0);
if (endRow > mapRows) endRow = mapRows;
screenCols = screenW / tileW + (screenW%tileW>0?1:0) 為螢幕可容納瓦片列數 (含不滿一塊)。
public class CanvasMain extends Canvas implements Runnable {
// --- 鍵碼常數 (跨平台虛擬鍵) ---
public static final byte VK_UP = -1, VK_DOWN = -2, VK_LEFT = -3, VK_RIGHT = -4;
public static final byte VK_FIRE = -5, VK_SOFT1 = -6, VK_SOFT2 = -7;
// Nokia/SE/Moto 具體 keyCode 映射於 keyPressed 中處理
// --- 地圖參數 ---
private static byte tileW = 16, tileH = 16;
private static byte[][] map; // 邏輯地圖 [row][col]
private static Image[] tileImages; // 圖塊圖集
private static int mapRows, mapCols, mapW, mapH;
// --- 攝影機參數 ---
private static int screenX = 0, screenY = 0;
private static int screenW = 154, screenH = 136; // 可視區像素
private static int screenCols, screenRows;
private static byte speed = 7; // 捲動步長
// --- 遊戲迴圈 ---
private static final int FRAME_MS = 80;
private static boolean running = false;
public CanvasMain() {
setFullScreenMode(true);
initMap();
initScreen();
System.gc();
}
private static void initMap() {
// 內嵌 30x40 測試地圖數據 (0~77 為瓦片索引)
map = new byte[][] {
{68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,28,9,18,3,3,73,19,25,25},
{68,68,68,68,38,38,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,68,70,68,28,18,3,3,13,14,3,3,19},
// ... 省略中間 38 行 ...
{25,25,25,25,25,29,29,29,29,77,68,68,68,68,68,69,69,69,72,29,36,29,29,25,29,15,72,69,68,68,68,68,68,69,69,72,29,29,29,35}
};
mapRows = map.length;
mapCols = map[0].length;
mapW = mapCols * tileW;
mapH = mapRows * tileH;
// 載入圖集:從 /village.png 切割 16x16 瓦片
tileImages = splitImage("/village.png", tileW, tileH);
}
private static void initScreen() {
screenCols = screenW / tileW + (screenW % tileW > 0 ? 1 : 0);
screenRows = screenH / tileH + (screenH % tileH > 0 ? 1 : 0);
if (screenCols > mapCols) screenCols = mapCols;
if (screenRows > mapRows) screenRows = mapRows;
}
// --- 圖集切割工具 ---
private static Image[] splitImage(String path, int cw, int ch) {
try {
Image src = Image.createImage(path);
int cols = src.getWidth() / cw;
int rows = src.getHeight() / ch;
Image[] tiles = new Image[rows * cols];
for (int i = 0; i < tiles.length; i++) {
int sx = (i % cols) * cw;
int sy = (i / cols) * ch;
tiles[i] = Image.createImage(src, sx, sy, cw, ch, Sprite.TRANS_NONE);
}
return tiles;
} catch (IOException e) { return null; }
}
// --- 繪製核心 ---
protected void paint(Graphics g) {
g.setColor(0xFFFFFF);
g.fillRect(0, 0, getWidth(), getHeight());
// 計算可視瓦片範圍
int startCol = screenX / tileW;
int endCol = startCol + screenCols + ((screenX % tileW) > 0 ? 1 : 0);
if (endCol > mapCols) endCol = mapCols;
int startRow = screenY / tileH;
int endRow = startRow + screenRows + ((screenY % tileH) > 0 ? 1 : 0);
if (endRow > mapRows) endRow = mapRows;
// 繪製可視瓦片
for (int col = startCol; col < endCol; col++) {
for (int row = startRow; row < endRow; row++) {
int tileIdx = map[row][col] & 0xFF; // byte 轉無符號
if (tileIdx != 0xFF && tileIdx < tileImages.length) { // 0xFF 視為空
int dx = 32 + col * tileW - screenX; // 32,64 為螢幕邊距
int dy = 64 + row * tileH - screenY;
g.drawImage(tileImages[tileIdx], dx, dy, Graphics.LEFT | Graphics.TOP);
// 調試:繪製瓦片邊框
g.setColor(0x00FF00);
g.drawRect(dx, dy, tileW, tileH);
g.setColor(0x000000);
}
}
}
// 繪製螢幕邊框與調試資訊
g.drawRect(32, 64, screenW, screenH);
g.drawString("View: " + screenW + "x" + screenH, 5, 5, 0);
g.drawString("Speed: " + speed, 120, 5, 0);
}
// --- 輸入處理 ---
private static int keyCode = 0;
protected void keyPressed(int k) { keyCode = k; handleInput(); repaint(); }
protected void keyRepeated(int k) { handleInput(); }
protected void keyReleased(int k) { keyCode = 0; }
private void handleInput() {
int action = getGameAction(keyCode); // 標準游戲動作
// 兼容數字鍵
if (keyCode >= 48 && keyCode <= 57) {
switch (keyCode) {
case 50: action = Canvas.UP; break; // 2
case 56: action = Canvas.DOWN; break; // 8
case 52: action = Canvas.LEFT; break; // 4
case 54: action = Canvas.RIGHT; break; // 6
}
}
switch (action) {
case Canvas.UP:
screenY = Math.max(0, screenY - speed); break;
case Canvas.DOWN:
screenY = Math.min(mapH - screenH, screenY + speed); break;
case Canvas.LEFT:
screenX = Math.max(0, screenX - speed); break;
case Canvas.RIGHT:
screenX = Math.min(mapW - screenW, screenX + speed); break;
}
}
// --- 遊戲迴圈 ---
public void run() {
running = true;
long last = System.currentTimeMillis();
while (running) {
long now = System.currentTimeMillis();
long elapsed = now - last;
if (elapsed < FRAME_MS) {
try { Thread.sleep(FRAME_MS - elapsed); } catch (InterruptedException ignored) {}
}
last = System.currentTimeMillis();
flushGraphics();
}
}
private void flushGraphics() { repaint(); serviceRepaints(); }
public static void exit() { running = false; }
}
public class MapDemo extends MIDlet implements Runnable {
private Display display;
private CanvasMain canvas;
private Thread thread;
public MapDemo() {
display = Display.getDisplay(this);
canvas = new CanvasMain();
display.setCurrent(canvas);
}
protected void startApp() {
thread = new Thread(this);
thread.start();
}
protected void pauseApp() {}
protected void destroyApp(boolean u) { canvas.exit(); notifyDestroyed(); }
public void run() { canvas.run(); }
}
byte[][]Image.createImage / 釋放map[row][col] 索引對應碰撞屬性表 (可通行/阻擋/傷害/觸發)screenX/Y 得螢幕座標MIDP 1.0 瓦片地圖核心在於:**邏輯陣列 + 圖集切割 + 攝影機裁剪 + 邊界修正**。上述引擎核心 < 200 行,支援鍵盤/數字鍵雙模輸入、固定幀率迴路、調試視覺化。掌握此模式,即可構建 RPG 世界地圖、ACT 關卡場景、SLG 戰略大圖等核心玩法基礎。