ざる魂

真似ぶ魂、学ぶの本質。知られざる我が魂

libGDX入門 その04 画面遷移

はじめに

libGDX を勉強するついでに解説記事を書く シリーズ 4回目です。

前回 は、以下のことを学びました。

  • ログ出力
  • スプライト表示のアニメーション
  • テクスチャラップ
  • バーチャルパッド

今回は、簡単なゲームを作ったので、その中の画面遷移を解説します。

ライフサイクルについて

今まであえて説明を避けてきたのですが(面倒そうなので←)、 Screenインタフェイスを扱うにあたり避けられそうもなかったので調査しました。

libGDXのライフサイクルと呼びだしメソッドの関係は以下のようになっています。

/img/libgdx-beginner/4/life-cycle.png
libgdxのライフサイクル

メソッドは、 ApplicationListener のものです。

注意して欲しいのは、PC版とAndroidで微妙に挙動が違うということです。 特にPC版は、待機状態のときもrenderメソッドが呼ばれ続けるので注意が必要です。 私はMacを持っていないのでわかりませんが、iOSも考慮したら更に違いがあるかもしれません。

create() アプリケーションが新しく生成された時に呼ばれます。
resize() アプリケーションが新しく起動されたとき、PC版でウィンドウサイズが変化したとき、スマホ版で、端末の向きが変わったときなどに呼ばれます。
pause() スマホで電話が鳴った時やHome画面にした時、PC版でフォーカスが外れたときなどに呼ばれます。
resume() 待機状態から、ウィンドウがアクティブになった時に呼ばれます。
dispose() アプリケーションが破棄される時に呼ばれます。

ライフサイクルに関する情報は下記が非常に詳しいです。一読をお勧めします。

上記サイトの説明にもありますが、ゲームの情報を保存するときは、 pause() が良いようです。

公式ドキュメントは下記にあります。

画面遷移

今回作成したゲームでは、次のような遷移があります。

/img/libgdx-beginner/4/screen.png
今回の画面遷移

まず「メインメニュー」が表示され、 「START」をタップすると「ゲーム」画面になり、 「QUIT」をタップすると「メインメニュー」画面に戻ります。 非常にシンプルですね。

その1でも紹介しましたが、関連するクラス図を再掲します。

/img/libgdx-beginner/4/class.png
Screenのクラス図

ApplicationAdapter クラスは、 ApplicationListener インタフェイスを空実装したデフォルトクラスでしたね。 ScreenAdapter クラスも同様に、 Screen インタフェイスを空実装したデフォルトクラスです。

今までは、 ApplicationAdapter クラスを継承していましたが、 今回からは、 Game クラスを継承します。

Game クラスは、 ApplicationListener を実装したクラスで、画面遷移を担当する Screen インタフェイスの インスタンスを保持します。今まで ApplicationAdapter を継承していたクラスはこのGameクラスを継承します。

Screenインタフェイスについては、ライフサイクルを確認したいため、 MyScreenAdapter クラスという デフォルト実装クラスを用意し、各メソッドでログを出力することにします。

public abstract class MyScreenAdapter implements Screen {
      :
    @Override
    public void show () {
        Gdx.app.log(LOG_TAG, "show");
    }
      :
}

今回のゲームで実装するクラスは以下のとおりです。

/img/libgdx-beginner/4/screen2.png
今回準備するクラスたち

LibGdxSample クラスは、処理の起点となるクラスです。 この図からは省いていますが、Screen系のクラスは先程紹介した MyScreenAdapter クラスを継承します。 MainManuScreen クラスは、最初に表示されるメニュー(タイトル)画面です。 GameScreen クラスは、実際のゲームを担当するクラスです。ほとんどの処理はこのクラスに集中します。

Screenインタフェイス

画面遷移がある場合は、画面ごとに Screen インタフェイスを実装します。 実装すべきメソッドのほとんどは ApplicationListener と共通です。 ライフサイクルについても、 ApplicationListener とほぼ同じです。 Screen では、 create() メソッドがなくなり、 変わりに show() メソッドと hide() メソッドが追加されました。 ただし、 dispose() メソッドは、名前は同じでも扱いが少し違うので注意が必要です(後述します)。 ここでは、下記3つのメソッドについて解説します。

  • show()
  • hide()
  • dispose()

show()とhide()

