libGDX入門 その03 ログ出力とアニメーションとバーチャルパッド
はじめに
ログの出しかた
プログラムの動作を追うとき、ログ出力はとても重宝しますよね。 デバッガもいいですが、私は昔ながらのデバッグプリントで追うのが好きです。
ということで、ログ出力の方法を解説します。
公式から引用します。
Gdx.app.log("MyTag", "my informative message");
Gdx.app.error("MyTag", "my error message", exception);
Gdx.app.debug("MyTag", "my error message");
log
メソッドは普通に情報を出力したい場合、
error
メソッドは、例外を共なうようなエラー出力(第3引数にはException型)をしたい場合、
debug
メソッドは開発時のみ出力したい場合に使用します。
第一引数の MyTag
は識別子ですね。一般的にはにはクラス名などが多いようです。
自分は下記のようにクラスフィールドを定義しておき、この LOG_TAG
を使用します。
public static final String LOG_TAG = GameScreen.class.getSimpleName();
こんな感じです。
Gdx.app.log(LOG_TAG, "my informative message");
次のように表示されます。
GameScreen: my informative message
こうするとログ出力にクラス名も一緒に表示されるので、 どこで出力したメッセージか一目でわかるので便利です。
これらのログ出力は、ログのレベルを指定することで メッセージの出力範囲を制御できます。
下記のように指定します。
Gdx.app.setLogLevel(Application.LOG_DEBUG);
上はデバッグレベルを指定しています。
ログレベルには下記の4種類あります。
Application.LOG_NONE | 全てのログを無効にします。 |
Application.LOG_DEBUG | 全てのログを出力します。 |
Application.LOG_ERROR | errorメソッドのログだけ出力します。 |
Application.LOG_INFO | logとerrorメソッドのログを出力します。 |
開発時は、DEBUGレベル、リリース時は、NONEレベルですかね。 パフォーマンス重視の場合は、ログメソッドはリリース時に全て削除したほうがいいです。
公式情報は下記にあります。
スプライトアニメーション
前回までは静止画を拡縮機能で伸び縮みさせただけでした。 今回は、パターンをつくってアニメーションさせて見ます。 アニメーションさせるとグっとゲーム感が増します。
完成したプログラムのスクリーンショットです。
仕様としては、
- ターゲット解像度はファミコンライク(16:9の256 x 144ドット)
- 16x16ドットの猫のキャラを表示
- 待機モーションあり
- 走りモーションあり
- 左右に移動
- バーチャルパッド
って感じにしました。
そして作ったのがこれ。
左から
- 待機1, 待機2, 走り1, 走り2, 走り3
と計5コマのスプライトアニメとなっております。 猫の画像の下には、左右に動かすためのボタンがあります。
画像のサイズは、256x256にしてあります。使用している画像サイズを考えると大きすぎますが、 一般的には、画像のサイズは512x512,1024x1024あたりがVRAMへの転送効率が良いと言われており、 今回は、単に512だと大きすぎて作業しずらかったので512の半分の256にしました。 画像サイズは2のn乗でないと効率的に扱えないことが多いので、512あたりのサイズが無難です。
また、画像の単位を16x16ドットのセル単位で描くようにしています。 こうすることによりコード内での単位が統一されプログラミングしやすくなります。
スプライトをアニメーションさせるには、 Animation
クラスを使用します。
今回は、走りモーションと待機モーションが2種類あり、左右分用意するため、
合計4種類のアニメーションが必要になります。
下記のフィールドを準備します。
private Animation animLeft;
private Animation animRight;
private Animation animIdleLeft;
private Animation animIdleRight;
private float stateTime = 0;
アニメーションの具体的なコードは下記になります。
TextureRegion[] split = new TextureRegion(img).split(16, 16)[0]; // #1
TextureRegion[] mirror = new TextureRegion(img).split(16, 16)[0]; // #2
for (TextureRegion region : mirror) // #3
region.flip(true, false);
animRight = new Animation(0.1f, split[2], split[3], split[4]); // #4
animLeft = new Animation(0.1f, mirror[2], mirror[3], mirror[4]);
animIdleRight = new Animation(0.5f, split[0], split[1]);
animIdleLeft = new Animation(0.5f, mirror[0], mirror[1]);
stateTime = 0; // #5
TextureRegion
クラスは、ある一枚のテクスチャの一部の矩形情報をもったオブジェクトです。
テクスチャにひとつの画像しかない場合は、 Texutre
オブジェクトをそのまま利用しますが、
今回のように1枚の画像にいろいろな画像を詰めこんである場合は、 TexutreRegion
オブジェクトを利用します。
TextureRegion
にアニメーションの1コマ1コマを格納し、その TextureRegion
を描画することで
アニメーションさせます。
#1
では、 TextureRegion#split()
メソッドで img
テクスチャを16x16(単位はピクセル)のセルで分割し、
TextureRegion の2次元配列を取得します。同時にその2次元配列の1行目を取得することで、
テクスチャの最初の行の画像(猫のアニメーションパターン)を取得しています。
(今回は、1行目だけが必要なので、0固定です)。
#2
では、 #1
と同様ですが、左向きのデータを得るために、 #3
でデータを左右反転しています。
TexutreRegion#flip()
メソッドの第一引数は、x軸、第二引数は、Y軸の反転指定になります。
この場合はX軸のみ指定しています。これで左向きのアニメーションパターンを手に入れることができました。
Animeation
クラスは、アニメーション情報を保持するオブジェクトです。
アニメーション情報とは、どのコマ(画像)を、何秒間表示させるか、といった情報です。
コンストラクタの第一引数に各コマの秒数、
第二引数以降には、そのアニメーションを構成する TextureRegin
オブジェクトを可変引数として渡します。
#4
の場合は、3コマを0.1秒ごとに切り替えるアニメで、先に生成したsplitからそれぞれのコマを指定します。
stateTime
は、アニメーションの進行に必要な経過時間を保持します。
以上で準備できました。
あとは、猫の状態に応じて(右を向いてるのか左を向いてるのか、止っているのか、走っているのかなど)、
適切な Animation
オブジェクトを選択し、そのAnimationオブジェクトが
指し示す TextureRegion
オブジェクトを描画します。
batch.begin();
bg.draw(batch);
boolean loop = true;
float width = 16;
float height = 16;
Animation nekoAnim = currentAnim(); // #1
batch.draw(nekoAnim.getKeyFrame(stateTime, loop), // #2
pos.x, pos.y, // #3
width, height); // #4
batch.end();
stateTime += Gdx.graphics.getDeltaTime(); // #5
#1
では、 currentAnim()
メソッド(自前メソッド、後述)から、現在のアニメーションを取得します。
#2
では、 Animation#getKeyFrame()
メソッドの第一引数へ現在の経過時間を渡し、
適切な TexutreRegion
オブジェクトを取得します。
第二引数では、アニメーションをループするかどうかを指定します。
#3
、 #4
で描画先の座標と、描画の幅と高さを指定します。
#5
では、 Gdx.graphics.getDeltaTime()
メソッドを使って、
前回のフレームからの経過時間を取得しています(単位は秒)。
stateTime
の値を増加させることでアニメーションのコマを進めることができます。
さて、このプログラムでは次のように直行する2つの状態を扱って、アニメーションを切り替えています。
この状態をプログラムしたのが上述の currentAnim()
メソッドです。
private Animation currentAnim() {
Animation anim = null;
if (state == STATE_MOVE) {
if (dir == DIR_LEFT) {
anim = animLeft;
} else {
anim = animRight;
}
} else {
if (dir == DIR_LEFT) {
anim = animIdleLeft;
} else {
anim = animIdleRight;
}
}
return anim;
}
state
は移動状態、 dir
は向きの状態です。中身は、クラスフィールドで定義しておきます。
libGDXには直接関係ありませんが、わかりづらいところかもしれないので解説しました。
ここまでの情報でアニメーションをどう表示させるかがわかったと思います。 しかし、どう動かすかはまだ決めていません。
前回までにタッチ処理の方法はわかったので、今回はスプライトでボタンもどきを作って そのボタンをタッチしている間、左右に動かすようにしてみます。
バーチャルパッド
仕組みは簡単です。左と右を表わすボタンスプライトを用意し、 タッチ座標がそのスプライトの矩形に入っているかどうかを判定します。 矩形に入っていれば、そのボタンの明度を下げて押されているように見せかけます。
まずスプライトの準備です。次のフィールドを用意します。
private Sprite leftButton;
private Sprite rightButton;
初期化は以下のような感じ。
leftButton = new Sprite(img, 0, 16*2, 16*3, 16*2);
rightButton = new Sprite(img, 16*3, 16*2, 16*3, 16*2);
leftButton.setPosition(8, 0);
rightButton.setPosition(8 + 16*3, 0);
左下に表示させるべく、16ドットを1マスとして計算しています。
そしてボタンがタッチされたかどうかの判定です。 これはこのプログラムの一番重要なところかもしれません。
leftButton.setColor(Color.WHITE); // #1
rightButton.setColor(Color.WHITE); // #2
if (Gdx.input.isTouched()) { // #3
float x = Gdx.input.getX(); // #4
float y = Gdx.input.getY();
uiViewport.unproject(touchPoint.set(x, y, 0)); // #5
Rectangle leftBounds = leftButton.getBoundingRectangle(); // #6
Rectangle rightBounds = rightButton.getBoundingRectangle(); // #7
if (leftBounds.contains(touchPoint.x, touchPoint.y)) { // #8
leftButton.setColor(Color.GRAY); // #9
left();
} else if (rightBounds.contains(touchPoint.x, touchPoint.y)) { // #10
rightButton.setColor(Color.GRAY); // #11
right();
}
}
#1
、 #2
では、ボタンスプライトの明度を初期化しています。カラーをホワイトにすることにより、
通常のスプライトのテクスチャのカラーをそのまま表示することになります。
#3
は、前回解説しました。タッチさたかどうかを判定するメソッドです。
画面をタッチしつづける限り真が返ってきます。
#4
は、タッチされた座標を取得します。この座標は左上を原点とするスクリーン座標が返ってくるので
そのままでは使えません。
#5
の uiViewPort.unproject()
メソッドで、
#6
で取得したタッチ座標をビューポート座標経由でワールド座標に変換します。
前回の記事でここの解説に誤りがありました( m(_ _)m )。前回の記事では、 Viewport#unproject
ではなく、
Camera#unproject
メソッドを使用していました。これだとビューポートを考慮しないので、正しい座標が返ってきません。
Viewport
クラスを使用している場合は、 Viewport#unproject
メソッドを使用してください。
#6
、 #7
では、ボタンスプライトの矩形情報を Rectangle
クラスに格納します。
Rectangle
クラスは、矩形の左下の位置と、幅と高さの情報を持つクラスです。
#8
、 #10
では、 Rectangle#contains()
メソッドにより、タッチ座標が矩形内にあるかどうかを判定します。
真であれば、ボタンをタッチしたことになるので、移動処理(right,leftメソッド)を実行します。
#9
、 #11
では、 ボタンが押されていることを視覚的にわからせるために、スプライトの明度を下げています。
Color.GRAY
を指定することで、半分位の暗さになり、丁度いい感じになります。
テクスチャラップ
今回、背景には、空の画像を使用していますが、 これには「テクスチャラップ」というOpenGLの機能を使用しています。
通常テクスチャの座標を指定するには、テクスチャ座標系を使用します。
テクスチャ座標系とは、左上を原点とし、U軸は右方向、 V軸は下方向に伸びる軸を持つ座標系のことです。1
テクスチャ座標系では、ピクセル座標ではなく、1.0を最大値とした正規化した座標を指定します。 正規化した座標とは、画像のサイズを1.0に丸めたものです。
正規化座標 = ピクセル座標 ÷ テクスチャサイズ
で求まります。
例えば、512x512のテクスチャサイズの(256,256)のUV座標を得る場合は、
256/512=0.5
となり、正規化座標は、0.5となります。
通常は、この1.0内のなかで座標を指定すれば良いのですが、 1.0より大きい値を指定することもできます。 ただしその場合は、1.0を超えたときの挙動を予め指定しておく必要があります。
今回は、背景の画像に対して、1.0以上の値を指定し、 その挙動として、テクスチャの画像を繰り返す指定をしました。
テクスチャラップのコードは以下になります。
bgImg.setWrap(TextureWrap.Repeat, TextureWrap.Repeat); // #1
bg = new Sprite(bgImg, 0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT); // #2
bg.setU(0); // #3
bg.setU2(5);
bg.setV(0);
bg.setV2(5);
#1
で、1.0を超えたUV指定をしたとき、テクスチャを繰り返す指定をしています。
#2
で、背景のスプライトサイズを指定しています。これはターゲット解像度と同じ大きさにしています。
#3
で、UV座標として、0.0 〜 5.0 の値を指定しています。
setU(), setV()
が左上の座標、 setU2(), setV2()
が右下の座標になります。
あとは、実際の描画ですが、今回の背景の場合、画面に固定で表示したかったため UIカメラを使って描画しています。
private void draw() {
// BGカメラセットアップ
uiCamera.update();
batch.setProjectionMatrix(uiCamera.combined);
batch.begin();
bg.draw(batch);
batch.end();
:
}
背景として描画するため、drawメソッドの開始直後に描画しています。
今回は詳しく解説しませんが、UV座標を毎フレームずらすことでスクロール処理することができます。
これを一般的にUVスクロールといいます。
一昔まえのゲームのタイトルやプレーヤーセレクト画面などで多様されていました。
update
メソッドの最後にコメントアウトしておきましたので、
興味のある人は、コメントを外してみてください。おもしろい効果だと思いますのでおすすめです。
以上で、今回の肝となる部分の解説を終えました。 ぜひ、次章のソースコードをコピペし、アセット一覧をダウンロードしてプログラムを実行してみてください。 生意気げなクロネコがリズムを取りながら待機し、ボタンを押すことで走りまわります。
隠れデバッグ機能として、Rキーでネコの位置をリセットします。
ソースコード
今回解説したプログラムの全ソースです。
package com.zarudama.libgdxtest;
import com.badlogic.gdx.Application;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.Input;
import com.badlogic.gdx.audio.Music;
import com.badlogic.gdx.graphics.Color;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.Texture.TextureWrap;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.BitmapFont;
import com.badlogic.gdx.graphics.g2d.Sprite;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Rectangle;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;
public class LibGdxSample extends ApplicationAdapter {
public static final String LOG_TAG = LibGdxSample.class.getSimpleName();
// 16:9
public static int LOGICAL_WIDTH = 256;
public static int LOGICAL_HEIGHT = 144;
// 1秒間に16x3ドット進む
private static final float MOVE_SPEED = 16*3.0f/60;
// 移動状態
private static final int STATE_IDLE = 0;
private static final int STATE_MOVE = 1;
// 向き状態
private static final int DIR_RIGHT = 1;
private static final int DIR_LEFT = 2;
// ゲームに使用するテクスチャ
private Texture img;
private SpriteBatch batch;
// BGM
private Music music;
// 画面フォント用
private BitmapFont font;
// ゲームカメラとビュポート
private OrthographicCamera camera;
private Viewport viewport;
// UIカメラとビュポート
private OrthographicCamera uiCamera;
private Viewport uiViewport;
// タッチ座標変換用ウケザラ
private Vector3 touchPoint;
// 左右のボタン
private Sprite leftButton;
private Sprite rightButton;
// 背景用
private Texture bgImg;
private Sprite bg;
// デバッグ用ワールド座標軸
private ShapeRenderer shapeRenderer;
// ねこアニメーション
private Animation animLeft;
private Animation animRight;
private Animation animIdleLeft;
private Animation animIdleRight;
private float stateTime = 0;
// 猫座標
private Vector2 pos;
// 状態
private int state;
private int dir;
// UVスクロール用
private float scrollCounter;
@Override
public void create() {
touchPoint = new Vector3();
batch = new SpriteBatch();
font = new BitmapFont();
img = new Texture(Gdx.files.internal("neko.png"));
pos = new Vector2();
music = Gdx.audio.newMusic(Gdx.files.internal("mixdown.mp3"));
music.setLooping(true);
music.setVolume(0.3f);
music.play();
bgImg = new Texture("bg.png");
bgImg.setWrap(TextureWrap.Repeat, TextureWrap.Repeat);
bg = new Sprite(bgImg, 0, 0, LOGICAL_WIDTH, LOGICAL_HEIGHT);
bg.setU(0);
bg.setU2(5);
bg.setV(0);
bg.setV2(5);
scrollCounter = 0.0f;
camera = new OrthographicCamera();
camera.position.x = 0;
camera.position.y = LOGICAL_HEIGHT/2 - 16*2;
viewport = new FitViewport(LOGICAL_WIDTH, LOGICAL_HEIGHT, camera);
uiCamera = new OrthographicCamera();
uiCamera.setToOrtho(false, LOGICAL_WIDTH, LOGICAL_HEIGHT);
uiViewport = new FitViewport(LOGICAL_WIDTH, LOGICAL_HEIGHT, uiCamera);
shapeRenderer = new ShapeRenderer();
// アニメーション情報構築
TextureRegion[] split = new TextureRegion(img).split(16, 16)[0];
TextureRegion[] mirror = new TextureRegion(img).split(16, 16)[0];
for (TextureRegion region : mirror)
region.flip(true, false);
animRight = new Animation(0.1f, split[2], split[3], split[4]);
animLeft = new Animation(0.1f, mirror[2], mirror[3], mirror[4]);
animIdleRight = new Animation(0.5f, split[0], split[1]);
animIdleLeft = new Animation(0.5f, mirror[0], mirror[1]);
stateTime = 0;
// 移動ボタン
leftButton = new Sprite(img, 0, 16*2, 16*3, 16*2);
rightButton = new Sprite(img, 16*3, 16*2, 16*3, 16*2);
leftButton.setPosition(8, 0);
rightButton.setPosition(8 + 16*3, 0);
// ログ情報取得
Gdx.app.setLogLevel(Application.LOG_DEBUG);
}
@Override
public void resize(int width, int height) {
Gdx.app.log(LOG_TAG, "risize");
viewport.update(width, height);
uiViewport.update(width, height);
}
private void reset() {
pos.set(0, 0);
}
private void left() {
dir = DIR_LEFT;
state = STATE_MOVE;
pos.x -= MOVE_SPEED;
}
private void right() {
dir = DIR_RIGHT;
state = STATE_MOVE;
pos.x += MOVE_SPEED;
}
private Animation currentAnim() {
Animation anim = null;
if (state == STATE_MOVE) {
if (dir == DIR_LEFT) {
anim = animLeft;
} else {
anim = animRight;
}
} else {
if (dir == DIR_LEFT) {
anim = animIdleLeft;
} else {
anim = animIdleRight;
}
}
return anim;
}
private void update() {
float deltaTime = Gdx.graphics.getDeltaTime();
if (Gdx.input.isKeyPressed(Input.Keys.R)) {
reset();
}
state = STATE_IDLE;
leftButton.setColor(Color.WHITE);
rightButton.setColor(Color.WHITE);
if (Gdx.input.isTouched()) {
float x = Gdx.input.getX();
float y = Gdx.input.getY();
uiViewport.unproject(touchPoint.set(x, y, 0));
Rectangle leftBounds = leftButton.getBoundingRectangle();
Rectangle rightBounds = rightButton.getBoundingRectangle();
// コメントを外せば、ボタンの矩形情報、タッチ座標などが得られる。
// String s0 = String.format("rawTouch(%f,%f)", x, y );
// String s1 = String.format("leftBoudns(%f,%f,%f,%f)",
// leftBounds.x,
// leftBounds.y,
// leftBounds.width,
// leftBounds.height);
// Gdx.app.log(LOG_TAG, s0);
// Gdx.app.log(LOG_TAG, s1);
// Gdx.app.log(LOG_TAG, "touchPoint(" + touchPoint.x + "," + touchPoint.y + ")");
if (leftBounds.contains(touchPoint.x, touchPoint.y)) {
left();
//Gdx.app.log(LOG_TAG, "left!");
leftButton.setColor(Color.GRAY);
} else if (rightBounds.contains(touchPoint.x, touchPoint.y)) {
right();
//Gdx.app.log(LOG_TAG, "right!");
rightButton.setColor(Color.GRAY);
}
}
//String info = String.format("fish pos(%f,%f)", fishpos.x, fishpos.y);
stateTime += deltaTime;
// コメントを外すと、UVスクロールが見れる。
// bg.setU(scrollCounter);
// bg.setV(scrollCounter);
// bg.setU2(scrollCounter + 5.0f);
// bg.setV2(scrollCounter + 5.0f);
// scrollCounter += 0.05f;
// if (scrollCounter > 5.0f)
// scrollCounter = 0.0f;
}
private void draw() {
Gdx.gl.glClearColor(0, 0, 1, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
// BGカメラセットアップ
uiCamera.update();
batch.setProjectionMatrix(uiCamera.combined);
batch.begin();
bg.draw(batch);
batch.end();
// ゲームカメラセットアップ
camera.update();
batch.setProjectionMatrix(camera.combined);
batch.begin();
// ネコの描画
boolean loop = true;
float width = 16;
float height = 16;
Animation nekoAnim = currentAnim();
batch.draw(nekoAnim.getKeyFrame(stateTime, loop),
pos.x, pos.y,
width, height);
batch.end();
// ワールド座標軸を描画する。
shapeRenderer.setProjectionMatrix(camera.combined);
shapeRenderer.begin(ShapeRenderer.ShapeType.Line);
shapeRenderer.setColor(1, 0, 0, 1);
shapeRenderer.line(-1024, 0, 1024, 0);
shapeRenderer.setColor(0, 1, 0, 1);
shapeRenderer.line(0, -1024, 0, 1024);
shapeRenderer.end();
// UIカメラセットアップ
uiCamera.update();
batch.setProjectionMatrix(uiCamera.combined);
// UIの描画
batch.begin();
leftButton.draw(batch);
rightButton.draw(batch);
//font.draw(batch, info, 0, 40);
batch.end();
}
@Override
public void render () {
update();
draw();
}
@Override
public void dispose() {
music.dispose();
batch.dispose();
font.dispose();
img.dispose();
bgImg.dispose();
shapeRenderer.dispose();
}
}
今回の記事のアセット一覧
プログラムを動かす前に、テクスチャやBGMをダウンロードして、 下記に配置してください。
${PROJECT_ROOT}/android/assets/
テクスチャ
背景
おわりに
今回は、「 ログ出力」と「スプライトアニメーション」、 そして「バーチャルパッドの作り方」を学びました。 次回 は、画面遷移について解説します。 もしかしたら、ゲームっぽいものに仕上げるかも知れないです。
参考書籍
変更履歴
2014/6/21
- ターゲット解像度を変更(256x192 => 256x144)
- 背景の画像を変更し、テクスチャラップ導入
- ボタンの画像を変更し、よりボタンらしい挙動をするようにした
注釈
1libGDXが使用しているOpenGLでは、すべての2次元座標系は左下が原点となるのが原則のようです。 libGDXが左下が原点なのものこの仕様に影響されているものと思われます。 UVも実は左下が原点なのですが、VRAMへ転送する際に上下が反転されるらしく、 結果的に左上が原点となるようです。