MIDP 低階事件:
keyPressed(int keyCode):按下瞬間觸發一次。keyReleased(int keyCode):釋放瞬間觸發一次。keyRepeated(int keyCode):長按持續觸發 (間隔由裝置決定)。痛點:大量低端裝置 不支援 keyRepeated ( Canvas.hasRepeatEvents() == false ),導致遊戲中「長按方向鍵持續移動」失效。
keyPressed 設 leftPressed = true。keyReleased 設 leftPressed = false。if (leftPressed) moveLeft();。keyRepeated,可在其中同步呼叫移動邏輯,獲得更低延遲手感。import javax.microedition.lcdui.*;
import javax.microedition.midlet.*;
public class KeyHoldDemo extends MIDlet {
private Display display;
private GameCanvas canvas;
protected void startApp() {
display = Display.getDisplay(this);
canvas = new GameCanvas();
display.setCurrent(canvas);
new Thread(canvas).start(); // 遊戲迴圈執行緒
}
protected void pauseApp() {}
protected void destroyApp(boolean u) {}
}
class GameCanvas extends Canvas implements Runnable {
// 狀態
private boolean leftPressed, rightPressed;
private int px, py; // 角色位置
private String lastAction = "";
public GameCanvas() {
px = getWidth() / 2;
py = getHeight() / 2;
}
// --- 事件處理 ---
public void keyPressed(int keyCode) {
int action = getGameAction(keyCode);
switch (action) {
case LEFT: leftPressed = true; lastAction = "LEFT"; break;
case RIGHT: rightPressed = true; lastAction = "RIGHT"; break;
case UP: py = Math.max(0, py - 5); lastAction = "UP"; break; // 即時單步
case DOWN: py = Math.min(getHeight(), py + 5); lastAction = "DOWN"; break;
case FIRE: lastAction = "FIRE"; break;
}
repaint();
}
public void keyReleased(int keyCode) {
int action = getGameAction(keyCode);
switch (action) {
case LEFT: leftPressed = false; break;
case RIGHT: rightPressed = false; break;
}
lastAction = "";
repaint();
}
// 若裝置支援 keyRepeated,同步處理可降低感知延遲
public void keyRepeated(int keyCode) {
int action = getGameAction(keyCode);
if (action == LEFT) moveLeft();
else if (action == RIGHT) moveRight();
repaint();
}
// --- 遊戲迴圈 ---
public void run() {
while (true) {
if (leftPressed) moveLeft();
if (rightPressed) moveRight();
try { Thread.sleep(50); } // 20 FPS 輪詢
catch (InterruptedException e) { break; }
}
}
private void moveLeft() {
if (px > 0) px -= 3;
}
private void moveRight() {
if (px < getWidth()) px += 3;
}
// --- 繪製 ---
protected void paint(Graphics g) {
g.setColor(0xFFFFFF); g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(0x000000);
g.drawString("Action: " + lastAction, 10, 10, Graphics.TOP|Graphics.LEFT);
g.drawString("Player", px, py, Graphics.HCENTER|Graphics.TOP);
}
}
| 要點 | 說明 |
|---|---|
| 獨立執行緒跑遊戲迴圈 | 避免阻塞事件分發執行緒,確保 keyPressed/Released 能即時更新旗標 |
固定 Thread.sleep(50) | 約 20 FPS,平衡流暢度與省電;可依遊戲類型調整 (動作遊戲 30~60 FPS) |
旗標用 boolean 而非 int | 原子讀寫,無需同步;若需多鍵組合 (斜向) 可用 int bitmask |
getGameAction() 統一鍵碼 | 屏蔽裝置鍵碼差異 (Nokia 左鍵 -6, Moto -21 等) |
保留 keyRepeated 處理 | 支援的裝置可獲得「零延遲」首次重複觸發,手感更佳 |
// 用位掩碼取代多個 boolean
private int keyMask = 0;
private static final int LEFT = 1;
private static final int RIGHT = 2;
private static final int UP = 4;
private static final int DOWN = 8;
public void keyPressed(int keyCode) {
switch (getGameAction(keyCode)) {
case LEFT: keyMask |= LEFT; break;
case RIGHT: keyMask |= RIGHT; break;
case UP: keyMask |= UP; break;
case DOWN: keyMask |= DOWN; break;
}
}
public void keyReleased(int keyCode) {
switch (getGameAction(keyCode)) {
case LEFT: keyMask &= ~LEFT; break;
case RIGHT: keyMask &= ~RIGHT; break;
case UP: keyMask &= ~UP; break;
case DOWN: keyMask &= ~DOWN; break;
}
}
public void run() {
while (true) {
if ((keyMask & LEFT) != 0) moveLeft();
if ((keyMask & RIGHT) != 0) moveRight();
if ((keyMask & UP) != 0) moveUp();
if ((keyMask & DOWN) != 0) moveDown();
// 斜向自動合成:同時按 LEFT+UP → 左上
Thread.sleep(50);
}
}
paint() 中處理移動 → 幀率不固定、事件延遲、畫面撕裂。必須在獨立迴圈。Thread.sleep() 在事件執行緒 → 凍結 UI、漏收 keyReleased。keyReleased 漏寫) → 角色自動奔跑無法停止。hasRepeatEvents() → 未在支援裝置上啟用原生重複,手感差一檔。「旗標輪詢法」是 J2ME 遊戲處理長按鍵的**通用、穩健、零依賴**標準解法。配合 getGameAction() 屏蔽鍵碼差異、獨立執行緒固定幀率輪詢、保留 keyRepeated 加成,即可在從 2003 年功能機到 2010 年 Symbian 智慧機的全谱系裝置上獲得一致的操作手感。