show() メソッドは、画面切り替え開始時に1度だけ呼ばれます。 hide() メソッドは、画面切り替え終了時に1度だけ呼ばれます。 画面切り替え時とは、 Game#setScreen() メソッド呼び出した時のことです。

例えば現在「メニュー画面」を表示していた場合、「ゲーム画面」に切り替えたくなったら、 下記のようなコードを実行します。

game.setScreen(new GameScreen(this));

この setScreen() メソッドの中で、 現在表示中の「メニュー画面」の MainMenu#hide() メソッドが呼ばれ、 次に表示予定の「ゲーム画面」の GameScreen#show() メソッドが呼ばれます。

Game#setScreen() のソースコードです。短かいので全部載せておきます。

    public void setScreen (Screen screen) {
        if (this.screen != null) this.screen.hide();
        this.screen = screen;
        if (this.screen != null) {
            this.screen.show();
            this.screen.resize(Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
        }
    }

setScreen() した時は、hide,show,resizeが即実行されるということを 頭に叩きこんでおいたほうが良さそうですね。

  1. 現在の画面の Screen#hide()
  2. 次の画面の Screen#show()
  3. 次の画面の Screen#resize()

以上から、 show() メソッドは初期化処理、 hide() メソッドは終了処理を実装すれば良いと思います。

ただし、 hide() メソッドには、リソース解放処理を記述してはいけません。 リソース解放処理とは、 Texture#dipose() や、 SpriteBatch#dispose() などのことです。 理由は後述します。

dispose()

dispose() メソッドには、注意すべき重要なことがあります。 それは何かというと、 Screen#dispose() はシステムから自動的に呼びだされないということです。 ApplicationListener#dispose() とは扱いが違いますね。

APIマニュアルにも記述されています。

Screens are not disposed automatically. 
You must handle whether you want to keep 
screens around or dispose of them when another screen is set.

ゲームによって、リソースの解放タイミングは様々なので、 このようにクライアントまかせになっているのかもしれません。 (そうすると、Screenインタフェイスを使わずに全部自分で作ったほうがいいんじゃないかという気もしてきますが。)

ということで、 dispose() は自分で呼びだす必要があります。

AplicationLisner 利用時は、特になにも考えずに ApplicationListener#dispose() にリソース解放処理を記述しておけばよかったのですが、 Screen を利用する場合は、 解放のタイミングを自分で制御しなくてはなりません。

dispose()の方法

サンプルやドキュメントを呼んでもいまいちこの辺の方法がわかりません。 とはいえ何もしないわけにもいきませんので、 現時点での解放方法について、私なりのやりかたを考えました。 他にも良い方法あるよ?って方がいたら教えてください。

その1 hide() で解放する

例えば、下記のように Screen#render() メソッドから setScreen() メソッドを呼びだすとします。

public void render (float deltaTime) {
       : 
    game.setScreen(new MainMenuScreen(game));
       : 
    leftButton.draw(batch);
       : 
}

このとき、現在実行中の Screen インスタンスの hide() メソッドが呼ばれます。 hide() メソッドは下記のようにテクスチャの開放処理を実装していたとします。

public void hide() {
    img.dispose();
}

結論をいうとこの書き方ではうまくいきません。 なぜなら、 hide() でテクスチャを解放したにもかかわらず、 その後に、テクスチャの描画処理を呼んでしまっているからです。

hide() にリソースの解放処理を入れるならば、 redner() メソッドを下記のように 書かなければなりません。

public void render (float deltaTime) {
       : 
       : 
    leftButton.draw(batch);
       : 
    game.setScreen(new MainMenuScreen(game));
    // ここには何も処理を書かない。
}

つまり、 setScreen()render() の一番最後に持ってこなければなりません。 しかしながら、最後に呼びだすなどのルールは忘れやすそうですし、 強制性がないので個人的にはお勧めしません。

その2 Game#dispose() で全て解放する

扱うリソースが少ないシンプルなゲームの場合に利用できる方法です。

Game#create() でそのゲームの全リソースをロードし、 Game#dispose() で全リソースを解放します。 Screen クラスでは、リソース管理を一切しません。 (スクリーンからこれらのリソースにアクセスするには、 Game のインスンタンス経由でアクセスします)。

こうすれば、 Screen 毎にリソースの管理を気にせずにすむので楽ですね。

その3 Game クラスを拡張する

今回採用した方法です。その1の方法を改良しました。

Gameクラスを継承したクラスを少し改造します。

public class LibGdxSample extends Game {
    private Screen nextScreen;
        :
    @Override
    public void render() {
        super.render();
        if (nextScreen != null) {
            super.setScreen(nextScreen);
            nextScreen = null;
        }
    }
        :
    @Override
    public void setScreen (Screen screen) {
        Gdx.app.log(LOG_TAG, "setScreen");
        nextScreen = screen;
    }
        :
}

まず、 nextScreen というフィールドを新設します。 そして setScreen メソッドをオーバライドし、 nextScreen フィールドに screen をセットするだけの処理にします。 更に render() メソッドの最後で、 nextScreen フィールドがセットされたときのみ setScreen() を呼びだすようにします。

このような仕組みを持つことで、 game.setScreen() をいつでも呼びだすことが可能になります。 なぜなら、 Screen#hide() の呼びだされるタイミングが必ず render() の一番最後になるからです。

以上で、 Screen#hide() の実行されるタイミングに気を使うことなく、 dispose() 処理を実装できるようになりました。

後は次のように、 hide() から dispose() 処理を呼びだせば良いですね。

    @Override
    public void hide() {
        dispose();
    }

    @Override
    public void dispose() {
        music.dispose();
        seGet.dispose();
        seMiss.dispose();
        batch.dispose();
        font.dispose();
        img.dispose();
        bgImg.dispose();
        shapeRenderer.dispose();
    }

最後に注意というか知っていたほうが良い知識として、 Screen#hide() は、 Game#dispose() からも自動で呼びだされるということを述べておきます。

public abstract class Game implements ApplicationListener {
     :
	public void dispose () {
		if (screen != null) screen.hide();
	}
     :
}

なので、 Game#dispose() 内でわざわざ自分で Screen#dispose() を呼びだす必要はありません。

    public void dispose() {
        super.dispose();
        Screen screen = getScreen();
        if (screen != null) screen.dispose();
    }

ゲームっぽい何か

さて、長い説明が終わってやっとゲームの説明です(といっても非常に単純なものですが)。 内容は、上から落ちてくる魚を取るだけのゲームです。

仕様は、

  • 最初にメニュー画面を表示。「START」タップでゲーム開始
  • 魚をキャッチすると、1点
  • 3回取り損ねると、ゲームオーバー
  • ポーズあり
  • ポーズ中に「QUIT」タップでメニュー画面へ戻る

といった感じです。

画像データは下記のツールを利用して作成しました。

効果音は下記のサイトで作成しました。

BGMは下記のサイトで作成しました。

プログラムの骨組みは、前回のものを踏襲しています。

/img/libgdx-beginner/4/001.png
タイトル画面
/img/libgdx-beginner/4/002.png
ゲーム画面

ゲームのプログラムは、ほとんどが今まで説明してきたlibGDXの内容でつくられてます。 なので詳しく説明しませんが、一点だけ解説するとすれば、当たり判定のところでしょうか。 魚と猫の当り判定のコードは以下のようになっています。

if (nekoBounds.overlaps(fishBounds)) {
    resetFish();
    seGet.play();
    score += 1;
}

nekoBoudnsfishBounds は、 Rectangle クラスで、それぞれキャラクタの矩形情報です。 Rectangle#overlaps() メソッドを呼ぶことで、2つの矩形が重なりあっているかを判定できます。

ソースコードについて

今回は、ソースコードが4ファイルになってしまったため、githubに載せることにしました。

思いのほかコード量が多くなってしまったのが反省点です。 いろいろ工夫の余地はあると思うので、今後改善していきたいと思います。

アセットの管理について

今回実装したゲームのアセットファイルは数えるほどしかありませんので、 画像やサウンドの読み込みは、単純に読み込むだけでした。

ゲームの種類によっては、沢山のアセットを駆使しなければならない場合もあるでしょう。 そんなときは、libGDXの AssetManager クラスを使うのが良さそうです。

非同期読み込みや依存管理、キャッシュの管理など便利な機能を多く実装しているようですね。 Screen 利用時の解放処理の煩雑さも、このクラスを利用すれば解決するのかもしれません。

ゲーム中のフォントについて

ゲーム中のフォントは、gebsite様の「Fantasy Gezone」フォントを利用させていただきました。 ありがとうございます。

おわりに

次回 予定している内容は、

  • ゲームデータの保存

です。