ざる魂

真似ぶ魂、学ぶの本質。知られざる我が魂

libGDX入門 その02 カメラとビューポート

はじめに

libGDX を勉強するついでに解説記事を書く シリーズ 2回目です。

前回 は、プロジェクトを作って、以下のことを学びました。

  • 画像の表示
  • キーボードによる操作
  • BGMの再生
  • 効果音の再生

今回は次のことを学びます。

  • 物理画面に依存しない画面表示
  • タッチ処理

物理画面に依存しない画面表示

物理画面とは何でしょうか?ここでは次のように定義します。

  • スマホやタブレット画面解像度
  • デスクトップのウィンドウサイズ

Androidには様々な画面サイズがありますし、 iPhoneもモデルチェンジ毎に画面サイズが変化しています。 これら様々な画面サイズのことを考慮しないと、 意図した通りの画面が表示できません。

ゲームを作るときの基本として、物理画面でプログラムしないというのがあります。 物理的なディスプレイのサイズに依存した座標管理をすると、 移植性が下がり仕様変更に弱くなったり、 端末毎の画面サイズの違いを吸収できないプログラムになってしまいます。

例えば横スクロールアクションを作ったときに、 Aさんの画面は小さいから敵の動きがところ狭しとなるところが、 Bさんの端末だと画面が大きいからフィールドが遠くまで見わたせて楽々プレイできる、 なんてことが発生します。端末によって難易度が変わってくるのです。

https://zarudama.github.io/img/libgdx-beginner/2/screen2.png
端末の小さいAさんは、端末の大きいBさんより不利になる

実は既にこの問題は、私の手元で発生しています。 下記は、Nexus7(2013)で表示したサンプルの画面です。

http://zarudama.github.io/img/libgdx-beginner/2/004.png
nexus7の画面
/img/libgdx-beginner/2/003.png
PC版の画面

Nexus7版は、PCの画面と全然違いますね。キャラクターや文字が非常に小さくなってます。 これはPC版が640x480の解像度なのに対して、Nexus7版は1920x1200の解像度で表示しているからです。 この状態でゲームを作ってしまったら全然別ものになってしまいますよね。 ちなみにMac持ってないのでiOS系ではどうなるかわかりません。

というわけで、どの端末でも公平に画面表示できるようにひと工夫必要になってきます (こういう処理は、ゲームづくりの序盤でやっておかないと、あとから変更するのは大変なので さっさと済ませておきたいことのひとつですね)。

カメラとビューポート

ではどうやってこの問題を解決するか。それにはカメラとビューポートを使用します。

カメラとは、ゲームの世界を現実世界のディスプレイに届けるためのオブジェクトです。

ビューポートとは、カメラの捉えたゲームの世界を、ディスプレイのどこに表示するかを 決める枠(矩形領域)のことです。

https://zarudama.github.io/img/libgdx-beginner/2/screen3.png
カメラとビューポート

ビューポートは、ディスプレイサイズと一致しているわけではないことに注意してください。 ゲームのサンプルなどだと一致していることが多いですが、 今回のように様々なディスプレイサイズに対応させる場合は、 一致しなくなることの方が多くなるはずです。また、他の使い方としては、 カメラを2つ用意して一方はゲーム画面、 一方は小さな枠で別のシーンを表示するなんてこともできるかもしれません(やったことないですが)。

カメラを使うことにより、カメラとして定義した論理空間でゲームを制御できます。 この空間でやりとりすれば、あとはlibGDXがよろしく画面に表示してくれるわけです。 カメラでできることを列挙してみます。

  • 物理的な画面サイズを気にせず、自分の定義した画面サイズでゲームを構築できる
  • ズームイン、ズームアウト、画面を回転させたり、揺らしたりなど、画面全体にかかるエフェクトが簡単にできる
  • カメラを動かすことで、スクロール処理が簡単に実装できる

