サイトのトップへ戻る

libGDX ドキュメント 日本語訳

サイト内検索

簡単なゲームを作る

libGDXのAPIを使う前に、感覚をつかむ為に各モジュールに少し触れたとても簡単な"ゲーム"を作成しましょう。 詳細に入る前にいくつかのコンセプトを説明します。

今回は以下の内容について見ていきます:

  • 基本的なファイルアクセス
  • 画面をクリアする
  • 画像を描写する
  • カメラを使用する
  • 基本的な入力処理
  • 効果音を再生する


プロジェクト設定

プロジェクト設定 とGradleの手順に沿って作業を進めてください。各名前には以下を使用します:

  • アプリケーション名: drop
  • パッケージ名: com.badlogic.drop
  • ゲームクラス: Drop

出力先(destination)と Android SDKの項目も入力します。 iOSサブプロジェクトについては、実行するのにOS Xが必要なので今回はチェックを外してください。 今回の簡単なチュートリアルでは拡張(extensions )については触れないので、全ての拡張のチェックを外します。

統合開発環境へのインポートが済むと、プロジェクトやモジュールが計5つ出来上がります: メインプロジェクトである drop、サブプロジェクトである android (もしくはEclipse配下の drop-android)と core / drop-coredesktop / drop-desktophtml / drop-htmlです。

ゲームの起動やデバッグを行うには、プロジェクト設定 とGradleのページであなた使っている統合開発環境について記載された項目を参照してください。 インポートしたプロジェクトをそのまま実行すると、スタートアップappによって生成された既定'ゲーム'が起動します: 赤い背景に BadLogic のゲーム画像が表示されます。 あまり面白い画面ではありませんので、そのうち修正する予定です。



ゲーム

ゲームのアイディアはとてもシンプルなものです:

  • バケツで雨粒をキャッチする。
  • 画面の下部にバケツを置く
  • 画面の上部にランダムに雨粒を発生させ、下方向へ加速させます。
  • プレイヤーはバケツをマウス/タッチで水平方向にドラッグするか、左右のカーソルキーを使って動かします。
  • このゲームに終わりません。禅体験のようなものと考えてください :)


ゲーム素材

ゲームの見栄えを幾分か良くするには、いくつか画像と効果音が必要です。 グラフィックのために、対象解像度を 800x480 ピクセル (Androidでの横画面モード)に定義する必要があります。 ゲームを動作させる端末がその解像度で無かった場合は、画面に収まるよう単純にゲーム画面の伸縮をします。 注意: 人目を引くようなゲームにするために、異なる画面密度ごとに異なるゲーム素材を用意したいかもしれません。 これについては話が大きくなってしまうので、今回は説明しません。

雨粒とバケツは画面よりも小さくする必要があるので、64x64 ピクセルのサイズにします。

以下のサイトからゲーム素材を取得します:

ゲーム素材をゲームで使用できるようにするには、それらをAndroidのassetsフォルダに配置します。 4つのファイルにそれぞれ drop.wav、 rain.mp3、 droplet.png 、bucket.png と名前をつけて android/assets/内に配置します。 デスクトップ用プロジェクトとHTML5用プロジェクトは、やり方は違いますが両方ともこのフォルダーを参照するようになっているので、ゲーム素材は一箇所に保存するだけで問題ありません。 その後、使用している統合開発環境ごとにプロジェクトツリーをリフレッシュして新規ファイルを作成する必要があります(Eclipseの場合、右クリック -> リフレッシュ)。そうしないと、 'file not found' の実行時エラーが発生する可能性があります。



起動クラスを設定する

任意の設定値を指定することで、起動クラスの設定を変えることができます。では デスクトップ用プロジェクトから始めましょう。 desktop/src/… (もしくは Eclipse配下のdrop-desktop)にあるDesktopLauncher.java クラスを開いてください。 800x480サイズのウィンドウでタイトルに"Drop"と設定します。コードは以下のようになります:

package com.badlogic.drop.desktop;

import com.badlogic.gdx.backends.lwjgl.LwjglApplication;
import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration;
import com.badlogic.drop.Drop;

public class DesktopLauncher {
   public static void main (String[] arg) {
      LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
      config.title = "Drop";
      config.width = 800;
      config.height = 480;
      new LwjglApplication(new Drop(), config);
   }
}

