初學者常將所有遊戲狀態 (主選單、幫助、遊戲中、暫停、遊戲結束) 的邏輯塞進單一 Canvas 的 keyPressed() 與 paint(),導致:
switch(state) 分支超過 2000 行。將每個遊戲狀態封裝為獨立類別,實作統一介面,委派給當前狀態物件處理輸入與繪製。
public interface GameState {
void onKeyPressed(int keyCode, StateManager sm);
void paint(Graphics g);
void onEnter(); // 可選:進入狀態初始化
void onExit(); // 可選:離開狀態清理
}
// 主選單狀態
public class MainMenuState implements GameState {
private static final String[] ITEMS = {"Start", "Help", "Exit"};
private int selectedIndex = 0;
private Image bgImage;
public MainMenuState() {
try { bgImage = Image.createImage("/res/menu_bg.png"); }
catch (IOException ignored) {}
}
public void onKeyPressed(int keyCode, StateManager sm) {
int action = getGameAction(keyCode);
if (action == Canvas.UP) {
selectedIndex = (selectedIndex - 1 + ITEMS.length) % ITEMS.length;
} else if (action == Canvas.DOWN) {
selectedIndex = (selectedIndex + 1) % ITEMS.length;
} else if (action == Canvas.FIRE) {
switch (selectedIndex) {
case 0: sm.setState(new PlayState()); break;
case 1: sm.setState(new HelpState()); break;
case 2: sm.quitGame(); break;
}
}
}
public void paint(Graphics g) {
if (bgImage != null) g.drawImage(bgImage, 0, 0, Graphics.TOP | Graphics.LEFT);
Font font = Font.getFont(Font.FACE_SYSTEM, Font.STYLE_BOLD, Font.SIZE_LARGE);
g.setFont(font);
for (int i = 0; i < ITEMS.length; i++) {
g.setColor(i == selectedIndex ? 0xFF0000 : 0xFFFFFF);
g.drawString(ITEMS[i], getWidth()/2, 100 + i * 40, Graphics.HCENTER | Graphics.TOP);
}
}
public void onEnter() {}
public void onExit() {}
}
// 幫助狀態
public class HelpState implements GameState {
private static final String[] LINES = {
"Use UP/DOWN to navigate",
"FIRE to select",
"Press any key to return"
};
public void onKeyPressed(int keyCode, StateManager sm) {
sm.setState(new MainMenuState()); // 任意鍵返回
}
public void paint(Graphics g) {
g.setColor(0x000000);
g.fillRect(0, 0, getWidth(), getHeight());
g.setColor(0xFFFFFF);
Font f = Font.getDefaultFont();
g.setFont(f);
for (int i = 0; i < LINES.length; i++) {
g.drawString(LINES[i], 10, 50 + i * 20, Graphics.LEFT | Graphics.TOP);
}
}
public void onEnter() {}
public void onExit() {}
}
// 遊戲進行狀態 (簡化)
public class PlayState implements GameState {
private Player player;
private Enemy[] enemies;
public PlayState() {
player = new Player(100, 100);
enemies = new Enemy[5];
for (int i = 0; i < enemies.length; i++) enemies[i] = new Enemy();
}
public void onKeyPressed(int keyCode, StateManager sm) {
int action = getGameAction(keyCode);
if (action == Canvas.LEFT) player.moveLeft();
else if (action == Canvas.RIGHT) player.moveRight();
else if (action == Canvas.UP) player.jump();
else if (action == Canvas.FIRE) player.shoot();
else if (keyCode == Canvas.KEY_STAR) sm.setState(new PauseState(this)); // 暫停
}
public void paint(Graphics g) {
// 繪製背景、玩家、敵人、UI...
g.setColor(0x87CEEB); g.fillRect(0, 0, getWidth(), getHeight()); // 天空
player.paint(g);
for (Enemy e : enemies) e.paint(g);
}
public void onEnter() { /* 載入關卡資源 */ }
public void onExit() { /* 釋放關卡資源 */ }
}
public class StateManager {
private GameState currentState;
private final Canvas canvas; // 回調畫布尺寸等
private final MIDlet midlet;
public StateManager(Canvas canvas, MIDlet midlet) {
this.canvas = canvas;
this.midlet = midlet;
setState(new MainMenuState()); // 初始狀態
}
public void setState(GameState newState) {
if (currentState != null) currentState.onExit();
currentState = newState;
currentState.onEnter();
canvas.repaint(); // 觸發首幀繪製
}
public void quitGame() {
midlet.destroyApp(false);
midlet.notifyDestroyed();
}
// 委派方法
public void onKeyPressed(int keyCode) {
if (currentState != null) currentState.onKeyPressed(keyCode, this);
}
public void paint(Graphics g) {
if (currentState != null) currentState.paint(g);
}
}
public class GameCanvas extends Canvas {
private StateManager sm;
public GameCanvas(MIDlet midlet) {
sm = new StateManager(this, midlet);
}
protected void keyPressed(int keyCode) {
sm.onKeyPressed(keyCode);
}
protected void paint(Graphics g) {
sm.paint(g);
}
}
public class StateManager {
private final Stack stateStack = new Stack<>();
public void pushState(GameState state) {
if (!stateStack.isEmpty()) stateStack.peek().onPause();
stateStack.push(state);
state.onEnter();
canvas.repaint();
}
public void popState() {
if (!stateStack.isEmpty()) {
stateStack.pop().onExit();
if (!stateStack.isEmpty()) {
stateStack.peek().onResume();
}
}
canvas.repaint();
}
public GameState current() {
return stateStack.isEmpty() ? null : stateStack.peek();
}
// 委派給 current()
}
適用場景:遊戲中按選單鍵 → pushState(new InGameMenuState()),選單繪製在遊戲畫面上方,選單關閉 → popState() 恢復遊戲。
| 指標 | 巨型 switch | State Pattern |
|---|---|---|
| 單一職責 | ❌ 違反 | ✅ 每狀態獨立類別 |
| 開閉原則 | ❌ 新增狀態改核心 | ✅ 新增類別無需改舊碼 |
| 可測試性 | ❌ 難以單元測試 | ✅ 狀態可獨立實例化測試 |
| 狀態轉換可視化 | ❌ 分散在 case 中 | ✅ 集中在 setState/pushState |
| 記憶體開銷 | 低 (單一物件) | 略高 (多物件),可用對象池優化 |