カメラを導入することでこのような自由が手に入るわけですが、 その代償として操作が複雑になってしまうのも事実です。 コーディング中は、今自分がどこの座標系で何を操作しているかを常に意識する必要があります。 ちょっと大袈裟ですが、慣れれば大したことありません。またこの考えはそのまま3Dプログラミングにも繋がります。

座標系には、以下の種類があります。

  • ワールド座標系。ゲームオブジェクトを置く論理空間。画面サイズは気にしなくて良い。
  • カメラ座標系。ワールドのゲームオブジェクトをカメラの枠での座標系で測りなおした座標系。真ん中が原点となる。
  • ビューポート座標系。左下を原点とした座標系。
  • スクリーン座標系。Android左上を原点とした最終的な座標系。タッチ座標などはOSからこの座標系の値が得られる。

ビューポート座標系とスクリーン座標系は他では別の呼びかたかもしれません。 座標系を意識する例を示します。

たとえば、画面をタッチして、その座標にキャラクターが向って行くのであれば、 タッチ操作で得られた座標(スクリーン座標系)をワールド座標系に変換する必要があります。

逆にワールドに落ちているコインなどのアイテムを画面UIのスコア表示に 吸いこまれるようなエフェクトをかけるときは、ワールドからスクリーンへの変換が必要になるでしょう。

カメラを使用しなくてもゲームは作れますが、いろいろと応用が効くので、 パズルゲームのような固定画面のゲームしか使わない予定であっても、 使いかたに慣れておいたほうが良いと思います。

実際のコーディング

講釈が多くなってしまいました。実際のコーディングに進みます。 まずゲームに使用する画面解像度を決定します。 これは、物理的なサイズではなく、論理的なものです。 今回は下記のように定義しました。

  • 横800 x 縦480

横長の割と無難なサイズです。

カメラの導入

では早速カメラを導入してみましょう。 前回のソースコードに手を入れていきます。 前回のサンプルでは、画面サイズはデフォルトのままでしたが、 今回はターゲットサイズを横800 x 縦480に設定したので、 createメソッド内でカメラを下記のように定義します。

  OrthographicCamera camera;

  @Override
  public void create () {
    
    camera = new OrthographicCamera(800, 480);
    
  }

フィールドに camera を追加しています。 ちなみになぜこんな長たらしい名前がついてるのかわかりませんが、 2D用のカメラは、 OrthographicCamera クラスといいます。 libGDXのソースコードを見渡しましたが、他に2D用のカメラは見当たらなかったので OrthographicCamera は2D専用と思って問題ないでしょう。

続いて、描画にカメラを反映させるため、 render メソッドを変更します。

@Override
public void render() {
    :
  Gdx.gl.glClearColor(1, 0, 0, 1);
  Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
  camera.update(); // 追加 #1
  batch.setProjectionMatrix(camera.combined); // 追加 #2
  batch.begin();
  sprite.draw(batch);
    :
}

#1 でカメラ自身の座標計算(行列演算)をし、 #2 でその結果をスプライトに適用させる準備をします。 この処理は定型処理なので、最初のうちは決めごとと思って問題ありません。

ここまでの実行結果は下記のとおりです。

https://zarudama.github.io/img/libgdx-beginner/2/005.png
とりあえずカメラ導入

なにか変です。画像のサイズが以前に比べて縦長になっています。なぜでしょうか?

ビューポートの導入

原因はビューポートの設定にあります。libGDXは、デフォルトのビューポート設定だと、 物理画面いっぱいに最終画像を引き伸ばします。 今回の場合、カメラの設定を 800x480 に設定したのに対し、 実際のウィンドウサイズが 640 x 480 のため歪みがでてしまったのです(横方向に潰れている)。

では、ビューポートはどのように設定すれば良いのでしょうか? libGDXは、OpenGLを直接操作する低レイヤのメソッドも用意してますので、 自分で直接ビューポートをやりくりすることもできます。 しかし、libGDXには、このような面倒な作業を請け負ってくれる、 高レイヤの ViewPort クラスがあります。 今回は、この便利クラスを使うことにしましょう。

