角色扮演遊戲(RPG)在功能機時代是手機遊戲的皇冠,也是技術難度最高的類型之一:巨大的地圖、複雜的狀態(選單、對話、戰鬥、商店)、海量資源(圖塊、角色、特效)、精確的幀率控制、跨裝置按鍵映射……本系列文章將以實戰代碼拆解一個基於 MIDP 1.0 的完整 RPG 架構,涵蓋幀循環、狀態機、渲染管線、資源管理、按鍵事件解耦五大核心模組。
採用經典的 MIDlet → Canvas → Game Loop 三層分離:
MyRPGMIDlet):生命週期管理、Display 綁定、來電中斷處理、資源釋放、退出入口。MyRPGCanvas):繼承 Canvas 並實作 Runnable,承擔渲染、邏輯更新、按鍵記錄、狀態機驅動。int status 區分每個畫面(LOGO、選單、地圖、商店、對話……),統一控制 paint()、run()、按鍵處理的分派。public class MyRPGMIDlet extends MIDlet {
static MyRPGMIDlet instance;
MyRPGCanvas mainScreen = new MyRPGCanvas();
public MyRPGMIDlet() {
instance = this;
Display.getDisplay(this).setCurrent(mainScreen);
}
protected void startApp() { if (mainScreen != null) mainScreen.startGame(); }
protected void pauseApp() { if (mainScreen != null) mainScreen.pauseGame(); }
protected void destroyApp(boolean u) {
if (mainScreen != null) { mainScreen.destroyGame(); mainScreen = null; }
}
public static void quitApp() { instance.destroyApp(true); instance.notifyDestroyed(); instance = null; }
}
設計要點:
static instance 提供全域存取,方便 Canvas 內部呼叫 quitApp()。MyRPGCanvas,MIDlet 僅負責平台生命週期橋接。GameCanvas,最大化相容性。// 系統畫面
private final int LOGO_STATUS = 0, MENU_STATUS = 1, HELP_STATUS = 2, ABOUT_STATUS = 3;
// 遊戲畫面
private final int GAME_MAP1_STATUS = 4, GAME_WEAPONSHOP1_STATUS = 5, GAME_DIALOG1_STATUS = 6;
paint(Graphics g)protected void paint(Graphics g) {
clearScreen(g); // 若每幀背景圖不透明,可省略清屏提升效能
switch (status) {
case LOGO_STATUS: paintLogo(g); break;
case MENU_STATUS: paintMenu(g); break;
case HELP_STATUS: paintHelp(g); break;
case ABOUT_STATUS: paintAbout(g); break;
case GAME_MAP1_STATUS: paintGame_Map1(g); break;
case GAME_WEAPONSHOP1_STATUS: paintGame_WeaponShop1(g); break;
case GAME_DIALOG1_STATUS: paintDialog1(g); break;
}
}
每個 paintXxx 方法專注單一畫面繪製,複雜畫面可再拆分子方法。
run()public void run() {
try {
while (isRunning) {
long start = System.currentTimeMillis();
if (!isPaused) {
switch (status) {
case LOGO_STATUS: doLogo(); break;
case MENU_STATUS: doMenu(); break;
case HELP_STATUS: doHelp(); break;
case ABOUT_STATUS: doAbout(); break;
case GAME_MAP1_STATUS: doGame_Map1(); break;
case GAME_WEAPONSHOP1_STATUS: doGame_WeaponShop1(); break;
case GAME_DIALOG1_STATUS: doDialog1(); break;
}
}
repaint();
serviceRepaints(); // 強制立即繪製,降低延遲
long end = System.currentTimeMillis();
if (end - start < INTERVAL_TIME)
Thread.sleep(INTERVAL_TIME - (end - start));
}
} catch (Exception e) {}
}
INTERVAL_TIME = 100 → 目標 10 FPS,功能機 CPU 足夠。isPaused 實現「暫停不佔 CPU」,來電/切換背景時由 MIDlet 呼叫 pauseGame() 設定。serviceRepaints() 關鍵:MIDP 1.0 無雙緩衝,主動刷新減少畫面撕裂。init() 時一次載入所有圖片/音效,destroyGame() 統一釋放。直接在 keyPressed/Released 寫邏輯會導致「按鍵響應延遲」與「長按重複觸發」問題。本架構採用事件記錄 + 迴輪詢模式:
private int keyStates; // 每個 bit 代表一個邏輯鍵是否按下
// 邏輯鍵定義(位元遮罩)
private final int KEY_UP = 1, KEY_DOWN = 1 << 1, KEY_RIGHT = 1 << 2, KEY_LEFT = 1 << 3;
private final int KEY_FIRE = 1 << 4, KEY_LEFT_SOFT = 1 << 5, KEY_RIGHT_SOFT = 1 << 6, KEY_ZERO = 1 << 7;
private int convertKey(int keyCode) {
switch (keyCode) {
case -6: return KEY_LEFT_SOFT;
case -7: return KEY_RIGHT_SOFT;
case Canvas.KEY_NUM2: case -1: return KEY_UP;
case Canvas.KEY_NUM4: case -3: return KEY_LEFT;
case Canvas.KEY_NUM6: case -4: return KEY_RIGHT;
case Canvas.KEY_NUM8: case -2: return KEY_DOWN;
case Canvas.KEY_NUM0: return KEY_ZERO;
default: return 0;
}
}
不同廠商裝置只需覆寫 convertKey(),遊戲邏輯完全不變。
public void keyPressed(int keyCode) {
int key = convertKey(keyCode);
keyStates |= key; // 位元或:設定對應 bit 為 1
}
public void keyReleased(int keyCode) {
int key = convertKey(keyCode);
keyStates &= ~key; // 位元與非:清除對應 bit
}
private void clearKey() { keyStates = 0; } // 畫面切換時呼叫,避免殘留按鍵
// 在 doGame_Map1() 等邏輯方法中
if ((keyStates & KEY_UP) != 0) { /* 角色向上移動 */ }
if ((keyStates & KEY_FIRE) != 0) { /* 確認/攻擊 */ }
優勢: 按鍵檢測與邏輯幀解耦、支援多鍵同時按下(斜向移動)、長按自動重複、跨裝置移植只改映射表。
為壓縮 JAR 體積,全遊戲僅兩個類別(MIDlet + Canvas),所有邏輯以 private final void methodName() 封裝。雖非標準 OO 設計,但:
final 關鍵字,JIT/預驗證器易優化內聯本架構展示了在極度受限環境(MIDP 1.0、無雙緩衝、堆疊 < 200KB)下,如何以狀態機 + 位元組按鍵 + 精確幀控構建商業級 RPG 基礎。後續可擴展:
TiledLayer 實現大地圖捲動渲染完整原始碼已整理於文中,建議下載實測於 WTK 模擬器與實機,體驗功能機時代的工程智慧。