鍵盤響應延遲、失效是 J2ME 遊戲最隱蔽的殺手。本文從 **VM 處理機制**、**遊戲迴路阻塞**、**三代處理架構演進**、**跨平台映射策略** 四個維度,系統剖析根因與解決方案。
在標準遊戲迴路中插入日誌:
while (running) {
updateState(); // 邏輯更新
repaint(); // 請求重繪
serviceRepaints(); // 執行 paint()
sleep(80 - elapsed);
}
在 updateState()、paint()、keyPressed()、keyReleased() 打印時間戳,觀察輸出順序:
UpdateState Paint KeyReleased UpdateState Paint KeyPressed UpdateState Paint KeyReleased KeyPressed
| 策略 | 做法 | 優點 | 缺點 |
|---|---|---|---|
| 優化主迴路 | 減少 paint/upate 耗時、對象池減 GC、分幀處理重邏輯 | 根治、提升整體幀率 | 需深度重構、有上限 |
| 強制讓出 CPU | 在關鍵處插入 Thread.sleep(20) 或 Thread.yield() | 立竿見影、代碼改動極小 | 略降幀率、非標準做法 |
實戰案例:某項目頻繁動態創建圖片導致主迴路卡死,在事件檢查點前插入 sleep(20) 即恢復響應,幀率從 12fps 回升至 18fps (原目標 20fps)。
protected void keyPressed(int k) {
switch (gameState) {
case MENU: if (k==FIRE) startGame(); break;
case PLAY: if (k==LEFT) player.moveLeft(); break;
}
}
缺點:狀態機混雜、邏輯阻塞事件分發、移植噩夢。
curKey 變量存儲當前按鍵UP_INSTANT=-1, UP_CONT=0, DN_INSTANT=1, DN_CONT=2, DN_DBL=3// 32-bit 掩碼,支援 32 個虛擬鍵同時獨立狀態
public static final int GK_UP = 1<<0;
public static final int GK_DOWN = 1<<1;
public static final int GK_LEFT = 1<<2;
public static final int GK_RIGHT = 1<<3;
public static final int GK_NUM0 = 1<<4;
// ... GK_NUM9, GK_STAR, GK_POUND, GK_LSOFT, GK_RSOFT, GK_MSOFT
// 當前幀按鍵狀態
private int curKeys = 0;
// 事件回呼極簡
protected void keyPressed(int k) { curKeys |= mapKey(k); }
protected void keyReleased(int k) { curKeys &= ~mapKey(k); }
// 邏輯層查詢:極快位運算
if ((curKeys & GK_LEFT) != 0) player.moveLeft();
if ((curKeys & (GK_UP | GK_FIRE)) == (GK_UP | GK_FIRE)) player.jumpAttack(); // 組合鍵
優點:O(1) 查詢、天然支援多鍵組合、狀態清晰無轉換邏輯。
厭倦每機型維護 keyCode 表?**統一目標虛擬鍵碼**,僅映射函數差異化:
// 統一目標碼 (所有機型相同)
public static final byte KEY_NONE = -1;
public static final byte KEY_0 = 0;
public static final byte KEY_UL = 1; // 左上
public static final byte KEY_U = 2; // 上
public static final byte KEY_UR = 3; // 右上
public static final byte KEY_L = 4; // 左
public static final byte KEY_A = 5; // 攻擊/確認
public static final byte KEY_R = 6; // 右
public static final byte KEY_DL = 7; // 左下
public static final byte KEY_D = 8; // 下
public static final byte KEY_DR = 9; // 右下
public static final byte KEY_STAR = 10;
public static final byte KEY_POUND = 11;
public static final byte KEY_SOFT1 = 12;
public static final byte KEY_SOFT2 = 13;
// 映射函數:每機型一份實現
protected byte mapKeyCode(int rawCode) {
try {
int action = getGameAction(rawCode); // 標準動作
switch (action) {
case Canvas.UP: return KEY_U;
case Canvas.DOWN: return KEY_D;
case Canvas.LEFT: return KEY_L;
case Canvas.RIGHT: return KEY_R;
case Canvas.FIRE: return KEY_A;
case Canvas.GAME_A: return KEY_STAR;
case Canvas.GAME_B: return KEY_POUND;
case Canvas.GAME_C: return KEY_SOFT1;
case Canvas.GAME_D: return KEY_SOFT2;
}
} catch (Exception e) { /* S700 等不支援 getGameAction */ }
// 兜底:直接按 rawCode 映射 (針對特定機型硬編碼)
switch (rawCode) {
case -1: return KEY_U; case -2: return KEY_D; // Nokia 標準負值
case 1: return KEY_U; case 6: return KEY_D; // Motorola 實測值
// ...
}
return KEY_NONE;
}
try {
action = getGameAction(code);
} catch (Exception e) {
action = -1; // 標記不可用
}
// 僅依賴 rawCode 硬編碼映射表
public class InputSystem {
private int curKeys = 0, prevKeys = 0;
private long[] pressTime = new long[32]; // 按下時間戳
private static final int LONG_PRESS_MS = 500;
private static final int DOUBLE_CLICK_MS = 300;
public void onKeyPressed(int raw) {
int vk = mapKeyCode(raw);
if (vk >= 0) {
curKeys |= (1 << vk);
pressTime[vk] = System.currentTimeMillis();
}
}
public void onKeyReleased(int raw) {
int vk = mapKeyCode(raw);
if (vk >= 0) curKeys &= ~(1 << vk);
}
// 每幀調用
public void update() {
prevKeys = curKeys;
}
// 查詢 API
public boolean isDown(int vk) { return (curKeys & (1< LONG_PRESS_MS); }
private boolean wasDown(int vk) { return (prevKeys & (1<
鍵盤響應問題本質是 **事件分發與遊戲迴路搶佔 CPU 時間片** 的競爭。解決路徑:**架構上** 採用虛擬鍵位掩碼分離輸入與邏輯,**平台上** 統一目標碼僅差映射表,**運行時** 保證主迴路適度 sleep 讓出分發機會。掌握此三層,J2ME 遊戲輸入不再是黑盒。