許多 J2ME 遊戲開發者在 Canvas 繼承類中實現 Runnable,使用如下迴圈:
public void run() {
while (true) {
gameLogic();
repaint();
Thread.sleep(500); // 以為固定 0.5 秒一幀
}
}
問題: gameLogic() 與 paint() 執行耗時不固定(受 GC、邏輯複雜度影響),實際幀間隔 = 執行時間 + 500ms,導致畫面卡頓、速度忽快忽慢,破壞遊戲體驗。
目標:維持固定幀率(如 20 FPS → 50ms/幀)。每幀記錄開始時間,執行邏輯與繪圖後,計算耗時,剩餘時間才 sleep。
public void run() {
final int RUN_RATE = 50; // 目標幀間隔 ms (20 FPS)
long time0 = System.currentTimeMillis();
while (true) {
long time1 = System.currentTimeMillis();
long elapsed = time1 - time0;
time0 = time1;
gameLogic(); // 遊戲邏輯
repaint(); // 觸發 paint()
time1 = System.currentTimeMillis();
long logicPaintTime = time1 - time0; // 實際耗時
long sleepTime = RUN_RATE - logicPaintTime;
if (sleepTime > 0) {
try {
Thread.sleep(sleepTime);
} catch (InterruptedException e) {
// 處理中斷或忽略
}
}
// 若 sleepTime <= 0 表示邏輯超時,立即進入下一幀(或記錄警告)
}
}
time0 = time1 放在邏輯執行前,確保下一幀計算基準正確。sleepTime > 0 才睡眠,避免負值導致異常或忙等待。Thread.sleep 精度受 OS 排程影響(Windows ~10-15ms),高精度需求可考慮 System.nanoTime() + 忙等待混合策略。Timer / TimerTask 或內部類實現定時器,語義更清晰,但同樣需處理執行超時情況。物理模擬、網路同步常採用 固定時間步長:累積幀時間,以固定 dt 多次更新邏輯,渲染採用插值。此為專業遊戲引擎標準架構(參考 Gaffer on Games "Fix Your Timestep")。
Thread.sleep(固定值) 是新手陷阱。專業遊戲迴圈必須 測量實際耗時、動態補償睡眠,才能保證幀率穩定、遊戲邏輯確定性。