libGDX ではアプリケーションのライフサイクルを六つ定義している。
- create
- render
- resize
- pause
- resume
- dispose
このライフサイクルにしたがってアプリケーションを作るためのインターフェースが ApplicationListener である。
ApplicationListener
libGDX では、ApplicationListener を実装したクラスが必要になる。各プラットフォームで用意されたスタータークラスからこのインターフェースを実装したクラスが呼ばれるためだ。このクラスはlibGDXゲームの核になる。
package com.badlogic.drop;
import android.os.Bundle;
import com.badlogic.gdx.backends.android.AndroidApplication;
import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration;
public class MainActivity extends AndroidApplication {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
AndroidApplicationConfiguration cfg = new AndroidApplicationConfiguration();
cfg.useGL20 = true;
cfg.useAccelerometer = false;
cfg.useCompass = false;
initialize(new Drop(), cfg);
}
}
Android アプリケーションの場合、AndroidApplication を実装したサブクラスから initialize により ApplicationListener を実装したクラスを見つける。AndroidApplication を実装したサブクラスは libGDX のセットアップツールを使えば自動的に生成される。
サンプルアプリケーションの一部では ApplicationListener を実装したクラスを用意しそこにすべての処理を記述しているが、他のデモプログラムで Game クラスのサブクラスを利用する場合もよく見受けられる。 Gameクラスは、 ApplicationListener を実装したクラスで、マルチスクリーン(メニュー画面、ゲームオーバー画面など複数画面持つ)ゲームでの利用に適している。
create
このメソッドは、ゲームの初期設定をするために呼ばれる。ゲームで使用する画像、BGM、効果音などのリソースをロードしたり、スプライトを用意したりなどする。
render
もっとも重要なメソッドはrenderだろう。renderはゲームのメインループを表現するもので、アプリから常に呼び出される。ゲームロジックの更新はここで行う必要がある。
後は、wikiに記載のとおり。
ゲームは、スタート画面、メニュー画面、マップ画面など複数の画面で構成されることが多い。libGDX では各画面をスクリーンとして用意し、スクリーンを切り替えることで複数の画面を扱えるようにすることができる。gdx-invadersを参考にSimpleAppをマルチスクリーン対応するにはどうすればいいか考えてみた。
gdx-invadersは、メインクラスで、ApplicationListener を直接実装せずに、Gameクラスで拡張をしている。Gameクラスは ApplicationListener を実装する抽象クラスで、Screenクラスを扱うためのセッター、ゲッター、nullチェックが組み込まれている。つまり、SimpleAppのメインクラス Dropで、ApplicationListenerを実装せずに、Gameで拡張すればマルチスクリーン対応ができることが分かる。
package com.badlogic.drop;
import com.badlogic.gdx.Game;
import com.badlogic.gdx.Gdx;
public class Drop extends Game {
@Override
public void create() {
setScreen(new MainMenuScreen());
}
@Override
public void render() {
DropScreen currentScreen = (DropScreen) getScreen();
currentScreen.render(Gdx.graphics.getDeltaTime());
if (currentScreen.isDone()) {
currentScreen.dispose();
if (currentScreen instanceof MainMenuScreen) {
setScreen(new GameLoopScreen());
}
}
}
@Override
public void dispose() {
}
@Override
public void resize(int width, int height) {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
}
Drop クラスは、単にスクリーンを切り替えるための処理だけを書くように修正できる。setScreen/getScreen は現在有効なスクリーンを設定/取得する。MainMenuScreen はメインメニューを扱うスクリーンで、 GameLoopScreen は、SimpleApp の元々の画面を扱うためのスクリーンだ。 currentScreen の render メソッドは、 基本的に ApplicationListener の render をそっくり置き換えて実行できるものと考えていいだろう。currentScreen は、DropScreen のインスタンス。 DropScreen はScreenを実装している。Screen の構造は ApplicationListener と基本的に同一なので、DropScreen やそれを継承したクラスは、ApplicationListener を実装したクラスのように利用できる。
DropScreen
package com.badlogic.drop;
import com.badlogic.gdx.Screen;
public abstract class DropScreen implements Screen {
/** Screen 終了フラグ **/
private boolean isDone = false;
/** ロジック更新 **/
public abstract void update (float delta);
/** グラフィックス更新 **/
public abstract void draw (float delta);
/** Screen 終了フラグ取得 **/
public boolean isDone (){
return isDone;
}
/** Screen 終了フラグ設定 **/
public void setDone(boolean done){
isDone = done;
}
@Override
public void render(float delta) {
update(delta);
draw(delta);
}
@Override
public void resize(int width, int height) {
}
@Override
public void show() {
}
@Override
public void hide() {
}
@Override
public void pause() {
}
@Override
public void resume() {
}
@Override
public void dispose() {
}
}
処理が分かりやすくなるので、gdx-invaders に従って、 render を update と draw に分割するようにしてみた。
MainMenuScreen
package com.badlogic.drop;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
public class MainMenuScreen extends DropScreen {
private final SpriteBatch spriteBatch;
private final BitmapFont font;
OrthographicCamera camera;
private final String text = "Touch screen to start!";
public MainMenuScreen() {
camera = new OrthographicCamera();
camera.setToOrtho(false, 800, 480);
spriteBatch = new SpriteBatch();
font = new BitmapFont(Gdx.files.internal("font16.fnt"), Gdx.files.internal("font16.png"), false);
}
@Override
public void update(float delta) {
if (Gdx.input.isTouched()) {
setDone(true);
}
}
@Override
public void draw(float delta) {
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
camera.update();
spriteBatch.setProjectionMatrix(camera.combined);
spriteBatch.begin();
float width = font.getBounds(text).width;
font.draw(spriteBatch, text, (800 / 2) - width / 2, 128);
spriteBatch.end();
}
@Override
public void dispose() {
spriteBatch.dispose();
font.dispose();
}
}
GameLoopScreen
package com.badlogic.drop;
import java.util.Iterator;
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.GL10;
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 GameLoopScreen extends DropScreen {
Texture dropImage;
Texture bucketImage;
Sound dropSound;
Music rainMusic;
SpriteBatch batch;
OrthographicCamera camera;
Rectangle bucket;
Array<Rectangle> raindrops;
long lastDropTime;
public GameLoopScreen() {
// load the images for the droplet and the bucket, 48x48 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 - 48 / 2; // center the bucket horizontally
bucket.y = 20; // bottom left corner of the bucket is 20 pixels above
// the bottom screen edge
bucket.width = 48;
bucket.height = 48;
// 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 - 48);
raindrop.y = 480;
raindrop.width = 48;
raindrop.height = 48;
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}
@Override
public void update(float delta) {
// 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 - 48 / 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 - 48) bucket.x = 800 - 48;
// 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 + 48 < 0) iter.remove();
if(raindrop.overlaps(bucket)) {
dropSound.play();
iter.remove();
}
}
}
@Override
public void draw(float delta) {
// 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(GL10.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();
}
@Override
public void dispose() {
dropImage.dispose();
bucketImage.dispose();
dropSound.dispose();
rainMusic.dispose();
batch.dispose();
}
}
libGDX ではBitmapFontクラスを利用して任意のフォントで文字表示をおこなうことができる。確認できたBitmapFontクラスのインスタンスを生成する方法は二つ、FreeTypeFontGeneratorクラスから生成する方法、BitmapFontコンストラクタで生成する方法だ。違いは、動的か静的か。
FreeTypeFontGeneratorクラスから生成する方法
String font = "タッチしてゲームスト";
String message = "タッチしてゲームスタート";
FreeTypeFontGenerator generator = new FreeTypeFontGenerator( Gdx.files.internal("mikachan.ttf"));
BitmapFont bitmapFont = generator.generateFont(30, font, false);
generator.dispose();
generateFont の第2引数に与える文字列が message ではなくて、font であることに注意。generateFontはフォントを生成するためのものなので、文字列中に重複する文字があってはならない。フォントを用意できたら、下記のように目的の文字列を描写できる。
spriteBatch.begin();
float width = bitmapFont.getBounds(message).width;
bitmapFont.draw(spriteBatch, message, (800 / 2) - width / 2, 128);
spriteBatch.end();
文字が重複しない文字列を自動的に生成して、generateFont を呼びたい場合は、SimpleFontGeneratorのようなクラスを自前で用意する必要がある。
public class SimpleFontGenerator {
private final FileHandle fileHandle;
public SimpleFontGenerator(FileHandle fileHandle) {
this.fileHandle = fileHandle;
}
public BitmapFont generateFont(int size, String characters, boolean flip) {
if (characters.length() == 0) {
return null;
}
String fonts = String.valueOf(characters.charAt(0));
for (int i = 0; i < characters.length(); i++) {
for (int j = 0; j < fonts.length(); j++) {
if (fonts.charAt(j) == characters.charAt(i)) {
break;
}
if (j + 1 == fonts.length()) {
fonts = fonts.concat(String.valueOf(characters.charAt(i)));
break;
}
}
}
FreeTypeFontGenerator generator = new FreeTypeFontGenerator(fileHandle);
BitmapFont font = generator.generateFont(size, fonts, false);
generator.dispose();
return font;
}
}
恐らくバグだと思うが、特定のフォントサイズを generateFont に与えた場合、フォントと画像のマッピングがずれたような問題が発生するらしく、メッセージの描写がうまくいかないことがあった。もしこの問題に遭遇したらワンサイズ大きさを変えることで解決できだろう。
BitmapFontコンストラクタで生成する方法
この方法は、事前にAngelCode由来のBMFontフォーマットフォントデータを用意する必要がある。このフォーマットでフォントデータを作るツールは幾つかある。
Bitmap Font Generator はAngelCode純正で機能が充実しているが、Windowsでしか利用できない。TWL Theme Editor はフォントデータ作成専用ではないが、Windows以外のプラットフォームでも利用できる。hiero.jarもマルチプラットフォームで利用可能で、libGDXプロジェクトで提供されているツールだ。
hiero.jarは意図した文字を用意できないことがあったので、Windows 以外は TWL Theme Editor を使うほうがいいかもしれない。TWL Theme Editor でフォントデータを作る場合は、メニューから Tools > Create Font の順序で実行できる。
用意できたら、
bitmapFont = new BitmapFont(Gdx.files.internal("jpn.fnt"), Gdx.files.internal("jpn_00.png"), false);
でBitmapFontインスタンスを生成できる。
ゲーム上に表示する物体を扱う方法としてスプライトという仕組みがある。libGDX では Sprite クラスで実現する。SimpleApp では、Rectangle を利用していたが Sprite と Rectangle は基本的に同じように利用できる。違いは、Sprite がテクスチャを持っていること前提に使用するものである。Rectangle はテクスチャを持たなくてもいい。基本的にゲームに表示する物体はテクスチャが必要であるから、特別な事情がなければ Rectangle ではなく Sprite の方が適切だろうし、感覚的に扱いやすい。
Sprite インスタンスを生成する方法はいくつかあるが、スプライト毎に異なるテクスチャを持つ必要があるなら、TextureAtlas インスタンスから生成する方法がいいだろう。
TextureAtlas クラスは、テクスチャアトラスを扱うためのクラス。スプライト毎にテクスチャを一枚の小さな画像から生成して描写する処理はフレームワーク側にとっては無駄の多い処理とされる。この問題に対処するため、複数の小さなテクスチャを一枚の大きな画像から生成して扱えるようにする仕組みが TextureAtlas だ。
TextureAtlas で使用するテクスチャの集合画像は、libgdx-texturepacker-gui で簡単に作成できる。input用ディレクトリにパッキング前の画像を用意すれば、パッキング後の画像と画像マッピングファイルを適当なディレクトリに出力できる。また、テクスチャーパッキングをするために設定した一連の情報はプロジェクトという形で保存することができる。
画像が用意できたら、スプライトを下記のコードで生成できる。
TextureAtlas atlas;
Sprite drop;
Sprite bucket;
atlas = new TextureAtlas(Gdx.files.internal("images.atlas"));
drop = atlas.createSprite("droplet");
bucket = atlas.createSprite("bucket");
マルチスクリーン対応したSimpleAppをスプライトとテクスチャアトラスを利用して修正したコード
package com.badlogic.drop;
import java.util.Iterator;
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.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.TimeUtils;
public class GameLoopScreen extends DropScreen {
TextureAtlas atlas;
Sprite drop;
Sprite bucket;
Sound dropSound;
Music rainMusic;
SpriteBatch batch;
OrthographicCamera camera;
Array<Sprite> raindrops;
long lastDropTime;
public GameLoopScreen() {
atlas = new TextureAtlas(Gdx.files.internal("images.atlas"));
drop = atlas.createSprite("droplet");
bucket = atlas.createSprite("bucket");
// 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, Defines.WINDOW_WITDH, Defines.WINDOW_HEIGHT);
batch = new SpriteBatch();
// create a Rectangle to logically represent the bucket
bucket.setX(Defines.WINDOW_WITDH / 2 - 48 / 2);
bucket.setY(20);
bucket.setSize(48, 48);
// create the raindrops array and spawn the first raindrop
raindrops = new Array<Sprite>();
spawnRaindrop();
}
private void spawnRaindrop() {
Sprite raindrop = new Sprite();
raindrop.setX(MathUtils.random(0, Defines.WINDOW_WITDH - 48));
raindrop.setY(Defines.WINDOW_HEIGHT);
raindrop.setSize(48, 48);
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}
@Override
public void update(float delta) {
// process user input
if (Gdx.input.isTouched()) {
Vector3 touchPos = new Vector3();
touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
camera.unproject(touchPos);
bucket.setX(touchPos.x - 48 / 2);
}
if (Gdx.input.isKeyPressed(Keys.LEFT))
bucket.setX(bucket.getX() - 200 * Gdx.graphics.getDeltaTime());
if (Gdx.input.isKeyPressed(Keys.RIGHT))
bucket.setX(bucket.getX() + 200 * Gdx.graphics.getDeltaTime());
// make sure the bucket stays within the screen bounds
if (bucket.getX() < 0)
bucket.setX(0);
if (bucket.getX() > Defines.WINDOW_WITDH - 48)
bucket.setX(Defines.WINDOW_WITDH - 48);
// 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<Sprite> iter = raindrops.iterator();
while (iter.hasNext()) {
Sprite raindrop = iter.next();
raindrop.setY(raindrop.getY() - 200 * Gdx.graphics.getDeltaTime());
if (raindrop.getY() + 48 < 0)
iter.remove();
if (raindrop.getBoundingRectangle().overlaps(bucket.getBoundingRectangle())) {
dropSound.play();
iter.remove();
}
}
}
@Override
public void draw(float delta) {
// 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(GL10.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(bucket, bucket.getX(), bucket.getY());
for (Sprite raindrop : raindrops) {
batch.draw(drop, raindrop.getX(), raindrop.getY());
}
batch.end();
}
@Override
public void dispose() {
atlas.dispose();
dropSound.dispose();
rainMusic.dispose();
batch.dispose();
}
}
入力イベントを調べたり制御するには Gdx.input クラスを使用する。 Gdx.input には、どのキーが押されたか、タッチされているか、などの入力イベントを調べるメソッドが用意されている。より複雑な入力イベントを制御するには InputProcessor をセットすることで、ドラッグなどを扱うことができる。
オリジナルの SimpleApp は水玉を受け取るバケットを isTouched メソッドとgetX, getYによって制御していた。この一連のメソッドでは、どの座標がタッチされているかまでしか分からないので、バケットをドラッグして移動することはできない。ドラッグさせるには、InputProcessorを実装し、touchDragged で処理を記述する必要になる。
なお、Gdx.input で扱うy座標は、通常のy座標(上から下へ値が増加)になる。
package com.badlogic.drop;
import java.util.Iterator;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.InputProcessor;
import com.badlogic.gdx.Input.Keys;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.audio.Sound;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.math.MathUtils;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.TimeUtils;
public class GameLoopScreen extends DropScreen implements InputProcessor {
TextureAtlas atlas;
Sprite drop;
Sprite bucket;
Sound dropSound;
Music rainMusic;
SpriteBatch batch;
OrthographicCamera camera;
Array<Sprite> raindrops;
long lastDropTime;
public GameLoopScreen() {
atlas = new TextureAtlas(Gdx.files.internal("images.atlas"));
drop = atlas.createSprite("droplet");
bucket = atlas.createSprite("bucket");
dropSound = Gdx.audio.newSound(Gdx.files.internal("drop.wav"));
rainMusic = Gdx.audio.newMusic(Gdx.files.internal("rain.mp3"));
rainMusic.setLooping(true);
rainMusic.play();
camera = new OrthographicCamera();
camera.setToOrtho(false, Gdx.graphics.getWidth(), Gdx.graphics.getHeight());
batch = new SpriteBatch();
bucket.setX(Gdx.graphics.getWidth() / 2 - 48 / 2);
bucket.setY(20);
bucket.setSize(48, 48);
raindrops = new Array<Sprite>();
spawnRaindrop();
Gdx.input.setInputProcessor(this);
}
private void spawnRaindrop() {
Sprite raindrop = new Sprite();
raindrop.setX(MathUtils.random(0, Gdx.graphics.getWidth() - 48));
raindrop.setY(Gdx.graphics.getHeight());
raindrop.setSize(48, 48);
raindrops.add(raindrop);
lastDropTime = TimeUtils.nanoTime();
}
@Override
public void update(float delta) {
if (Gdx.input.isKeyPressed(Keys.LEFT))
bucket.setX(bucket.getX() - 200 * Gdx.graphics.getDeltaTime());
if (Gdx.input.isKeyPressed(Keys.RIGHT))
bucket.setX(bucket.getX() + 200 * Gdx.graphics.getDeltaTime());
if (bucket.getX() < 0)
bucket.setX(0);
if (bucket.getX() > Gdx.graphics.getWidth() - 48)
bucket.setX(Gdx.graphics.getWidth() - 48);
if (TimeUtils.nanoTime() - lastDropTime > 1000000000 / 2)
spawnRaindrop();
Iterator<Sprite> iter = raindrops.iterator();
while (iter.hasNext()) {
Sprite raindrop = iter.next();
raindrop.setY(raindrop.getY() - 500 * Gdx.graphics.getDeltaTime());
if (raindrop.getY() + 48 < 0)
iter.remove();
if (raindrop.getBoundingRectangle().overlaps(bucket.getBoundingRectangle())) {
dropSound.play();
iter.remove();
}
}
}
@Override
public void draw(float delta) {
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
camera.update();
batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(bucket, bucket.getX(), bucket.getY());
for (Sprite raindrop : raindrops) {
batch.draw(drop, raindrop.getX(), raindrop.getY());
}
batch.end();
}
@Override
public void dispose() {
atlas.dispose();
dropSound.dispose();
rainMusic.dispose();
batch.dispose();
}
@Override
public boolean touchUp(int arg0, int arg1, int arg2, int arg3) {
return false;
}
@Override
public boolean touchDragged(int x, int y, int pointer) {
if (bucket.getBoundingRectangle().contains(x, Gdx.graphics.getHeight() - y )) {
Vector3 touchPos = new Vector3();
touchPos.set(x, y, 0);
camera.unproject(touchPos);
bucket.setX(touchPos.x - 48 / 2);
return true;
}
return false;
}
@Override
public boolean touchDown(int arg0, int arg1, int arg2, int arg3) {
return false;
}
@Override
public boolean scrolled(int arg0) {
return false;
}
@Override
public boolean mouseMoved(int arg0, int arg1) {
return false;
}
@Override
public boolean keyUp(int arg0) {
return false;
}
@Override
public boolean keyTyped(char arg0) {
return false;
}
@Override
public boolean keyDown(int arg0) {
return false;
}
}
ゲーム上の演出で爆発だったり魔法を表現するのにパーティクルは使える。libGDXではParticle Editorで見た目を確認しながら好みのパーティクルを作ることができる。
パーティクルを作成するには、少なくともエミッターが必要になる。エミッターはパーティクルの構成要素で同じ形の粒子の集まりのようなもの。粒子は一枚の画像が必要になる。
パーティクルが作成できたら、ParticleEffect.load()で読み込むことができる。詳細はhttps://github.com/libgdx/libgdx/blob/master/tests/gdx-tests/src/com/badlogic/gdx/tests/ParticleEmitterTest.java
scene2dは2Dグラフィックス用のコンポーネントで、図形を描写したり、描写した図形に対して発生したイベントの制御ができる。これを利用してUIウィジェットを扱えるようにしたものが、scene2duiである。
scene2duiはテーブルレイアウトやボタン、チェックボックス、リストなどの色々なウィジェットをサポートしており、ゲームの設定画面を作ったりするのに向いている。また、スキンを変えることで色々な見栄えを表現できる。基本的なウィジェットは大体揃っているのでこれを使ってゲーム以外のアプリケーションを作成することもできるだろう。Android標準コンポーネントを使用してアプリを作った場合、移植性が無いが、libGDXならばそれも容易になるはず。
package com.badlogic.drop;
import aurelienribon.tweenengine.Timeline;
import aurelienribon.tweenengine.Tween;
import aurelienribon.tweenengine.TweenManager;
import aurelienribon.tweenengine.equations.Bounce;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL10;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.InputEvent;
import com.badlogic.gdx.scenes.scene2d.InputListener;
import com.badlogic.gdx.scenes.scene2d.Stage;
import com.badlogic.gdx.scenes.scene2d.ui.ImageButton;
import com.badlogic.gdx.scenes.scene2d.ui.Skin;
import com.badlogic.gdx.scenes.scene2d.ui.Table;
import com.badlogic.gdx.scenes.scene2d.ui.TextButton;
import com.badlogic.gdx.scenes.scene2d.utils.ChangeListener;
import com.badlogic.gdx.scenes.scene2d.utils.SpriteDrawable;
public class GameLoopScreen extends DropScreen {
private int width = 800;
private int height = 480;
Texture dropImage;
SpriteBatch batch;
OrthographicCamera camera;
Sprite raindrop;
TweenManager tweenManager;
Stage stage;
public GameLoopScreen() {
dropImage = new Texture(Gdx.files.internal("droplet.png"));
camera = new OrthographicCamera();
camera.setToOrtho(false, width, height);
batch = new SpriteBatch();
raindrop = new Sprite(dropImage);
raindrop.setX(0);
raindrop.setY(height);
Tween.registerAccessor(Sprite.class, new RainDropAccessor());
tweenManager = new TweenManager();
Timeline.createSequence()
.beginParallel()
.push(Tween.to(raindrop, RainDropAccessor.POSITION_Y, 1.5f).target(0,0).ease(Bounce.OUT))
.end()
.start(tweenManager);
stage = new Stage();
Gdx.input.setInputProcessor(stage);
Skin skin = new Skin(Gdx.files.internal("data/uiskin.json"));
Table table = new Table();
stage.addActor(table);
table.setSize(260, 195);
table.setPosition(190, 142);
// table.align(Align.right | Align.bottom);
table.debug();
ImageButton button = new ImageButton(new SpriteDrawable(new Sprite(dropImage)));
// TextButton button = new TextButton("Button 1", skin);
button.addListener(new InputListener() {
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
System.out.println("touchDown 1");
raindrop.setY(height);
tweenManager.killAll();
Timeline.createSequence()
.beginParallel()
.push(Tween.to(raindrop, RainDropAccessor.POSITION_Y, 1.5f).target(0,0).ease(Bounce.OUT))
.end()
.start(tweenManager);
return true;
}
});
table.add(button);
// table.setTouchable(Touchable.disabled);
Table table2 = new Table();
stage.addActor(table2);
table2.setFillParent(true);
table2.bottom();
TextButton button2 = new TextButton("Button 2", skin);
button2.addListener(new ChangeListener() {
public void changed (ChangeEvent event, Actor actor) {
System.out.println("2!");
}
});
button2.addListener(new InputListener() {
public boolean touchDown (InputEvent event, float x, float y, int pointer, int button) {
System.out.println("touchDown 2");
return false;
}
});
table2.add(button2);
}
@Override
public void update(float delta) {
tweenManager.update(delta);
}
@Override
public void draw(float delta) {
Gdx.gl.glClearColor(0, 0, 0.2f, 1);
Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT);
camera.update();
batch.setProjectionMatrix(camera.combined);
batch.begin();
batch.draw(dropImage, raindrop.getX(), raindrop.getY());
batch.end();
stage.act(Gdx.graphics.getDeltaTime());
stage.draw();
Table.drawDebug(stage);
}
@Override
public void resize(int width, int height) {
stage.setViewport(width, height, true);
}
@Override
public void dispose() {
stage.dispose();
dropImage.dispose();
batch.dispose();
}
}
LibGDXのマップ関連クラスはcom.badlogic.gdx.mapsパッケージで見つけられる。パッケージルートには基本的なクラスがあり、サブパッケージには専門的なマップや特徴的なマップクラスがある。
基本クラスはマップを表現する基本的なクラスである。タイルマップ以外の2Dマップもサポートされている。マップはレイヤーのセットである。レイヤーはオブジェクトのセットである。マップ、レイヤー、オブジェクトはプロパティをもっており、ロードしたマップフォーマットに依存している。なぜなら、専門的なマップ、レイヤーオブジェクトの実装が色々あるためである。
プロパティは、MapPropertiesによって表現される。このクラスは基本的にキーと任意の値からなるハッシュマップである。そのキーと値はロードされたフォーマットに依存する。プロパティへアクセスするには
map.getProperties().get("custom-property", String.class);
layer.getProperties().get("another-property", Float.class);
object.getProperties().get("foo", Boolean.class);
サポートされた多くのマップエディターはマップ、レイヤー、オブジェクトのプロパティの指定ができる。
マップレイヤーは0を開始としたindexで取り出せる。
MapLayer layer = map.getLayers().getLayer(0);
名前でも取り出せる。
MapLayer layer = map.getLayers().getLayer("my-layer");
getLayer()は、常にMapLayerを返すので、専門的なマップやより多くの機能を使いたいならキャストで取り出す必要がある。
TiledMapTileLayer tiledLayer = (TiledMapTileLayer)map.getLayers().getLayer(0);
マップレイヤーは幾つかの標準かされた属性をもっており取り出せるようになっている。
String name = layer.getName();
float opacity = layer.getOpacity();
boolean isVisible = layer.isVisible();
マップオブジェクトはgetObjectsから取り出せる。
MapObjects objects = map.getObjects();
MapObjectsから個々のマップオブジェクトをインデックス、名前、インスタンスを指定して取り出すことができる。
マップAPIは少量ではあるが、CircleMapObject, PolygonMapObject等の専門的なマップオブジェクトを提供している。それらのオブジェクトはマップローダによってパースされ個々のレイヤーに置かれる。全てのマップオブジェクトは標準化された属性をもっており取り出すことができる。
String name = object.getName();
float opacity = object.getOpacity();
boolean isVisible = object.isVisible();
Color color = object.getColor();
PolygonMapObjectのような専門的なオブジェクトは追加属性を持っている。
Polygon poly = polyObject.getPolygon();
オブジェクトは一般的にスパウンポイントや衝突シェイプのトリガーとして使われる。なお、後述するタイルマップのレイヤーはマップオブジェクトをもっていない。
MapRendererインターフェースは、マップ、レイヤー、オブジェクトを描画するためのメソッドを定義する。描画を行う前にマップのビューを設定しなければならない。簡単なやり方はOrthographicCameraを与えることだ。
mapRenderer.setView(camera);
または、プロジェクションマトリックス(射影行列)と境界線を手動で設定できる。
mapRenderer.setView(projectionMatrix, startX, startY, endx, endY);
全てのマップレイヤーの描写は引数無しのrender()を呼べばいい。レイヤーの描写を制御したい場合はレイヤーの番号をint配列で指定することもできる。バックエンドのレイヤーが二つ、フォアグラウンドのレイヤーが一つあったとしたらこうなる。
int[] backgroundLayers = { 0, 1 }
int[] foregroundLayers = { 2 };
mapRenderer.render(backgroundLayers);
mapRenderer.render(foregroundLayers);
タイルマップ関連のクラスはcom.badlogic.gdx.maps.tiledにある。タイルマップはTiledMapインスタンスにロードされる。TiledMapは基本マップクラスのサブクラスだ。TiledMapは追加の属性とメソッドをもっている。
タイルマップレイヤーはTiledMapTileLayerに保持される。
TiledMap tiledMap = loadMap();
TiledMapTileLayer layer = (TiledMapTileLayer)tiledMap.getLayers().getLayer(0);
TiledMapTileLayerはMapLayerの標準的属性、プロパティ、オブジェクトを持っている。
概要
Overlap2Dは、LibGDX 向けのレベル or UI エディタ。Unity とか他のフレームワークでは公式エディタがあるが、これは公式というわけでは無い。
チュートリアルを一通りやってみて感じたのは、強制終了などの不具合が気になるものの、横スクロールの2Dアクションをはじめとして、落ち物パズルゲーム等は効率的に開発できるように思われる。
Overlap2D エディタで作成したデータは、android または、core 直下の assets ディレクトリにエクスポートされたものがデフォルトで利用されるようになっている。もし、デフォルトのパス以外からデータ利用をしたい場合は、アプリ側にコードを追加する必要があるだろう。
また、Overlap2D のデータを利用するには専用ランタイムが必要になる。このランタイムはアプリで参照している libGDX のバージョンと同じにする必要がある。同じにして置かないと Overlap2D のランタイム側の方がバージョンが古いため、一部の libGDX API を利用できない問題が発生する。この問題は build.gradle の gdxVersion、box2dlightsVersion を修正することで解決できる。
Overlap2D ランタイムは、wiki で説明されるような、Box2d の初期化処理を請け負ってくれる。
ランタイム
Overlap2d上のアイテム
Overlap2 上のアイテムは基本的に、scene2d の Actor のサブクラスで、IBaseItem インターフェースを実装している。IBaseItem は、Box2D の要になる Body を持たせることができる。Body が無いアイテムは Box2D の効果を得られない。
動的にアイテムを追加する
Overlap2D では、通常、エディタでアイテムを配置して利用することを想定しているが、動的に追加させることもできる。動的に追加を行う場合は、事前に、アセットを Overlap2D 管理下にしておく必要がある。
Overlap2D で動的にアイテムを画面に追加したい場合は、sceneLoader.getRoot() に対して addItem() を実行する。addItem() は、引数のアイテムを sceneLoader.getRoot() の子として追加する。
Box2D による制御を行いたい場合は、エディタでフィジックスの編集を済ませた上で、 meshId と physicsBodyData を addItem() の前の段階で設定しておく必要がある。
SimpleImageVO simpleImageVO = new SimpleImageVO();
simpleImageVO.imageName = imageName;
simpleImageVO.meshId = essentials.rm.getProjectVO().assetMeshMap.get(imageName);
simpleImageVO.physicsBodyData = essentials.rm.getProjectVO().meshes.get(simpleImageVO.meshId + "").initialProperties;