フィールドに viewport を追加し、createメソッド内で ビューポートを定義します。

  OrthographicCamera camera;
  Viewport viewport;  // 追加
  @Override
  public void create () {
    
    camera = new OrthographicCamera(800, 480);
    viewport = new FitViewport(800, 480, camera); // 追加
    
  }

ViewPort はスーパークラスであり、このクラスを継承した様々なクラスがあります。 インスンタンスを設定する際には、用途に応じたViewPort継承クラスを指定する必要があります。 今回は、 FitViewport クラスを利用します。

続いて resize メソッドを追加します。 resize メソッドは、 ApplicationListener クラスのメソッドであり、 画面の向きが変わったり、アプリケーションが起動したタイミングなどで呼ばれます。

  @Override
  public void resize(int width, int height) {
      viewport.update(width, height); 
  }

ビューポートは画面の大きさが変化した時だけ設定すれば良いため、 resize メソッドから呼びだすだけでよく、 render メソッドなどで呼びだす必要はありません。

ここまでの実行結果は下記のとおりです。

https://zarudama.github.io/img/libgdx-beginner/2/006.png
とりあえずビューポートも導入

やった、歪みがなくなりました! しかし、ここでひとつおかしいことに気づかないでしょうか? カメラとビューポートのサイズには 800x480 を設定しました。 でも、このサンプルのウィンドウサイズは 640x480 です。なぜ表示できるんでしょうか?

このままでは、背景となる塗りつぶし色の赤と、ビューポートの枠の区別がつきにくため、 ビューポートの働きがわかりつらくなっています。 そこで、 FitViewPort の動きを分かりやすくするために、大きなサイズの画像を背景として表示してみます。

背景の追加

背景用に次の画像を「右クリ→名前をつけて保存」で保存してください。

https://zarudama.github.io/img/libgdx-beginner/2/bg.png
背景

保存したファイルを下記に追加してください。

~/dev/libgdxtest/android/assets/

下記のように変更します。

フィールド

  Texture bgImg; // 追加
  Sprite bg;     // 追加