Android プロジェクトに移動して、アプリケーションを横画面モードで起動します。 この場合、android (もしくは drop-android) ルートディレクトリ内にあるAndroidManifest.xmlを編集する必要があります。 編集すると以下のようになります:

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.badlogic.drop.android"
    android:versionCode="1"
    android:versionName="1.0" >

    <uses-sdk android:minSdkVersion="8" android:targetSdkVersion="20" />

    <application
        android:allowBackup="true"
        android:icon="@drawable/ic_launcher"
        android:label="@string/app_name"
        android:theme="@style/GdxTheme" >
        <activity
            android:name="com.badlogic.drop.android.AndroidLauncher"
            android:label="@string/app_name"
            android:screenOrientation="landscape"
            android:configChanges="keyboard|keyboardHidden|orientation|screenSize">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />
                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>

我々に代わって既にセットアップツールが正しい値を入力しており、 android:screenOrientation には "landscape"が設定されています。 このゲームを縦画面モードで実行したい場合は、その属性に"portrait"と設定します。

また、バッテリーを節約したいので加速度センサーとコンパスを無効にします。 これはandroid/src/… (もしくは drop-android)内のAndroidLauncher.javaファイルで行います。内容は以下のようになります:

package com.badlogic.drop.android;

import android.os.Bundle;

import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
import com.badlogic.drop.Drop;

public class AndroidLauncher extends AndroidApplication {
   @Override
   protected void onCreate(Bundle savedInstanceState) {
      super.onCreate(savedInstanceState);
      AndroidApplicationConfiguration config= new AndroidApplicationConfiguration();
      config.useAccelerometer = false;
      config.useCompass = false;
      initialize(new Drop(), config);
   }
}

Activityの解像度はAndroidOSが設定するので、我々は定義できません。 先ほど定義したように、どのような端末の解像度に対しても800x480の画面サイズが収まるよう合わせるだけです。

最後に、HTML5 プロジェクトでも 800x480サイズの描写エリアを使用するようにします。 この場合、 html/src/… (もしくは drop-html)内にあるHtmlLauncher.javaファイルを編集します。:

package com.badlogic.drop.client;

import com.badlogic.gdx.ApplicationListener;
import com.badlogic.gdx.backends.gwt.GwtApplication;
import com.badlogic.gdx.backends.gwt.GwtApplicationConfiguration;
import com.badlogic.drop.Drop;

public class HtmlLauncher extends GwtApplication {
   @Override
   public GwtApplicationConfiguration getConfig () {
      return new GwtApplicationConfiguration(800, 480);
   }

   @Override
   public ApplicationListener getApplicationListener () {
      return new Drop();
   }
}

これで全てのプラットフォーム用の起動クラスが正しく設定できたので、この素晴らしいゲームの実装へ移りましょう。



コード

今回はコードをいくつかの項目に分割します。 便宜上、core/src/… (もしくは Eclipse内のdrop-core)にあるコアプロジェクトのDrop.javaファイルの中身はすべてそのままにします。



ゲーム素材を読み込む

最初にやることは、ゲーム素材を読み込んでそれらへの参照を保持することです。 ゲーム素材は通常 ApplicationAdapter.create() メソッド内で読み込みます。では、やってみましょう:

package com.badlogic.drop;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.Texture;

public class Drop extends ApplicationAdapter {
   private Texture dropImage;
   private Texture bucketImage;
   private Sound dropSound;
   private Music rainMusic;

   @Override
   public void create() {
      // load the images for the droplet and the bucket, 64x64 pixels each
      dropImage = new Texture(Gdx.files.internal("droplet.png"));
      bucketImage = new Texture(Gdx.files.internal("bucket.png"));

      // load the drop sound effect and the rain background "music"
      dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
      rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));

      // start the playback of the background music immediately
      rainMusic.setLooping(true);
      rainMusic.play();

      ... more to come ...
   }

   // rest of class omitted for clarity

