libGDX Scene2d の基礎

概要

libGDX で 2D UI Widget (Button, Label, CheckBox, etc.) を使用するための基礎となる Stage, Viewport, Layout についてまとめます。 (ゲームライブラリとしてではなく、マルチプラットフォームGUI ライブラリとしての使用を模索中です。)

確認環境

  • OS X El Capitan (10.11.6)
  • Xcode 8.0
  • Android Studio 2.2.2
    • Multi-OS Engine Plugin 1.2.1
    • Kotlin 1.0.4
    • buildToolVersion 25.0.0
    • compileSdkVersion 24
  • libGDX 1.9.5-SNAPSHOT

参考情報

解説

Scene2d は基本的な 2D scene graph の機能を提供する。 Scene2d により以下が提供される。

  • アクター (actor)
    • 2D scene graph のノード。位置 (position)、サイズ (rectangular size)、原点 (origin)、スケール (scale)、回転 (rotation)、色 (color) を持つ。
  • グループ (group)
    • アクターのグループ。
  • 描画 (drawing)
    • 回転 (rotation)、スケール (scale) を考慮した描画を行う。
  • 当たり判定 (hit detection)
    • 回転 (rotation)、スケール (scale) を考慮した当たり判定を行う。
  • イベント (event)
    • 各アクターに入力イベントを分配する。
  • アクション (action)
    • 各アクターを時間に応じて変化させる。

Scene2d を基礎とした UI Widget が用意されている。 UI Widget 関連のクラスを図に示す。

Stage

Stage は 複数の Actor を子に持ち、Actor の root 要素になる。 Stage は InputProcessor であり、入力イベントを各 Actor に分配する。 Viewport により Stage を Screen に投影する方法を指定する。

基本的な使い方は以下のとおり。

class MyGdxGame : ApplicationAdapter() {

    private val stage by lazy {
        Stage()
    }

    override fun create() {
        Gdx.input.inputProcessor = stage
        Gdx.graphics.isContinuousRendering = false
        stage.addActor(Image(Texture("badlogic.jpg")))
    }

    override fun resize(width: Int, height: Int) {
        stage.viewport.update(width, height, true)
    }

    override fun render() {
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
        stage.act(Gdx.graphics.deltaTime)
        stage.draw()
    }

    override fun dispose() {
        stage.dispose()
    }
}

stage プロパティは var lateinit stage でも良いが、stage プロパティを変更不可としたいため val stage by lazy を使用している。 by lazy を使用することにより、最初の get 呼び出しでインスタンスが生成されるようになる。

Viewport

Viewport により Stage を Screen に投影する方法を指定する。 Viewport のクラス階層を以下に示す。

各 Viewport における、Screen サイズ (単位はピクセル) と Stage サイズ (単位はユニット) の関係を以下に示す。 Stage を構成する単位 (Stage の論理的な画素) を 1 ユニットとする。

  • ScreenViewport
    • Stage サイズ = Screen サイズ × 1 ピクセル当たりのユニット (unitsPerPixel)
    • デフォルトでは、1 ユニット = 1 ピクセル (unitsPerPixcel = 1)。
  • ScalingViewport
    • Stage サイズを指定し、Screen に合わせて Scale (拡大/縮小) して表示する。Stage サイズは指定したサイズ。
    • Scale 方法は以下のとおり。
      • none : 1 ユニット = 1 ピクセルとする。
      • fit : 縦横一方を Stage = Screen となるように、一方を Stage < Screen となるように、Stage のアスペクト比を変えずに拡大/縮小する。
      • fill : 縦横一方を Stage = Screen となるように、一方を Stage > Screen となるように、Stage のアスペクト比を変えずに拡大/縮小する。
      • stretch : 縦横共に Stage = Screen となるように、Stage のアスペクト比を変えて拡大/縮小する。
  • FitViewport
    • ScalingViewport の fit と同じ。
  • FillViewport
    • ScalingViewport の fill と同じ。
  • StretchViewport
    • ScalingViewport の stretch と同じ。
  • ExtendViewport
    • Stage サイズを指定し、Screen に合わせて拡大/縮小して表示する。拡大/縮小に合わせて Stage サイズを変更する。
    • FitViewport と同様に拡大/縮小した後に、Stage < Screen となっている辺の Stage サイズを変更する。
      • 最大サイズが指定されていない場合は、Stage = Screen となるように Stage サイズを変更する。
      • 最大サイズが指定されている場合は、指定した最大サイズに Stage サイズを変更する。

