在 MIDP 2.0 LayerManager 架構中,**View Window** 定義了「世界座標系」中哪一矩形區域會被渲染到屏幕上。通過移動這個窗口,即可實現大場景在小屏幕上的平滑滾動,無需移動每個 Sprite/Layer 的絕對座標。
世界座標系 (大場景,如 500x500 像素) +---------------------------+ | | | [View Window 140x140] | ← 當前可見區域 | (scnX, scnY) | | | +---------------------------+ 屏幕座標系 (手機屏幕,如 176x208) +-----------+ | View | ← LayerManager.paint(g, screenX, screenY) | Window | 將視口內容繪製到屏幕 (screenX, screenY) | Content | +-----------+
scnX++ → 視口在世界中右移 → 屏幕顯示內容左移scnX-- → 視口左移 → 內容右移scnY
public class ScrollingGameCanvas extends GameCanvas implements Runnable {
private boolean running = false;
private int frameDelay = 20;
private int scrW, scrH;
private int viewW = 140, viewH = 140; // 視口大小
private int scnX = 55, scnY = 20; // 視口世界座標左上角
private Image bgImage;
private Sprite bgSprite;
private LayerManager layerManager;
public ScrollingGameCanvas() throws IOException {
super(true);
scrW = getWidth();
scrH = getHeight();
// 載入背景圖 (需大於視口尺寸才能滾動)
bgImage = Image.createImage("/background.png"); // 如 300x200
bgSprite = new Sprite(bgImage);
layerManager = new LayerManager();
layerManager.append(bgSprite); // 背景放最底層
}
public void start() {
running = true;
new Thread(this).start();
}
public void stop() { running = false; }
public void run() {
Graphics g = getGraphics();
while (running) {
handleInput();
renderFrame(g);
flushGraphics();
try { Thread.sleep(frameDelay); } catch (InterruptedException ignored) {}
}
}
private void handleInput() {
int keys = getKeyStates();
// 左移:視口 X 減小,邊界檢查
if ((keys & LEFT_PRESSED) != 0) {
if (scnX > 0) scnX--;
}
// 右移:視口 X 增加,不超過背景寬度 - 視口寬度
if ((keys & RIGHT_PRESSED) != 0) {
if (scnX + viewW < bgImage.getWidth()) scnX++;
}
// 上/下 同理
if ((keys & UP_PRESSED) != 0) {
if (scnY > 0) scnY--;
}
if ((keys & DOWN_PRESSED) != 0) {
if (scnY + viewH < bgImage.getHeight()) scnY++;
}
}
private void renderFrame(Graphics g) {
// 清屏
g.setColor(0xFFFFFF);
g.fillRect(0, 0, scrW, scrH);
// 關鍵:設置視口世界座標與尺寸
layerManager.setViewWindow(scnX, scnY, viewW, viewH);
// 繪製到屏幕 (20,20) 偏移處,留邊距給 UI
layerManager.paint(g, 20, 20);
}
}
public class ScrollingMIDlet extends MIDlet {
private Display display;
public void startApp() {
if (display == null) {
display = Display.getDisplay(this);
try {
ScrollingGameCanvas canvas = new ScrollingGameCanvas();
canvas.start();
display.setCurrent(canvas);
} catch (IOException e) {
e.printStackTrace();
// 顯示錯誤 Alert
}
}
}
public void pauseApp() {}
public void destroyApp(boolean u) { notifyDestroyed(); }
}
LayerManager.setViewWindow(int x, int y, int width, int height)scnX, scnY)paint() 僅繪製各 Layer 與該矩形相交的部分LayerManager.paint(Graphics g, int x, int y)g 的 (x, y) 處 (屏幕座標)
// 目標視口中心 = 主角世界座標
int targetX = player.getX() - viewW / 2;
int targetY = player.getY() - viewH / 2;
// 邊界鉗制
targetX = Math.max(0, Math.min(targetX, worldWidth - viewW));
targetY = Math.max(0, Math.min(targetY, worldHeight - viewH));
// 線性插值 (lerp) 實現平滑跟隨
scnX += (targetX - scnX) * 0.2f; // 0.2 = 跟隨係數
scnY += (targetY - scnY) * 0.2f;
// 遠景層 (移動慢)
farLayer.setPosition(scnX * 0.2f, scnY * 0.2f);
// 中景層
midLayer.setPosition(scnX * 0.5f, scnY * 0.5f);
// 近景層 (跟隨視口)
nearLayer.setPosition(scnX, scnY);
TiledLayer.setAnimatedTile() 實現水面、火焰等動畫瓦片scnX ∈ [0, worldW - viewW]paint() 會裁剪或異常scnX, scnY 用基本類型,LayerManager 復用drawImage 而非 LayerManagerlayerManager.remove() 遠離視口的實體,動態增減LayerManager 的 View Window 機制將「世界座標」與「屏幕座標」解耦:遊戲邏輯在無限大的世界座標系中運算,渲染層只需移動一個矩形窗口即可實現任意方向、任意速度的平滑滾動。掌握 setViewWindow(scnX, scnY, w, h) + 邊界鉗制 + 插值跟隨,即可構建從簡單背景滾動到複雜視差多層、瓦片大世界的完整滾動系統。