ここまではActorsとActions と入力制御の観点から、Scene2D が提供する機能について見てきました。 今回は、Scene2D が提供するシーン管理機能についていくつか見ていきます。 Scene2D が持つ非常に強力な機能の一つは、グループ化です。 それでは、さっそく例を見てみましょう。今回の例では、以下二つの画像を使用します:
ところで、これらは二つの異なる画像です。一つは飛行機で、二つはエンジンの排気です。 一つのシーン内で、上記二つの画像を操作可能な一つのエンティティとしてグループ化する方法について見てみましょう。 ゲーム開発では非常に一般的な作業です。
package com.me.mygdxgame; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.TextureRegion; import com.badlogic.gdx.scenes.scene2d.Actor; import com.badlogic.gdx.scenes.scene2d.Group; import com.badlogic.gdx.scenes.scene2d.Stage; import static com.badlogic.gdx.scenes.scene2d.actions.Actions.*; public class SceneManagementDemo implements ApplicationListener { private Stage stage; private Group group; @Override public void create() { stage = new Stage(Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),true); final TextureRegion jetTexture = new TextureRegion(new Texture("data/jet.png")); final TextureRegion flameTexture = new TextureRegion(new Texture("data/flame.png")); final Actor jet = new Actor(){ public void draw(Batch batch, float alpha){ batch.draw(jetTexture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), getScaleX(), getScaleY(), getRotation()); } }; jet.setBounds(jet.getX(), jet.getY(), jetTexture.getRegionWidth(), jetTexture.getRegionHeight()); final Actor flame = new Actor(){ public void draw(Batch batch, float alpha){ batch.draw(flameTexture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), getScaleX(), getScaleY(), getRotation()); } }; flame.setBounds(0, 0, flameTexture.getRegionWidth(), flameTexture.getRegionHeight()); flame.setPosition(jet.getWidth()-25, 25); group = new Group(); group.addActor(jet); group.addActor(flame); group.addAction(parallel(moveTo(200,0,5),rotateBy(90,5))); stage.addActor(group); } @Override public void dispose() { stage.dispose(); } @Override public void render() { Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); stage.act(Gdx.graphics.getDeltaTime()); stage.draw(); } @Override public void resize(int width, int height) { } @Override public void pause() { } @Override public void resume() { } }
実行すると、以下が表示されます:
ご覧のように、グループ化されると取り付けられた各actorは、所属グループに適用される変形処理をそのまま受け継ぎます。 この例のコードは非常に簡単なもので、新しく追加された要素はあまりありません。 最初に、二つの画像をTextureRegionsとして読み込んでいます。 それから各TextureRegion用にactorを作成し、その両方で setBounds()を使って境界を設定します。設定しないと、正しく描写されません。 各Actor について、 batch.draw()をフルで実装し、回転と拡大縮小がされても正しく描写されるようにします。 最後にエンジン排気のtextureについて、飛行機のActorからの相対位置を設定します。
それからGroup オブジェクトを新規に作成します。このGroup オブジェクトがScene2D のグループ化における最も重要な要素になります。 その後、二つのActorをSceneに追加するのではなく、Groupに追加します。そしてそのGroupをSceneに追加します。 この例がどのように動くのかを見るために、moveTo とrotateBy のアクションをgroupへ適用します。 アクションの詳細について知りたい場合は、 前回のチュートリアルで説明しています。 今回のサンプルでは説明していない重要な点が一つあります。Group内の各Actor は個別に操作することが可能です。
Scene2D のもう一つの機能は、タッチされたかどうかを判別することです。
以前のScene2D チュートリアルPart 1では、
Scene 内のActorがタッチされた時に呼び出されるtouchDown機能について見てきました。
今回は、この処理が動作するロジックについて簡単に見ていきます。
Stageのhit() メソッドが呼び出されると、stage内の全てのactorのhit()メソッドを順々に呼び出していきます。
hit()メソッドでは、開発者が簡単に処理を行えるように、変換されていない座標を引数として渡します。
既定のhit()メソッドは、単にActorの境界ボックスを確認しているだけです…
以下の例では代わりに、判定用の境界に円を使用しています… in case you had a say… circular object!
package com.me.mygdxgame; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.Batch; import com.badlogic.gdx.graphics.g2d.TextureRegion; 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.Touchable; import java.util.Random; public class SceneManagementDemo implements ApplicationListener { // Create an Actor "Jet" that displays the TextureRegion passed in class Jet extends Actor { private TextureRegion _texture; public Jet(TextureRegion texture){ _texture = texture; setBounds(getX(),getY(),_texture.getRegionWidth(), _texture.getRegionHeight()); this.addListener(new InputListener(){ public boolean touchDown(InputEvent event, float x, float y, int pointer, int buttons){ System.out.println("Touched" + getName()); setVisible(false); return true; } }); } // Implement the full form of draw() so we can handle rotation and scaling. public void draw(Batch batch, float alpha){ batch.draw(_texture, getX(), getY(), getOriginX(), getOriginY(), getWidth(), getHeight(), getScaleX(), getScaleY(), getRotation()); } // This hit() instead of checking against a bounding box, checks a bounding circle. public Actor hit(float x, float y, boolean touchable){ // If this Actor is hidden or untouchable, it cant be hit if(!this.isVisible() || this.getTouchable() == Touchable.disabled) return null; // Get centerpoint of bounding circle, also known as the center of the rect float centerX = getWidth()/2; float centerY = getHeight()/2; // Square roots are bad m'kay. In "real" code, simply square both sides for much speedy fastness // This however is the proper, unoptimized and easiest to grok equation for a hit within a circle // You could of course use LibGDX's Circle class instead. // Calculate radius of circle float radius = (float) Math.sqrt(centerX * centerX + centerY * centerY); // And distance of point from the center of the circle float distance = (float) Math.sqrt(((centerX - x) * (centerX - x)) + ((centerY - y) * (centerY - y))); // If the distance is less than the circle radius, it's a hit if(distance <= radius) return this; // Otherwise, it isnt return null; } } private Jet[] jets; private Stage stage; @Override public void create() { stage = new Stage(Gdx.graphics.getWidth(),Gdx.graphics.getHeight(),true); final TextureRegion jetTexture = new TextureRegion(new Texture("data/jet.png")); jets = new Jet[10]; // Create/seed our random number for positioning jets randomly Random random = new Random(); // Create 10 Jet objects at random on screen locations for(int i = 0; i < 10; i++){ jets[i] = new Jet(jetTexture); //Assign the position of the jet to a random value within the screen boundaries jets[i].setPosition(random.nextInt(Gdx.graphics.getWidth() - (int)jets[i].getWidth()) , random.nextInt(Gdx.graphics.getHeight() - (int)jets[i].getHeight())); // Set the name of the Jet to it's index within the loop jets[i].setName(Integer.toString(i)); // Add them to the stage stage.addActor(jets[i]); } Gdx.input.setInputProcessor(stage); } @Override public void dispose() { stage.dispose(); } @Override public void render() { Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); stage.act(Gdx.graphics.getDeltaTime()); stage.draw(); } @Override public void resize(int width, int height) { } @Override public void pause() { } @Override public void resume() { } }
このアプリを実行するとランダムに配置された10個の飛行機の画像が画面に描写されます。
各飛行機をクリックすると、クリックされた飛行機は消えます。 理解しておくべき重要な点は、hit()はオブジェクトが作成されたのとは逆の順番で判定されるということです。 そのため、同一スペースに複数のオブジェクトが存在している場合、stageに最後に追加されたオブジェクトのhit()判定が最初に行われます。 hit()とは、飛行機がタッチされているかされていないかを判別する機能です。 hit()では、境界円を作成してそれを画像のサイズに合わせて、マウスの指す位置がその境界円内にあるかを確認することで判定を行っています。 当たり判定が発生した時はその対象オブジェクトを戻り値として返し、発生していない場合はnullを返します。
このシステムの良いところは、親オブジェクトに適用された操作や既にオブジェクト自身に適用された操作について、ユーザーは気にする必要がないということです。 It’s also important to realize this is a pretty derived example. 上書きしたhit()メソッドを削除した場合は、実装されている既定の hit()メソッドが動作します。 既定のhit()メソッドがあなたの要件を満たしているのであれば、Actorを継承したクラスでhit() メソッドを独自実装する必要はありません。 今回の例では、Scene2Dの当たり判定がどのように動作するのか、独自の当たり判定をどのように実装するのか、を例示しているに過ぎません。 ピクセル単位での完璧な当たり判定を実装したいというのであれば、それも可能です。 コードの詳細について説明するために、今回の例ではいつもよりも少し多めにコメントを記載しています。