首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> Java 程式設計 >> Java 綜合教學

LibGDX Viewport 屏幕適配全解析:從原理到實戰

     編輯:Java 綜合教學

前言:為什麼需要 Viewport

遊戲開發中,邏輯世界坐標系統(固定寬高)與物理設備屏幕(千差萬別的分辨率、長寬比)之間存在天然矛盾。LibGDX 的 Viewport 抽象層統一了解決方案:定義虛擬世界尺寸,自動計算投影矩陣與視口矩形,實現跨設備一致的渲染表現。

核心 Viewport 類型對比

所有 Viewport 繼承自 Viewport 接口,核心方法 update(screenWidth, screenHeight, centerCamera) 根據屏幕尺寸重新計算世界投影。

Viewport縮放策略黑邊世界尺寸變化典型適用場景
StretchViewport非等比拉伸填滿屏幕無固定UI 佈局、簡單 2D、原型驗證
FitViewport等比縮放,保持長寬比,內容完整顯示有(單側或雙側)固定像素風遊戲、嚴格長寬比要求
FillViewport等比縮放,填滿屏幕,內容可能被裁剪無固定背景圖鋪滿、允許邊緣裁剪
ExtendViewport先 Fit,再向長邊延伸世界尺寸至屏幕邊緣可能(若設 maxWorldSize)動態擴展(min~max)策略遊戲、大地圖、希望大屏見更多
ScreenViewport世界尺寸=屏幕像素尺寸(1:1),無縮放無隨屏幕變化編輯器、工具類、桌面端像素精確操作
ScalingViewport通用封裝,接受任意 Scaling 枚舉視 Scaling 而定視 Scaling 而定自定義縮放策略組合

深度原理解析

ScreenViewport:像素級精確

public void update(int screenWidth, int screenHeight, boolean centerCamera) {
    setScreenBounds(0, 0, screenWidth, screenHeight);
    setWorldSize(screenWidth * unitsPerPixel, screenHeight * unitsPerPixel);
    apply(centerCamera);
}
  • 默認 unitsPerPixel = 1,即 1 像素 = 1 世界單位。
  • 大屏顯示更多內容,小屏內容被截斷(通常頂部優先消失)。
  • 適合圖形編輯器、CAD、桌面端工具類應用。

ExtendViewport:動態視野擴展

  • 構造參數:minWorldWidth, minWorldHeight, [maxWorldWidth, maxWorldHeight], camera。
  • 邏輯:先按 Scaling.fit 縮放至最小世界完整顯示,若屏幕更寬/高,則向右/上擴展世界尺寸,直至達到 maxWorldSize 或填滿屏幕。
  • 若設置 maxWorldSize 過小可能導致黑邊。

FitViewport vs FillViewport:取捨藝術

  • Fit:保證所有遊戲元素可見,犧牲屏幕利用率(黑邊)。
  • Fill:保證屏幕利用率 100%,犧牲邊緣內容完整性(裁剪)。
  • 選型原則:UI 關鍵元素居中佈局用 Fit;沉浸式背景、跑酷類用 Fill。

StretchViewport:暴力美學

  • 直接將世界矩陣拉伸到屏幕矩形,忽略長寬比。
  • 優點:代碼最簡單、無黑邊、無裁剪。
  • 缺點:圓變橢圓、正方形變矩形、UI 變形嚴重。
  • 適用:非像素風、長寬比不敏感、快速原型。

實戰代碼:多 Viewport 動態切換測試

public class ViewportTest extends ApplicationAdapter {
    private static final float MIN_W = 800, MIN_H = 600;
    private static final float MAX_W = 1280, MAX_H = 800;

    private OrthographicCamera camera;
    private SpriteBatch batch;
    private Texture bg, sprite;
    private BitmapFont font;
    private ObjectMap viewports;
    private int current = -1;