言葉では分かりづらいため図示すると以下のようになる。 赤は Screen サイズ (単位はピクセル)。 黒は Stage サイズ (単位はユニット)。

ScreenViewport (unitsPerPixcel = 1) ScreenViewport (unitsPerPixcel = 2)

ScalingViewport (none, 640, 480) ScalingViewport (fit, 640, 480)
ScalingViewport (fill, 640, 480) ScalingViewport (stretch, 640, 480)

ExtendViewport (640, 480) ExtendViewport (640, 480, 800, 480) ExtendViewport (640, 480, 800, 640)

表示例を得るために以下のソースコードを使用した。

class MyGdxGame : ApplicationAdapter() {

    companion object {
        private val TAG = MyGdxGame::class.java.name
    }

    private val stage by lazy {
        Stage(ScreenViewport())
        /*
        Stage(ScreenViewport().apply {
            unitsPerPixel = 2f
        })
        */
        //Stage(ScalingViewport(Scaling.none, 640f, 480f))
        //Stage(ScalingViewport(Scaling.fit, 640f, 480f))
        //Stage(FitViewport(640f, 480f))
        //Stage(ScalingViewport(Scaling.fill, 640f, 480f))
        //Stage(FillViewport(640f, 480f))
        //Stage(ScalingViewport(Scaling.stretch, 640f, 480f))
        //Stage(StretchViewport(640f, 480f))
        //Stage(ExtendViewport(640f, 480f))
        //Stage(ExtendViewport(640f, 480f, 800f, 480f))
        //Stage(ExtendViewport(640f, 480f, 800f, 640f))
    }

    override fun create() {
        Gdx.app.logLevel = Application.LOG_DEBUG
        Gdx.app.debug(TAG, "create")
        Gdx.graphics.isContinuousRendering = false
        stage.addActor(object : Actor() {
            override fun draw(batch: Batch, parentAlpha: Float) {
                batch.end()
                ShapeRenderer().run {
                    projectionMatrix = batch.projectionMatrix
                    begin(ShapeRenderer.ShapeType.Filled)
                    color = Color.RED
                    rect(0f, 0f, stage.width, stage.height)
                    Gdx.app.debug(TAG, "${stage.width} ${stage.height}")
                    end()
                }
                batch.begin()
            }
        })
        stage.addActor(Image(Texture("badlogic.jpg")))
    }

    override fun resize(width: Int, height: Int) {
        Gdx.app.debug(TAG, "resize $width $height")
        stage.viewport.update(width, height, true)
    }

    override fun render() {
        Gdx.app.debug(TAG, "render")
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT)
        stage.draw()
    }

    override fun dispose() {
        Gdx.app.debug(TAG, "dispose")
        stage.dispose()
    }
}

レイアウト

UI Widget は自身のサイズと位置を決定しない。 親の UI Widget が子の UI Widget のサイズと位置を決定する。 UI Widget は親がサイズと位置を決定するためのヒントとなる最小サイズ、推奨サイズ、最大サイズを提供する。

UI Widget が描画 (draw) されるとき、最初に validate メソッドが呼ばれる。 UI Widget のレイアウトが invalid のとき、描画の前に layout メソッドが呼ばれる。 描画においては、layout によりキャッシュされたレイアウト情報 (サイズと位置) を用いる。 invalidate, invalidateHierarchy メソッドを呼ぶことで、レイアウトを invalid にできる。

UI Widget の状態が変化してキャッシュされたレイアウト情報を再計算する場合には invalidate メソッドを呼ぶ。 但し、最小サイズ、推奨サイズ、最大サイズに影響がない場合に限る。 サイズが変化せず、親 Widget が影響を受けない場合に使用する。

UI Widget の状態が変化して最小サイズ、推奨サイズ、最大サイズに影響が及ぶ場合には invalidateHierarcy メソッドを呼ぶ。 親 Widget のレイアウトに影響が及ぶため、root までの全ての親 Widgetinvalidate メソッドが呼ばれる。