各ゲーム素材について Drop クラス内でフィールドを定義しているので、後から参照できます。 create()メソッド内の最初の二行では、雨粒とバケツの画像を読み込んでいます。 Texture はビデオRAM内に保存された読み込み画像を表します。 通常はTextureに対して描写を行うことはできません。Textureのコンストラクタの引数にゲーム素材ファイルへの FileHandleを渡すことで、Textureの読み込みができます。 こうした FileHandle インスタンスは、Gdx.filesが提供するメソッドのいずれかを使って取得します。 ファイルには様々なタイプがあり、今回はassetsフォルダを参照する"internal"タイプのファイルを使用します。 internalタイプのファイルはAndroidプロジェクトの assets ディレクトリに置かれています。前にも説明したように、デスクトップ用プロジェクトと HTML5 用プロジェクトは同一のディレクトリを参照しています。

次に効果音とBGMを読み込みます。 Libgdx は効果音と音楽を区別します。効果音はメモリ内に保存され、音楽は保存されている場所からストリーミング再生されます。 音楽は通常サイズが大きすぎてメモリ内に全て保持することができないため、このように区別しています。 経験則として、音源が10秒以内の場合は Sound インスタンスを使用して、それより長い場合はMusic インスタンスを使用すると良いでしょう。

Sound インスタンスと Music インスタンスの読み込みは、Gdx.audio.newSound()Gdx.audio.newMusic()を使って行います。 Textureの時と同じように、 これらメソッドには両方とも FileHandleを設定します。

create() メソッドの最後で、Music インスタンスにループ再生の設定をして即座に再生を開始しています。 アプリケーションを実行すると、ピンクの背景が表示されて雨音が聞こえるでしょう。



Camera と SpriteBatch

次は camera と SpriteBatchの作成に入ります。 cameraは、実際の画面解像度に関わらず800x480ピクセルの解像度を使って描写をできるようにするために使用します。 SpriteBatchは、前回読み込んだTextureのように、2D画像を描写するために使用される特別なクラスです。

クラスにcameraとbatchという名前の二つのフィールドを追加します:

   private OrthographicCamera camera;
   private SpriteBatch batch;

create() メソッド内では、最初に以下のようにして camera を作成します:

   camera = new OrthographicCamera();
   camera.setToOrtho(false, 800, 480);

これで、cameraは常にゲーム内世界の中で800x480単位の範囲を表示します。これはゲーム内世界を見るための仮想ウィンドウと考えてください。 現在、我々は作業を簡単にするために一定数のピクセルを一つの単位として見ています。 しかしこれは、他の単位(例えばメートルでも何でもいいです)を使ってもかまいません。 Cameraは非常に強力で、チュートリアルではカバーしきれないほど多くのことができるようになります。詳細情報については、開発者ガイドの残りのページを確認してください。

次は SpriteBatchを作成します (これもcreate() メソッド内での作業になります):

   batch = new SpriteBatch();

これで簡単なゲームを実行するのに必要なものが全て作成できました。



バケツを追加する

バケツと雨粒を表すものが足りません。 コード上でこれらを表すために必要なものについて考えて見ましょう:

  • バケツと雨粒は 800x480 単位の画面内で (x,y)座標の情報を持っている。
  • バケツと雨粒は画面内で表示するための横幅と高さの情報を持っている。
  • バケツと雨粒を視覚的に表現するためのグラフィック。これは既に Texture インスタンスの形式で読み込んでいます。

そのため、バケツと雨粒を描写するには位置情報とサイズ情報を保持する必要があります。 LibgdxにはRectangleクラスがあり、これを使って目的を達成することができます。 ではバケツを表す Rectangle を作成してみましょう。新規フィールドを追加します:

   private Rectangle bucket;

create()メソッド内でRectangleのインスタンスを作成して初期値を設定します。 画面下部より20ピクセル上、横中央位置にバケツを配置します。

   bucket = new Rectangle();
   bucket.x = 800 / 2 - 64 / 2;
   bucket.y = 20;
   bucket.width = 64;
   bucket.height = 64;

これで画面下部より20ピクセル上、横中央位置にバケツを配置しました。ちょっと待ってください。なぜ bucket.yに20を設定したのでしょう。 480 - 20ではないのでしょうか? 実は、libgdx(と OpenGL)の既定では全ての描写は、y軸の下から上の方向に向かって行われるのです。 バケツの位置を(x,y)座標を指定した場合、バケツの左下隅がその(x,y)座標の位置に来ます。 また、画面の左下が描写の開始点(0,0座標)となります。 rectangleの横幅と高さには、対象とする解像度の高さよりも小さい 64x64を設定します。

注意: この設定を変更し(関連議論)、y軸を下向きにして画面の左上を開始点にすることは可能です。 OpenGL と cameraクラスは柔軟性が高いので、非常に多くのビューアングルから好きなものを使用できます。 今回はとりあえず設定はそのままで作業を続けます。



バケツを描写する

バケツを描写する時が来ました。最初にするのは画面をダークブルー色でクリアすることです。 render() メソッドを以下のように変更するだけです:

   @Override
   public void render() {
      Gdx.gl.glClearColor(0, 0, 0.2f, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

      ... more to come here ...
   }

あなたが TextureSpriteBatchのような高度なクラスを主に使用している場合、この二行についてはOpenGL 関連の命令ということだけ知っていればいいです。 最初の呼び出しではクリア色に青を設定しています。引数には、色に関する赤、緑、青、透明度の情報をそれぞれ0-1の範囲内で設定します。 次の呼び出してはOpenGLに指示をして実際に画面をクリアしています。

次に、 camera に updateを行うよう指示する必要があります。Cameraはマトリクスと呼ばれる数学的 entity を使用します。これは描写用の座標系を司ります。 これらのマトリクスは、カメラの位置などの情報が変更される度に再計算される必要があります。 今回の簡単な例ではそこまでしませんが、一般的にはフレームごとにcameraをupdateすることをお勧めします:

   camera.update();

以下でバケツを描写できます:

   batch.setProjectionMatrix(camera.combined);
   batch.begin();
   batch.draw(bucketImage, bucket.x, bucket.y);
   batch.end();

最初の行では、cameraで指定された座標系を使うようSpriteBatchに指示します。 先ほど述べたとおり、この処理はマトリクスと呼ばれるものを使って行われます、もう少し具体的に言うとプロジェクションマトリクスです。 camera.combinedフィールドがこのマトリクスです。 これで、SpriteBatchは前述の座標系内に存在する全てのものを描写するようになります。

次に、SpriteBatch に新しい batchを開始するよう指示します。なぜこれが必要なのでしょうか? batchとは一体何なのでしょうか? OpenGL では画像に対して個別に指示を出すのは良くありません。 可能な限り多くの画像を一度に描写するよう指示を出すのが好まれます。

SpriteBatch クラスは OpenGL に楽をさせるよう手助けをします。 SpriteBatchSpriteBatch.begin()SpriteBatch.end()の間に行われた全ての描写命令を記録します。 SpriteBatch.end()を実行すると貯まった全ての描写リクエストを一度に処理し、描写速度がかなり向上します。 最初は面倒に思えるかもしれませんが、毎秒20フレームで100個のスプライトを描写する場合と毎秒60フレームで500個のスプライトを描写する場合とでパフォーマンスの違いがでてきます。



バケツを移動させる (タッチ/マウス操作)

ユーザーがバケツを操作できるようにしましょう。先ほど述べたように、ユーザーがバケツをドラッグできるようにします。 それでは、簡単に実装してみましょう。 ユーザーが画面をタッチした(もしくはマウスボタンを押した)場合、バケツをタッチ位置の中心まで持っていきます。 これを実装するためにrender()メソッドの最後尾に以下のコードを追加します:

   if(Gdx.input.isTouched()) {
      Vector3 touchPos = new Vector3();
      touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
      camera.unproject(touchPos);
      bucket.x = touchPos.x - 64 / 2;
   }

まず最初にGdx.input.isTouched()を呼び出し、現在画面がタッチされているか(もしくはマウスボタンが押されているか)をinputモジュールに問い合わせます。 次に、タッチ座標/マウス座標をcamera内での座標に変換します。 タッチ座標/マウス座標の座標系がゲーム内世界でオブジェクトを表すために使われている座標系と違う場合があるため、これが必要です。

Gdx.input.getX()Gdx.input.getY() は現在のタッチ位置/マウス位置を戻り値として返します(libgdx ではマルチタッチもサポートしていますが、それについては別記事で説明します)。 これらの座標を cameraの座標系に変換するためには、camera.unproject() メソッドを呼び出す必要があります。 camera.unproject()には三次元ベクトルのVector3を引数として渡します。 そうしたvectorを作成し、現在のタッチ座標/マウス座標を設定して、このメソッドを呼び出します。 こうしてこのvectorには、バケツが存在する座標系におけるタッチ座標/マウス座標が格納されました。 最後にバケツの位置をタッチ座標/マウス座標の中心に変更します。

注意: Vector3インスタンスのようなオブジェクトを毎回新規作成するのは、非常に非常によろしくありません。 ガベージコレクタがこうした短命オブジェクトの回収を頻繁に行わなければならなくなるためです。 デスクトップの場合は大したことないですが、Androidの場合はGCが最大で数百ミリ秒の一時停止を引き起こし、結果遅延が起きてしまう可能性があります。 特定の状況においてこの問題を解決するために、毎回インスタンスを作成するのではなく、DropクラスにtouchPosのフィールドを作成することができます。

注意 #2: touchPos は三次元ベクトルです。2Dしか使わないのに、なぜ三次元ベクトルを使うのか不思議かもしれません。 実はOrthographicCamera は3D カメラで、Z座標も認識しています。CAD アプリケーションについて考えてみてください。 CAD アプリケーションでも同様に3D orthographicカメラを使用していますが、我々はそれを2Dグラフィックを描写するのにしか使っていませんよね。



バケツを動かす(キーボード操作)

デスクトップとブラウザの場合、キーボード入力を受け取ることもできます。では、左カーソルキーや右カーソルキーが押された時にバケツが移動するようにしてみましょう。

バケツの移動には加速度を付けず、毎秒200ピクセル/単位ほど左か右に移動させます。 こうした時間ベースの動きを実装するには、直前の描写フレームから現在の描写フレームまでの間にどれだけ時間が経過したかを知る必要があります。 以下のようにすれば全てできます:

   if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime();
   if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();

Gdx.input.isKeyPressed()メソッドは特定のキーが押されたかどうかを通知します。 Keysにはlibgdxがサポートしている全てのキーコードが格納されています。 Gdx.graphics.getDeltaTime()メソッドは直前のフレームから現在のフレームまでの間に経過した秒単位時間を戻り値として返します。 バケツのX座標から経過時間(秒単位)x200の値を増減させて、座標を変更するだけで良いのです。

また、バケツが画面の範囲にないに収まるようにする必要があります:

   if(bucket.x < 0) bucket.x = 0;
   if(bucket.x > 800 - 64) bucket.x = 800 - 64;


雨粒を追加する

雨粒の場合は、Rectangle インスタンスのリストを用意して、雨粒ごと位置とサイズを保持します。では、フィールドとしてリストを追加しましょう:

   private Array<Rectangle> raindrops;

Array クラスは、 ArrayListのような標準のJavaコレクションに代わって使用するlibgdx のユーティリティクラスです。 ArrayListには、様々な過程で不要データを生み出すという問題があります。 Array クラスでは、不要データの発生をできるだけ最小限に抑えています。 またLibgdx では、他にもガベージコレクタを意識した hash-mapやsetのようなコレクションが用意されています。

また、最後に雨粒が発生した時間を記録しておく必要があるので、もう一つフィールドを追加します:

   private long lastDropTime;

ナノ秒単位で時間を保存するので、long型を使用しています。

雨粒の作成を簡単にするために、 spawnRaindrop()という名前のメソッドを記述します。 このメソッドではRectangleのインスタンスを新規に作成し、画面上部のランダムな位置にそれを設置し、raindrops配列に追加します。

   private void spawnRaindrop() {
      Rectangle raindrop = new Rectangle();
      raindrop.x = MathUtils.random(0, 800-64);
      raindrop.y = 480;
      raindrop.width = 64;
      raindrop.height = 64;
      raindrops.add(raindrop);
      lastDropTime = TimeUtils.nanoTime();
   }

このメソッドについては、何をやっているのか一目瞭然でしょう。 MathUtils クラスとは、様々な数学関連の静的メソッドが使用できる、libgdxのクラスです。 今回の場合は、0から800-64の間の値をランダムに戻り値として返します。 TimeUtils クラスとは、様々な時間関連の基本的な静的メソッドが使用できる、libgdxのクラスです。 今回の場合は、現在の時間をナノ秒単位で保存しています。後で、この値に基づいて雨粒を新しく作成するかどうかを判断します。

create() メソッド内で、雨粒インスタンスの配列を作成して最初の雨粒を作成します。:

create() メソッド内でarrayのインスタンスを作成する必要があります:

   raindrops = new Array<Rectangle>();
   spawnRaindrop();

次に、最後に雨粒を新規作成してからどれだけ時間が経過しているかを確認して必要であれば雨粒を新規作成する処理をrender()メソッドに追加します:

   if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();

また雨粒を落下させる必要もあります。では、手っ取り早く毎秒200 ピクセル/単位の速度を維持して落下するようにしましょう。 雨粒が画面最下部よりも下まで落ちた場合は、それをarrayから削除します。

   Iterator<Rectangle> iter = raindrops.iterator();
   while(iter.hasNext()) {
      Rectangle raindrop = iter.next();
      raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
      if(raindrop.y + 64 < 0) iter.remove();
   }

雨粒の画像を描写する必要があります。 SpriteBatch の描写処理に以下のように追加します:

   batch.begin();
   batch.draw(bucketImage, bucket.x, bucket.y);
   for(Rectangle raindrop: raindrops) {
      batch.draw(dropImage, raindrop.x, raindrop.y);
   }
   batch.end();

最終調整: 雨粒がバケツに当たった場合、落下音を再生して雨粒をarrayから削除します。 これは雨粒の更新用ループに以下を追加するだけでできます:

      if(raindrop.overlaps(bucket)) {
         dropSound.play();
         iter.remove();
      }

Rectangle.overlaps() メソッドは、このrectangle が他のrectangleと重なっているかを確認します。 今回は、落下の効果音を再生して雨粒をarrayから削除するよう処理をしています。



破棄

ユーザーはいつでもアプリケーションを終了できます。 この単純な例の場合は破棄の処理を行う必要はないでしょう。 しかし一般的には、OSに配慮して自分が作った不要データを破棄するのは良い心がけでしょう。

Disposable インタフェースを実装してdispose()メソッドを持つ libgdx のクラスは、不要になったら手動で破棄する必要があります。 今回の例で言えば、Textureとsoundとmusicと SpriteBatchがそれに該当します。 良いプログラマーは以下のように ApplicationAdapter.dispose()メソッドを上書きします:

   @Override
   public void dispose() {
      dropImage.dispose();
      bucketImage.dispose();
      dropSound.dispose();
      rainMusic.dispose();
      batch.dispose();
   }

リソースを破棄したら、それには一切アクセスしないでください。

通常、破棄できるのはJavaガーベッジコレクターで制御されないネイディブリースです。 このため、それらを手動で破棄する必要があるのです。 Libgdxは様々な方法でゲーム素材管理の手助けをします。 知りたいのであれば開発者ガイドの残りページを読んでください。



一時停止処理/再開処理を制御する

Android では、ユーザーが着信を受けたりホームボタンを押す度にアプリケーションの一時停止と再開が発生するので注意が必要です。 例えば、消えてしまった画像の再読み込み (本当に酷い話、OpenGL コンテキストの消失のことです)やストリーム再生している音楽の一時停止・再開など、 Libgdxはあなたに代わって多くのことを自動的にやってくれます。

今回のゲームでは、一時停止/再開の制御は必要ありません。 ユーザーがアプリケーションに戻ってくると、すぐにゲームが再開されます。 普通は一時停止画面を実装して、画面をタッチしてゲームを再開するようユーザーに求めるでしょう。 この処理については、あなたへの宿題としてここでは説明しません - ApplicationAdapter.pause() メソッドと ApplicationAdapter.resume() メソッドを確認してください。



ソースコードの全容

以下が、今回作成した簡単なゲームの短いソースコードです:

package com.badlogic.drop;

import java.util.Iterator;

import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.TimeUtils;

public class Drop extends ApplicationAdapter {
   private Texture dropImage;
   private Texture bucketImage;
   private Sound dropSound;
   private Music rainMusic;
   private SpriteBatch batch;
   private OrthographicCamera camera;
   private Rectangle bucket;
   private Array<Rectangle> raindrops;
   private long lastDropTime;

   @Override
   public void create() {
      // load the images for the droplet and the bucket, 64x64 pixels each
      dropImage = new Texture(Gdx.files.internal("droplet.png"));
      bucketImage = new Texture(Gdx.files.internal("bucket.png"));

      // load the drop sound effect and the rain background "music"
      dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
      rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));

      // start the playback of the background music immediately
      rainMusic.setLooping(true);
      rainMusic.play();

      // create the camera and the SpriteBatch
      camera = new OrthographicCamera();
      camera.setToOrtho(false, 800, 480);
      batch = new SpriteBatch();

      // create a Rectangle to logically represent the bucket
      bucket = new Rectangle();
      bucket.x = 800 / 2 - 64 / 2; // center the bucket horizontally
      bucket.y = 20; // bottom left corner of the bucket is 20 pixels above the bottom screen edge
      bucket.width = 64;
      bucket.height = 64;

      // create the raindrops array and spawn the first raindrop
      raindrops = new Array<Rectangle>();
      spawnRaindrop();
   }

   private void spawnRaindrop() {
      Rectangle raindrop = new Rectangle();
      raindrop.x = MathUtils.random(0, 800-64);
      raindrop.y = 480;
      raindrop.width = 64;
      raindrop.height = 64;
      raindrops.add(raindrop);
      lastDropTime = TimeUtils.nanoTime();
   }

   @Override
   public void render() {
      // clear the screen with a dark blue color. The
      // arguments to glClearColor are the red, green
      // blue and alpha component in the range [0,1]
      // of the color to be used to clear the screen.
      Gdx.gl.glClearColor(0, 0, 0.2f, 1);
      Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);

      // tell the camera to update its matrices.
      camera.update();

      // tell the SpriteBatch to render in the
      // coordinate system specified by the camera.
      batch.setProjectionMatrix(camera.combined);

      // begin a new batch and draw the bucket and
      // all drops
      batch.begin();
      batch.draw(bucketImage, bucket.x, bucket.y);
      for(Rectangle raindrop: raindrops) {
         batch.draw(dropImage, raindrop.x, raindrop.y);
      }
      batch.end();

      // process user input
      if(Gdx.input.isTouched()) {
         Vector3 touchPos = new Vector3();
         touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
         camera.unproject(touchPos);
         bucket.x = touchPos.x - 64 / 2;
      }
      if(Gdx.input.isKeyPressed(Keys.LEFT)) bucket.x -= 200 * Gdx.graphics.getDeltaTime();
      if(Gdx.input.isKeyPressed(Keys.RIGHT)) bucket.x += 200 * Gdx.graphics.getDeltaTime();

      // make sure the bucket stays within the screen bounds
      if(bucket.x < 0) bucket.x = 0;
      if(bucket.x > 800 - 64) bucket.x = 800 - 64;

      // check if we need to create a new raindrop
      if(TimeUtils.nanoTime() - lastDropTime > 1000000000) spawnRaindrop();

      // move the raindrops, remove any that are beneath the bottom edge of
      // the screen or that hit the bucket. In the later case we play back
      // a sound effect as well.
      Iterator<Rectangle> iter = raindrops.iterator();
      while(iter.hasNext()) {
         Rectangle raindrop = iter.next();
         raindrop.y -= 200 * Gdx.graphics.getDeltaTime();
         if(raindrop.y + 64 < 0) iter.remove();
         if(raindrop.overlaps(bucket)) {
            dropSound.play();
            iter.remove();
         }
      }
   }

   @Override
   public void dispose() {
      // dispose of all the native resources
      dropImage.dispose();
      bucketImage.dispose();
      dropSound.dispose();
      rainMusic.dispose();
      batch.dispose();
   }
}


この後何をすれば良いのか

今回のものは、libgdxを使って最小限のゲームを作成する方法の非常に基本的な例です。 雨粒を削除する度に破棄する必要があるRectangleをメモリ管理を使って全てリサイクルする、といったように改善できる部分がいくつかあります。 また、OpenGL は一つのbatch の中で非常に多くの画像を持つような状況は苦手です (今回の場合は画像は二つしかないので問題ありません)。 通常そういった画像は全て、 TextureAtlasとして知られる一つのTextureにまとめます。 また、ユーザー操作の幅を広げるために ScreenGameを使用することもできます; さらに学ぶには、このページに続きに当たる 次のチュートリアルを参照してください。

開発者ガイドの残りのページを読み、Git リポジトリのデモやテストを確認することを強く推奨します。それでは、楽しんでコーディングしてください。