create メソッド

  @Override
  public void create () {
      :
     // 追加
     bgImg = new Texture("bg.png");
     bg = new Sprite(bgImg);
     bg.setScale(2.0f, 2.0f);
     bg.setPosition(-400, -240);
      :

render メソッド

  @Override
  public void render() {
      :
	batch.setProjectionMatrix(camera.combined);
      batch.begin();
      bg.draw(batch); // 追加
      sprite.draw(batch);
      sprite2.draw(batch);
      :

dispose メソッド

  @Override
  public void dispose() {
       :
      bgImg.dispose();
  }

これで実行してみます。

http://zarudama.github.io/img/libgdx-beginner/2/007.png
背景を追加

するとどうでしょう?赤い枠が上下に出現しています。 これは、 FitViewPort がウィンドウサイズからビューポートのサイズを自動計算して 当初の縦横比を保ってくれるからなのです。

試しにウィンドウサイズを色々マウスでドラッグして変化させてみてください。

/img/libgdx-beginner/2/008.png
ウィンドウを横長にしてみた
/img/libgdx-beginner/2/009.png
ウィンドウを縦長にしてみた

こんな風に画面のサイズに応じて、ビューポートのサイズを動的に変化さてくれます。 画面の短い辺に対して最大のサイズを割りあて、長い辺に大してはその比率を調整するようですね。

もちろんNexus7の実機でもうまく表示できています。

/img/libgdx-beginner/2/nexus7.png
ちにみにこれはNexus7の2012年度版

このように、 FitViewPort クラスを使うことにより、どのような画面サイズであっても、 こちらの意図したとおりの画面比率で表示されるようになります。

ViewPort クラスには他にも様々な種類のクラスがあります。 いろいろ試してみてください。

カメラの位置調整

以上で、カメラとビューポートの導入を終えたのですが、まだおかしいところがあります。 どこでしょうか?

実はスプライト画像がひとつしか表示されてません。本来は2つ表示されているハズなのですが。 最初の時はこうでした。

/img/libgdx-beginner/2/003.png
PC版の画面(再掲)

何が起きているかというと、カメラの位置がおかしいのです。 カメラはスプライトと違って、真ん中が原点となります。 なので、ワールド原点を中心とした枠が表示されているのです。

/img/libgdx-beginner/2/010.png
カメラはワールド原点(0,0)に位置している

左上が(-400,240),右上が(400,240),右下が(400,-240),左下が(-400,-240)の枠となっています。 このために2つめのスプライトがちょうど右上の枠の外に位置しており、画面から消えてしまっているのです。

ワールド軸の描画

ここで、見た目をわかりやすくするためにワールドの座標軸を描画してみます。

インポート

import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

フィールド

  ShapeRenderer shapeRenderer; // 追加

create メソッド

  @Override
  public void create () {
      :
     // 追加
     shapeRenderer = new ShapeRenderer();
      :

render メソッド

  @Override
  public void render() {
      :
    // ワールド座標軸を描画する。
    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();
      :

dispose メソッド

  @Override
  public void dispose() {
       :
     shapeRenderer.dispose();
  }

これで実行してみます。

/img/libgdx-beginner/2/012.png
カメラをsetToOrthoした結果

X軸を赤、Y軸を緑で描画しています。 これで状況がよりわかりやすくなったと思います。

setToOrthメソッド

画面の原点を左下に調整するというのはよくあることなので、 専用のメソッドが用意されています。

そこで試しに下記のメソッドを追記してみましょう。

create メソッド

  @Override
  public void create () {
      :
     camera = new OrthographicCamera(800,480);
     camera.setToOrtho(false, 800, 480);    // 追加
      :
  }

Camera#setToOrtho メソッドは、カメラを右上に移動させて、左下に丁度ワールド原点がくるように調整するものです。 第1引数は、ydownといって、trueにするとY軸が反転されますが、通常はfalseで良いと思います。 第2引数と第3引数は、カメラのの幅と高さです。 setToOrth をコンストラクタに続けて指定する時は、 コンストラクタには、サイズ指定はいらないですね(今回は修正が面倒なのでそのまま)。

これを実行すると以下のように表示されます。

/img/libgdx-beginner/2/013.png
カメラをsetToOrthoした結果

無事カメラが移動されて、2つ目のスプライトも表示されるようになりました。

カメラを動かす

こんどはカメラをキーボードで動かしてみましょう。

render メソッドで下記のように追記します。

  @Override
  public void render() {
      if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) {
          //pos.x -= 1;            // 削除
          camera.position.x -= 2;  // 追加
      }
      if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {
          //pos.x += 1;            // 削除
          camera.position.x += 2;  // 追加
      }
      if (Gdx.input.isKeyPressed(Input.Keys.UP)) {
          //pos.y += 1;            // 削除
          camera.position.y += 2;  // 追加
      }
      if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) {
          //pos.y -= 1;            // 削除
          camera.position.y -= 2;  // 追加
      }

pos ベクトルへの処理を削除し、変わりに camera.position への処理を追加します。 これを実行すると以下のように表示されます。

/img/libgdx-beginner/2/014.png
カメラを動かしている様子

カーソルキーでカメラを動かしてみてください。 背景のBG画像があまり大きくないので端にいくと表示が変な見ためになるので注意です。

ここで、カメラが本当に動いているのか数値からも判断するため、 カメラの座標を表示してみます。

render メソッドで下記のように追記します。

  @Override
  public void render() {
    
    // カメラの座標の文字列を作って
    String info = String.format("cam pos(%f,%f)", camera.position.x, camera.position.y);


    batch.setProjectionMatrix(camera.combined);
    batch.begin();
        
    font.draw(batch, info, 0, 20); // 追加
        
    batch.begin();

座標は大体左したに表示されるように指定しています。

これを実行すると以下のように表示されます。

/img/libgdx-beginner/2/015.png
カメラの座標を表示してみる

左下に表示されてますね。ではカーソルキーでカメラを動かしてみてください。

/img/libgdx-beginner/2/016.png
カメラの座標文字がついてこない?!

あれ、期待した動きと違ってませんか? 私は違いました。 どう動いて欲しかったというと、画面にひっついて常に左下に表示して欲しいと思いました。

実は、このフォント描画はワールド座標に対して行なわれているのです。 フォント文字もワールドに存在するスプライトと同じオブジェクトとして 描画されているんですね。

これを画面にひっつかせて描画させるには、ひと工夫必要です。

UI用カメラの導入

答えは簡単です。もうひとつカメラを用意すれば良いのです。 UI専用のカメラを準備して、そのカメラは動かさずにおく。 これだけでオッケーです(libGDXのサンプルでもやっている方法です)。

フィールド

  OrthographicCamera uiCamera;  // 追加

create メソッド

  @Override
  public void create () {
      :
     // 追加
     uiCamera = new OrthographicCamera();
     uiCamera.setToOrtho(false, 800, 480);
      :

render メソッド

  @Override
  public void render() {
      :
    //font.draw(batch, info, 0, 20);
    //font.draw(batch, "Hello libGDX", 200, 400);
      :
    // render の一番最後で描画すること
    uiCamera.update();
	batch.setProjectionMatrix(uiCamera.combined);
    batch.begin();
    font.draw(batch, info, 0, 20);
    font.draw(batch, "Hello libGDX", 200, 400);
    batch.end();
  }

以前の font.draw はコメントアウトもしくは、削除します。 注意すべき点は、UIの描画物は render メソッド内の一番最後で描画するということです。 これは、画面上の一番上に描画するためです。

実行してみます。

/img/libgdx-beginner/2/017.png
UIカメラを追加してみた

おおお、無事希望どおりの動きになりました!

さて、ここで気になることがあります。 それは、画面サイズとして 800x480 を指定しているにも関わらず、 ウィンドウサイズが 640x480 のままだということです。

Windowサイズの変更

スマホなどの端末は、ハードウェアなので画面サイズを変更できませんが、 Desktop版は、ウィンドウなので、初期サイズを変更できます。

下記のファイルを編集すればオッケーです。

~/dev/libgdxtest/desktop/src/com/zarudama/libgdxtest/desktop/DesktopLauncher.java
public class DesktopLauncher {
	public static void main (String[] arg) {
		LwjglApplicationConfiguration config = new LwjglApplicationConfiguration();
		config.width = 800;  // 追加
		config.height = 480; // 追加
		new LwjglApplication(new LibGdxSample(), config);
	}
}

実行結果です。

/img/libgdx-beginner/2/018.png
UIカメラを追加してみた

画面サイズと論理サイズが一致したので赤い帯がなくなりましたね!

タッチ処理

いよいよ最後のお題です。私も疲れてきました^^。

前回は、キーボード入力の処理方法は学んだものの スマホ特有の処理、タッチ操作の処理方法は先送りにしていました。

指でタッチしているかどうかを検知するには、 Gdx.input.isTouched メソッドを使用します。

if (Gdx.input.isTouched(0)) {
    :
  // なにかの処理
    :
}

このメソッドの引数には番号が必要ですが、これはタッチした指の番号です。 最初にタッチした指の番号は0, 2番目にタッチした座標の番号は1という具合です。

タッチの座標を得るには、 Gdx.input.getX , Gdx.input.getY メソッドを使用します。

float x0 = Gdx.input.getX(0);
float y0 = Gdx.input.getY(0);

番号は先程の説明と同じ意味です。

ただしこの座標、注意が必要で、得られる座標は、画面左上を原点とします。

x座標は左から右、Y座標は上から下に伸びます。

/img/libgdx-beginner/2/screen.png
スクリーン座標系

この座標系は一般的にはスクリーン座標系などと呼ばれ、 最終的な絶対座標(物理座標)として使われるものですが、libGDXでは左下を原点とした 座標のためこのままでは使用できません。

ただし私たちは今回カメラを導入しています。なのでカメラの便利メソッドで簡単に変換できます。 こんな感じです。

  Vector3 touchPos = new Vector3();
  touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
  camera.unproject(touchPos); // #1
  targetPos.set(touchPos.x, touchPos.y);

#1 で、スクリーン座標で得た左上を原点とした座標をワールドの座標に変換します。

2013/06/19 追記 上記説明には、誤りがありました。 カメラだけを使用している場合は、 Camera#unproject() メソッドで良いのですが、 Viewport クラスを使用している場合は、 Viewport#unproject() メソッドを使用する必要があります。 従って今回は、 Viewport#unproject() を使用しなければなりません。 説明はこのままにしておきますが、ソースコードの方は訂正してあります。

この処理を if で囲ってあげれば判定処理の出来あがりです。

if (Gdx.input.isTouched(0)) {
  Vector3 touchPos = new Vector3();
  touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
  camera.unproject(touchPos); // #1
  targetPos.set(touchPos.x, touchPos.y);
}

Gdx.input.isTouched メソッドは、タッチし続けてれば、毎フレーム「真」になります。

他にタッチを検出するメソッドとして、 Gdx.input.justTouched メソッドがあります。 このメソッドは、ひとつ前のフレームにタッチしていた場合は、偽になります。 どういうことかというと、押しっぱなしにしてた場合は、 最初の一回しか真にならないということです。

例えば、下記のようなコードで、タッチした時だけ音を鳴らすことができます。

if (Gdx.input.justTouched()) {
    sound.play();
}

ちなみにjustTouchedと同等の機能のキーボード版のメソッドはありません。 恐らく、このメソッドの実装には、状態変数の保持が必要になってくるので、 そこをライブラリ側で実装してしまうと無駄が多くなるので、 ユーザー側に任せたのだと思います。

では、上記の説明をもとに、スプライトをタッチした場所へ移動させるコードを追加します。 下記のように修正してみてください。

インポート。

import com.badlogic.gdx.math.Vector3;

フィールド。

  Vector2 targetPos;

render メソッド。キーボード処理の直後に追加します。

  @Override
  public void render() {
       
    if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) {
        pos.y -= 1;
        camera.position.y -= 2;
    }
       :
    // ↓追加
    if (Gdx.input.justTouched()) {
         sound.play();
    }
    if (Gdx.input.isTouched(0)) {
      Vector3 touchPos = new Vector3();
      touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
      camera.unproject(touchPos);
      targetPos.set(touchPos.x, touchPos.y);
    }
    // ↑追加
    String info = String.format("cam pos(%f,%f)", camera.position.x, camera.position.y);
    // ↓追加
    pos.lerp(targetPos, 0.2f);
    sprite.setPosition(pos.x, pos.y);
    // ↑追加
       
  }

render メソッド内で実行している pos.lerp メソッドは、第一引数の座標 へ値を少しずつ近づけていく処理を実施します。「最初早くて後おそく」って感じの動きです。

では、実行してみます。

/img/libgdx-beginner/2/019.png
マウスでクリックすると、音を鳴らしながら移動します

これまでのコードを忠実に入力していれば、音をビヨーンとならしながら移動します。

以上で今回の解説は終了です。お疲れさまでした。

ソース

最後に今回のソースコードを掲載しておきます。

package com.zarudama.libgdxtest;

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.audio.Sound;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.PerspectiveCamera;
import com.badlogic.gdx.graphics.Camera;
import com.badlogic.gdx.graphics.Texture;
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.math.Vector2;
import com.badlogic.gdx.math.Vector3;
import com.badlogic.gdx.utils.viewport.FitViewport;
import com.badlogic.gdx.utils.viewport.FillViewport;
import com.badlogic.gdx.utils.viewport.StretchViewport;
import com.badlogic.gdx.utils.viewport.ScreenViewport;
import com.badlogic.gdx.utils.viewport.ExtendViewport;
import com.badlogic.gdx.utils.viewport.Viewport;
import com.badlogic.gdx.graphics.glutils.ShapeRenderer;

public class LibGdxSample extends ApplicationAdapter {
    SpriteBatch batch;
    BitmapFont font;
    Texture img;
    Sprite sprite;
    Sprite sprite2;
    Vector2 pos;
    Sound sound;
    Music music;
    float angle;
    OrthographicCamera camera;
    Viewport viewport;

    Texture bgImg;
    Sprite bg;
    ShapeRenderer shapeRenderer;

    OrthographicCamera uiCamera;

    Vector2 targetPos;

    @Override
    public void create () {
        batch = new SpriteBatch();
        font = new BitmapFont();
        img = new Texture("badlogic.jpg");
        sprite = new Sprite(img);
        sprite2 = new Sprite(img);
        pos = new Vector2();
        sound = Gdx.audio.newSound(Gdx.files.internal("jump.wav"));
        music = Gdx.audio.newMusic(Gdx.files.internal("mixdown.mp3"));
        music.setLooping(true);
        music.setVolume(0.5f);
        music.play();

        bgImg = new Texture("bg.png");
        bg = new Sprite(bgImg);
        bg.setScale(2.0f, 2.0f);
        bg.setPosition(-400, -240);

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

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

        shapeRenderer = new ShapeRenderer();

        targetPos = new Vector2();
    }

    @Override
    public void resize(int width, int height) {
        viewport.update(width, height);
    }

    @Override
    public void render() {
        if (Gdx.input.isKeyPressed(Input.Keys.LEFT)) {
            camera.position.x -= 2;
        }
        if (Gdx.input.isKeyPressed(Input.Keys.RIGHT)) {
            camera.position.x += 2;
        }
        if (Gdx.input.isKeyPressed(Input.Keys.UP)) {
            camera.position.y += 2;
        }
        if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) {
            camera.position.y -= 2;
        }
        if (Gdx.input.justTouched()) {
            sound.play();
        }
        if (Gdx.input.isTouched(0)) {
            Vector3 touchPos = new Vector3();
            touchPos.set(Gdx.input.getX(), Gdx.input.getY(), 0);
            viewport.unproject(touchPos);
            targetPos.set(touchPos.x, touchPos.y);
        }
        String info = String.format("cam pos(%f,%f)", camera.position.x, camera.position.y);

        pos.lerp(targetPos, 0.2f);
        sprite.setPosition(pos.x, pos.y);
        sprite.setScale((float) Math.sin(angle));
        angle += 0.04;

        sprite2.setRotation(angle);
        sprite2.setPosition(200, 300);
        sprite2.setRotation(angle);

        Gdx.gl.glClearColor(1, 0, 0, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
        camera.update(); // ワールドからスクリーンまでのマトリックスを生成する。
        batch.setProjectionMatrix(camera.combined);
        batch.begin();
        bg.draw(batch);
        sprite.draw(batch);
        sprite2.draw(batch);
        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();

        uiCamera.update(); // uiCameraを動かさないのであれば、必要ない。
        batch.setProjectionMatrix(uiCamera.combined);
        batch.begin();
        font.draw(batch, info, 0, 20);
        font.draw(batch, "Hello libGDX", 200, 400);
        batch.end();
    }

    @Override
    public void dispose() {
        sound.dispose();
        music.dispose();
        batch.dispose();
        font.dispose();
        img.dispose();
        bgImg.dispose();
        shapeRenderer.dispose();
    }
}

おわりに

面倒だけど重要な説明が終りました。 これで、端末の画面サイズを気にせずプログラミングできるようになりました。 次回 は、そろそろゲームっぽいものをつくろうかなと思います。 予定している内容は、

  • アニメーション処理
  • 画面遷移
  • 2DScene

などです。

参考URL

ビューポートについては下記にも詳しい内容があります。合せて読めば理解も深まると思います。

変更履歴

  • 2014/06/19 タッチ座標の取得方法に問題があったのを訂正した。