    @Override
    public void create() {
        camera = new OrthographicCamera();
        batch = new SpriteBatch();
        bg = new Texture("background.jpg");
        sprite = new Texture("badlogic.jpg");
        font = new BitmapFont();
        font.getData().setScale(2f);

        viewports = new ObjectMap<>();
        viewports.put("Stretch", new StretchViewport(MIN_W, MIN_H, camera));
        viewports.put("Fit", new FitViewport(MIN_W, MIN_H, camera));
        viewports.put("Fill", new FillViewport(MIN_W, MIN_H, camera));
        viewports.put("Screen", new ScreenViewport(camera));
        viewports.put("Extend (no max)", new ExtendViewport(MIN_W, MIN_H, camera));
        viewports.put("Extend (max)", new ExtendViewport(MIN_W, MIN_H, MAX_W, MAX_H, camera));

        selectNext();
    }

    private void selectNext() {
        current = (current + 1) % viewports.size;
        Viewport vp = viewports.values().get(current);
        vp.apply(true);
        vp.update(Gdx.graphics.getWidth(), Gdx.graphics.getHeight(), true);
        log(vp);
    }

    @Override
    public void resize(int w, int h) {
        viewports.values().get(current).update(w, h, true);
    }

    @Override
    public void render() {
        Gdx.gl.glClearColor(0.39f, 0.58f, 0.92f, 1f);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

        Viewport vp = viewports.values().get(current);
        batch.setProjectionMatrix(camera.combined);
        batch.begin();
        batch.draw(bg, 0, 0, vp.getWorldWidth(), vp.getWorldHeight());
        batch.draw(sprite, 0, 0);
        font.draw(batch, viewports.keys().get(current), 10, 400);
        batch.end();
    }

    @Override
    public boolean touchDown(int x, int y, int pointer, int button) {
        selectNext();
        return true;
    }

    private void log(Viewport vp) {
        Gdx.app.log("VP", "World: " + vp.getWorldWidth() + "x" + vp.getWorldHeight()
            + " Screen: " + vp.getScreenWidth() + "x" + vp.getScreenHeight()
            + " Cam: (" + camera.position.x + "," + camera.position.y + ")");
    }
}

關鍵 API 速查

方法說明
viewport.update(w, h, true)屏幕尺寸變化時必調,centerCamera=true 保持居中
viewport.apply(true)應用投影矩陣到相機,通常在切換 Viewport 後調用
viewport.getWorldWidth/Height()獲取當前邏輯世界尺寸(繪圖座標系依據)
viewport.getScreenX/Y/W/H()獲取視口在物理屏幕上的矩形區域(含黑邊)
camera.unproject(vec.set(x, y, 0))屏幕觸控座標 → 世界座標(必須配合正確 Viewport)
viewport.setUnitsPerPixel(f)ScreenViewport 專用,調整像素密度

常見坑與最佳實踐

  1. 座標轉換錯誤:觸摸事件 screenX/screenY 必須經 camera.unproject() 轉世界座標,且相機必須已 viewport.apply()。
  2. resize 重複調用:LibGDX 啟動時會觸發 resize,create() 中手動 update 會導致雙次計算,建議統一在 resize 處理。
  3. UI 佈局策略:使用 Scene2D.ui 的 Stage 並傳入 Viewport,自動適配;或手動按 getWorldWidth() 百分比佈局。
  4. 橫豎屏切換:AndroidManifest.xml 配置 configChanges="orientation|screenSize",在 resize 重新計算 Viewport。
  5. 高 DPI 處理:Gdx.graphics.getDensity() 獲取密度,ScreenViewport 可配合 setUnitsPerPixel(density) 實現物理尺寸一致。

總結選型決策樹

需要像素級精確(編輯器/工具) → ScreenViewport
├─ 允許變形、最省事 → StretchViewport
├─ 必須保持長寬比:
│   ├─ 所有內容必須可見(UI 重、棋類、像素風) → FitViewport
│   ├─ 背景沉浸、邊緣可犧牲(跑酷、射擊) → FillViewport
│   └─ 大屏希望看到更多世界(策略、SLG、大地圖) → ExtendViewport
└─ 自定義組合縮放邏輯 → ScalingViewport

掌握 Viewport 本質是「世界座標系 → 屏幕座標系」的可控映射。實戰中建議:遊戲邏輯層只認世界座標,渲染層交給 Viewport,輸入層用 unproject 回世界座標,三層解耦,輕松應對千奇百怪的設備碎片化。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved