libGDXのAPIを使う前に、感覚をつかむ為に各モジュールに少し触れたとても簡単な"ゲーム"を作成しましょう。 詳細に入る前にいくつかのコンセプトを説明します。
今回は以下の内容について見ていきます:
プロジェクト設定 とGradleの手順に沿って作業を進めてください。各名前には以下を使用します:
出力先(destination)と Android SDKの項目も入力します。 iOSサブプロジェクトについては、実行するのにOS Xが必要なので今回はチェックを外してください。 今回の簡単なチュートリアルでは拡張(extensions )については触れないので、全ての拡張のチェックを外します。
統合開発環境へのインポートが済むと、プロジェクトやモジュールが計5つ出来上がります: メインプロジェクトである drop
、サブプロジェクトである android
(もしくはEclipse配下の drop-android
)と core
/ drop-core
と desktop
/ drop-desktop
と html
/ 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は、実際の画面解像度に関わらず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();
これで簡単なゲームを実行するのに必要なものが全て作成できました。
バケツと雨粒を表すものが足りません。 コード上でこれらを表すために必要なものについて考えて見ましょう:
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 ... }
あなたが Texture
やSpriteBatch
のような高度なクラスを主に使用している場合、この二行については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 に楽をさせるよう手助けをします。
SpriteBatch
はSpriteBatch.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
にまとめます。
また、ユーザー操作の幅を広げるために Screen
と Game
を使用することもできます; さらに学ぶには、このページに続きに当たる 次のチュートリアルを参照してください。
開発者ガイドの残りのページを読み、Git リポジトリのデモやテストを確認することを強く推奨します。それでは、楽しんでコーディングしてください。