この記事ではゲームのエンジンと部品の作成から少し離れ、 libgdxライブラリを使ってゲームのプロトタイプ開発を素早く行う方法について説明します。
ここでは以下の内容について学びます:
これは一日で終わらせるプロジェクトなので自由にできる時間は非常に限られおり、ここでの目的は実際にゲームを作るのではなくゲーム作る技術を学ぶことです。
こうした目的のために、他のゲームからアイディアを借りてその技術的観点に重点を置くことにします。
Star Guardというゲームから多くのアイディアを拝借します。
これはVacuum Flowersが作成した小さなゲームです。
ゲームを取得して、内容を確認してください。
シンプルなスタイルで古典的なアーケードゲーム風の、とてもシンプルなシューティング型プラットフォーム・ゲームです。
キャラクターを操作して敵を倒しながら敵からの攻撃を回避し、ステージを進んでいくというアイディアのゲームです。
操作はとても簡単で、 矢印キーでキャラクターを左や右へ動かし、 Zキーでジャンプして Xキーでレーザーを撃ちます。ジャンプボタンを長く押すと、キャラクターはより高くジャンプします。空中で方向を変えたり、レーザーを撃つこともできます。
後ほど、これらの操作をAndroid上で行えるようにする方法を見ていきます。
The next steps (2 and 3) can be skipped over as we will already have this taken care of because of the functioning game.
では、作業を始めます。ゲーム作成にはlibgdx ライブラリを使用します。なぜ libgdxなのでしょう? libgdxは、基礎的技術をそれほど多く知らなくてもゲーム開発を簡単にできる最高のライブラリです(私の意見としては)。 これを使うとデスクトップ上でゲームを作成して、変更を加えずにAndroid へ配布できます。 ゲームで使われる全ての要素が用意されており、特定の技術やハードウェアを扱う難しい部分を意識しなくて済むようになります。 実際に使ってみるとそれが良く分かります。
Aurelien Ribon氏が作成した LibGdx プロジェクトセットアップUI ツールを使用します。
libgdxの wiki ページに記載されている手順に従って作業をすることを推奨しますが、 I will give you a quick run-down on how it can be done for Star Guard.
最初に、実行する jar ファイルを http://libgdx.badlogicgames.com/nightlies/dist/gdx-setup-ui.jarからダウンロードします。
ファイルをダブルクリックして起動します。起動しない場合は、jarファイルをダウンロードしたディレクトリでコマンドラインから java -jar gdx-setup-ui.jar
を実行してみてください。
初期設定画面が表示されます。
Create ボタンをクリックすると、以下画像のような値が入力された画面が表示されます。
プロジェクトの適切な出力先を選ぶようにしてください。
丸で囲んだボタンをクリックすると自動的に最新の libgdx (安定版) がダウンロードされるので、 LibGDX の文字が緑色に変わるまで待ちます。
緑色に変わったら、右下にある Open the generation screen ボタンをクリックします。
次の画面では、 Launch ボタンをクリックして完了するまで待ちます。完了すると “All done!”と表示されます
プロジェクトの準備ができると、作成時に選んだディレクトリからEclipse にインポートできます。
Eclipseで ファイル -> インポート... -> 一般 -> 既存のプロジェクトをワークスぺースへ
の順にクリックして、プロジェクトのあるディレクトリを開きます。
完了
をクリックすると、インポートできるプロジェクトの一覧が表示されます。それらを全てインポートします。
完了をクリックすると、設定は全て完了です。
最初にライブラリをダウンロードする必要があります。
http://libgdx.badlogicgames.com/nightlies/へ行って libgdx-nightly-latest.zip
ファイルをダウンロードして解凍します。
Eclipseで簡単なJavプロジェクトを作成します。プロジェクトの名前は star-assault
とします。
デフォルト設定のままでプロジェクトを作成し、できたプロジェクトを右クリックして新規->フォルダーと選んでlibs
という名前のディレクトリを作成します。
解凍された libgdx-nighly-latest
ディレクトリから、gdx.jar
ファイルを新しく作成した libs
ディレクトリへコピーします。
また。gdx-sources.jar
ファイルも libs
ディレクトリへコピーします。
このファイルは解凍されたgdxディレクトリのsources
サブディレクトリ内にあります。
単純に、Eclipse上でこのjarファイルをディレクトリにドラッグするだけでも同じことができます。
エクスプローラーやfinderやその他の方法でファイルをコピーする場合は、後でF5キーを押してeclipseプロジェクトをリフレッシュするのを忘れないでください。
ディレクトリ構造は以下の画像のようになります:
gdx.jar
を依存ファイルとしてプロジェクトに追加します。プロジェクトを右クリック して プロパティを選ぶとこれを実行できます。
表示された画面で Java ビルドパスを選んでライブラリー タブをクリックします。
JAR 追加…をクリックして libs
ディレクトリを開き、gdx.jar
を選んで OKをクリックします。
gdxのソースコードにアクセスしてゲームを簡単にデバッグできるようにするため、gdx.jarファイルにソースを追加すると良いでしょう。
これを行うには、gdx.jar
ノードを展開して ソース添付を選び、編集…をクリックして、それからワークスペース… をクリックし、 gdx-sources.jar
を選んで、全てのポップアップウィンドウが閉じられるまで OKをクリックします。
libgdx 使用時のプロジェクト設定に関する全ドキュメントは公式の wikiにあります。
このプロジェクトはゲームのコアプロジェクトです。 コアプロジェクトにはゲームのメカニズム、エンジン、全てが含まれています。
今回は、動作対象とする2つのプラットフォームで起動させるため、さらに二つのプロジェクトを作成する必要があります。
一つはAndroid 用のプロジェクトでもう一つはデスクトップ用のプロジェクトです。
これらのプロジェクトはとても単純なもので、それぞれのプラットフォームでゲームを動作させるために必要な依存ファイルのみを含んでいます。
これらはメインメソッドを持つクラスとして考えてください。
なぜこれらのプロジェクトを分ける必要があるのでしょうか?
libgdx は基礎となるOS部分とやり取りする難しい処理 (グラフィック、音声、ユーザー入力、ファイル i/o,など。)をユーザーに意識させないようにするために各プラットフォームで独自の実装を持っており、対象のプラットフォームで必要となる実装のみをインクルード (bindings)する必要があります。
また、アプリケーションのライフサイクルやゲーム素材の読み込み(画像や音声などの読み込み)やアプリケーションのその他一般的な部分がとても簡略化できるため、
プラットフォーム固有の実装は異なるJARファイル内に格納され、対象とするプラットフォームで必要なもののみインクルードされます。
前回の手順で行ったようにシンプルなJavaプロジェクトを作成して名前を star-assault-desktop
とします。
また同様に、 libs
ディレクトリを作成します。この時、ダウンロードしたzip
ファイルにある以下の jar
ファイルが必要です:
gdx-natives.jar
,
gdx-backend-lwjgl.jar
,
gdx-backend-lwjgl-natives.jar
.
前回作成したプロジェクトと同じように、これらjar
ファイルを依存ファイルとしてプロジェクトに追加します。(プロジェクトを右クリック -> プロパティ -> Java ビルドパス -> ライブラリ -> JAR 追加、と進んでこれら三つの JARファイルを選んでOKをクリックします)
また star-assault
プロジェクトを依存関係に追加する必要もあります。これを行うには、 プロジェクト タブをクリックして、 追加をクリックし、 star-assault
プロジェクトにチェックを入れて OKをクリックします。
重要! We need to make the star-assault
project a transitive dependency, meaning that dependencies for this project to be made dependencies of projects depending on this. これを行うためには: メインプロジェクトを右クリック -> プロパティ -> Java ビルドパス -> 順序およびエクスポート -> gdx.jar ファイルにチェックを入れる、そしてOKをクリックします。
このためには、 Android SDK をインストールする必要があります。
Eclipse上で Android プロジェクトを新規に作成します: ファイル -> 新規 -> プロジェクト -> Android アプリケーション・プロジェクト.
名前はstar-assault-android
とします。 ビルドターゲットについては、 “Android 2.3″にチェックを入れます。
パッケージ名を net.obviam
もしくはあなたの好きな名前に変更します。
その下の “Create Activity” には StarAssaultActivity
と入力します。 完了をクリックします。
プロジェクトのディレクトリを開いてlibs という名前のサブディレクトリを作成します(この作業はEclipse上から行えます)。
nightly zip
の中にある、gdx-backend-android.jar
ファイルと armeabi
ディレクトリと armeabi-v7a
ディレクトリを
新しく作成した libs
ディレクトリ内に置きます。
Eclipse上で、 プロジェクトを右クリック -> プロパティ -> Java ビルドパス -> ライブラリ -> JAR 追加、と進んで gdx-backend-android.jar
を選んで OKをクリックします。
再度 JAR 追加 をクリックして、メインプロジェクト (star-assault
)配下の gdx.jar をクリックして OKをクリックします。
プロジェクト タブをクリックし、追加をクリックし、 メインプロジェクトにチェックを入れて OK を2回クリックします。
これでディレクトリ構造は以下のようになります:
重要!
ADT リリース17 以降の場合、 the gdx jar
ファイルをエクスポートをするために明示的にマークする必要があります。
これをするためには
Android プロジェクトを右クリック
プロパティを選ぶ
Java ビルドパスを選ぶ (手順 1)
順序およびエキスポートを選ぶ (手順 2)
gdx.jar
、gdx-backend-android.jar
、 メインプロジェクト (star-assault
)などのような、全ての参照にチェックを入れる(手順 3)。
以下の画像のような状態にしてください。
また、この問題に詳細情報についてはここを参照してください。
ゲームはデスクトップとAndroidで同一のものですが、それぞれ別のプロジェクトで分けてビルドする必要があるので、画像、音声その他データを共有ディレクトリでまとめて保存したいですね。
メインプロジェクトはAndroidとデスクトップの両方でインクルードされるので、理想としてはメインプロジェクトが共有ディレクトリになりそうですが、
Android ではこれら全てのファイルを保存するディレクトリについて厳格なルールがあるため、そのルールに合わせなければなりません。
Android 用プロジェクト内に自動的に作成されるassets
ディレクトリが、その条件に合致するディレクトリになります。
eclipse には、ディレクトリをlinux/macにおけるシンボリックリンクやWindowsにおけるショートカットとしてリンクすることができます。
Android 用プロジェクトのassets
ディレクトリをデスクトップ用プロジェクトにリンクするには、以下のようにします:
star-assault-desktop
プロジェクトを右クリック -> プロパティ -> Java ビルドパス -> ソース タブ -> ソースのリンク… -> 参照… -> と進んで star-assault-android
プロジェクトのasssets
ディレクトリを開き、 完了をクリックします。
また、 assets
ディレクトリを参照するのではなく、変数…の拡張をすることもできます。
プロジェクトファイルシステムの独立させられるのでお勧めです。
また assets
ディレクトリがソースフォルダとしてインクルードされるようにしてください。これをするには、Eclipse上から assets
ディレクトリで右クリック し(デスクトップ用プロジェクトで)、ビルドパス -> ソース・フォルダーとして使用を選びます。
この段階で設定の準備ができたのでゲーム作りに進めます。
コンピュータアプリケーションとは、マシン上で動作するソフトウェアです。起動し、何らかの処理を行い (何もしないこともあります)、何らかの方法で停止できます。 コンピュータゲームとは、この“何らかの処理を行う”の部分でゲームに関する処理を行うタイプのアプリケーションです。 起動と終了については、全てのアプリケーションで共通する処理です。 また、ゲームは連続ループに基づいた非常に単純なアーキテクトを持っています。 詳細については アーキテクチャ と ループ を参照してください。
libgdx のおかげで、劇場でおこわなれる演劇のように、ゲームを場面ごとにつなぎ合わせることができます。 必要なので、ゲームを演劇として考えることだけです。 我々はステージ、演者、彼らの役割と動作を定義しますが、実際の操作についてはプレイヤーに委ねます。
ゲーム/プレイを設定するには、以下の手順を行う必要があります:
非常に簡単に見えますが、実際に簡単です。彼らを画面上で表現するための概念と要素を実装します。
ゲームを作成するには、単純に一つのクラスだけが必要です。
それでは、star-assault
プロジェクト内でStarAssault.java
を作成しましょう。2つの例外を除き、全てのクラスはこのプロジェクト内に作成されます。
package net.obviam.starassault; import com.badlogic.gdx.ApplicationListener; public class StarAssault implements ApplicationListener { @Override public void create() { // TODO Auto-generated method stub } @Override public void resize(int width, int height) { // TODO Auto-generated method stub } @Override public void render() { // TODO Auto-generated method stub } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { // TODO Auto-generated method stub } }
gdx
のApplicationListener
を実装すると、 eclipse は実装に必要なメソッド・スタブを生成します。
これらは全て、アプリケーションのライフサイクルの実装で必要なメソッドです。
たったこれだけのコードでAndroidやデスクトップにおいてOpenGL コンテキストの初期化やその他の面倒な(難しい)作業ができると思えば、とてもシンプルなものと言えるのではないでしょうか。
create()
メソッドがまず最初に呼び出されます。このメソッドは、アプリケーションの準備ができてゲーム素材の読み込みとステージ・演者の作成ができるようになったタイミングで呼び出されます。
全ての資材が届いて準備が整った後に、劇場内で演劇用のステージを作るようなものだと考えてください。
劇場がどこにあるのか、どのような手段で資材を入手するのか、によってはコードの構造は難解なものになってしまいます。
手で運ぶことも、飛行機で運ぶことも、トラックで運ぶこともできますが…我々はそれを知る必要はありません。
我々は劇場内で資材の準備をして組み立てるだけです。
libgdx が我々に代わって、プラットフォームに関係なく資材を出荷して劇場まで届けてくれます。
resize(int width, int height)
メソッドは、描写可能なsurface のサイズが変更される度に毎回呼び出されます。
このおかげで、劇が始まる前にちょっとした再配置をすることができるようになります。
例えは、これはゲームが動作しているウィンドウのサイズが変更されたタイミングで発生します。
全てのゲームの肝となるのは render()
メソッドです。このメソッドは、無限ループと同等の機能を持っています。
我々がゲームが終了したのでプログラムを終了させたいと決めるまで、このメソッドは継続的に呼び出され続けます。
これが、処理のおける劇にあたります。
注意:コンピューターの場合、ゲームの終了とプラグラムの終了は同じではありません。
ゲームの終了とは、ただの状態を表すものです。ゲームが終了した状態でも、プログラムは動作したままです。
一時停止によって劇を中断したり、中断した劇を再開することももちろんできます。
pause()
メソッドは、デスクトップやAndroidで、アプリケーションがバックグラウンドへ入る度に呼び出されます。
アプリケーションがフォアグラウンドに戻ってきた時、それは再開されてresume()
メソッドが呼び出されます。
ゲームが完了してアプリケーションが閉じされた時、dispose()
が呼び出されます。このタイミングで必要なクリア処理を行います。
これは劇が終わって観客が去り、ステージが片付けられた時と似たようなものです。
もう元に戻すことはできません。ライフサイクルの詳細については ここを参照してください。
では、実際にゲームを作る手順を開始しましょう。まず最初の目標は、キャラクターが動くことができるゲーム内の世界を作ることです。
ゲーム内世界はステージによって構成され、各ステージは 地形によって構成されます。地形とは、キャラクターが通り抜けることができないブロックのようなものです。
今のところは、ゲーム内で演者とエンティティを識別するのは簡単です。
キャラクター(彼をボブと呼びましょう – libgdx にはボブを使ったチュートリアルがあります)と、世界を組み上げるブロックを用意します。
Star Guard のゲームをプレイすると、ボブはいくつかの状態を持っていることが分かります。 どこもタッチしない時、ボブは待機状態になります。 ボブは移動状態(両方の向きの)にもなり、 ジャンプ状態にもなります。 さらに、死亡状態になった時、ボブは何もできなくなります。 ボブは常に、4つある状態のうちのどれか一つになります。 他にも状態はありますが、今のところは置いておきます。
ボブの状態:
もう一つの演者はブロックです。簡単にするため、ブロックだけにします。このステージは二次元空間に配置されたブロックで構成されます。
簡単にするため、ブロックにはただの格子を使用しています。
Star Guard にボブとブロックの要素を追加すると、以下のような見た目になります。:
上の画像はオリジナルのもので、下の画像はゲーム内世界の表現を加えたものです。
世界を作るといっても、我々が認識できる尺度のシステムにする必要があります。
簡単にするために、世界にあるブロックの横幅と高さはそれぞれ1単位としましょう。
さらに簡単にするために単位をメートルに換算することもできますが、ボブのサイズが0.5単位のためメートル換算では0.5メートルになってしまいます。
ゲーム内世界における1単位を4メートルとすれば、ボブのサイズは2メートルになります。
例えばボブが走る速度を計算する時にどういった処理をするのか知っておく必要があるので、これが重要になります。
それでは、ゲーム内の世界を作成しましょう。
我々がメインで使用できるキャラクターは ボブ
です。
Bob.java
クラスは以下のようになります:
package net.obviam.starassault.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Bob { public enum State { IDLE, WALKING, JUMPING, DYING } static final float SPEED = 2f; // unit per second static final float JUMP_VELOCITY = 1f; static final float SIZE = 0.5f; // half a unit Vector2 position = new Vector2(); Vector2 acceleration = new Vector2(); Vector2 velocity = new Vector2(); Rectangle bounds = new Rectangle(); State state = State.IDLE; boolean facingLeft = true; public Bob(Vector2 position) { this.position = position; this.bounds.height = SIZE; this.bounds.width = SIZE; } }
#16-#21 行目ではボブの属性を定義しています。これらの属性の値によって、任意のタイミングでのボブの状態が決まります。
position
– ゲーム内世界におけるボブの位置。 これは世界座標で表されます (詳細は後ほど)。
acceleration
– これはボブがジャンプする時の加速度を決定します。
velocity
– ボブが移動するために計算され、使用されます。
bounds
– ゲーム内の各要素は 境界ボックスを持っています。これはrectangleと同様のもので、ボブが壁の中を走っていないか、弾丸に当たっていないか、敵に攻撃が当たったかを知るためにあります。
これは当たり判定のために使われます。 Think of playing with cubes.
state
– 現在のボブの状態。 歩く動作が実行された時、state は WALKING
となり、このstate に基づいて画面に描写する画像を判断します。
facingLeft
– ボブの向きを表します。簡単な 2D プラットフォーム・ゲームでは、向きは左と右の2つだけあります。
#12-#15 行目では、ゲーム内世界での速度や位置を計算するために使用するいくつかの定数を定義しています。これらの数値は後で調整します。
また、ゲーム内世界を組み上げるためのブロックも必要です。
Block.java
クラスは以下のようになります:
package net.obviam.starassault.model; import com.badlogic.gdx.math.Rectangle; import com.badlogic.gdx.math.Vector2; public class Block { static final float SIZE = 1f; Vector2 position = new Vector2(); Rectangle bounds = new Rectangle(); public Block(Vector2 pos) { this.position = pos; this.bounds.width = SIZE; this.bounds.height = SIZE; } }
ブロックとは、ゲーム内世界に置かれる rectangle のようなものです。これらブロックを使って地形を組み上げます。簡単なルールが一つあります。ブロックを通り抜けることはできません。
libgdx メモ
libgdxのVector2
型を使っているということに気づいたかもしれません。
Vector2
にはユークリッドベクトルを使うのに必要な機能が全て揃っているので、作業がかなり楽になります。
我々はベクトルを使ってエンティティの位置決めと速度の計算と移動を行います。
現実世界のように、我々のゲーム内世界も次元を持っています。フラットな部屋について考えてみてください。部屋には横幅と高さと奥行きがあります。
それを二次元にすると奥行きが削除されます。その部屋が5メートルの横幅と3メートルの高さの場合、その部屋はメートル法で表記されていると言うことができます。
横幅が1メートルで高さが1メートルのテーブルを部屋の中央に置いた状況は簡単にイメージできます。
我々はテーブルを通り抜けることはできません。テーブルを越えて進むには、テーブルの上にジャンプして1メートル進み、飛び降りる必要があります。
複数のテーブルを使えば、部屋の中にピラミッドといくつかの不思議なデザインを作成することができます。
star assaultのゲーム内世界において、 世界とは現実世界の部屋、ブロックとは現実世界のテーブル、単位とは現実世界のメートルを表します。
10km/時間の速さで走った場合、それは 2.77777778 メートル / 秒 ( 10 * 1000 / 3600)の速度に変換されます。
これをStar Assault世界の座標に変換するには、10km/時間 の速度に近づけるために 2.7 単位/秒 を使用します。
世界座標系での境界ボックスとボブを表した以下の図を見てください。
赤い四角は、ブロックの境界ボックスです。緑の四角は、ボブの境界ボックスです。
空の四角は、何もない空間です。
この格子はあくまで参考程度のものです。
この世界の中でシミュレーションを作成します。
この座標系の開始点は左下になるため、左方向に 10.000単位/時間 の速度で歩くとボブの位置のX座標は毎秒2.7ユニット減ります。
Also note that the access to the members is package default and the models are in a separate package. エンジンからそれらへアクセスするためのアクセスメソッド(getters とsetters) を作成する必要があります。
最初のステップとして、小さな部屋をハードコーディングしてゲーム内世界を作成します。
部屋は横幅10単位で高さ7単位になります。以下のように表示されるようボブとブロックを配置します。
World.java
は以下のようになります:
package net.obviam.starassault.model; import com.badlogic.gdx.math.Vector2; import com.badlogic.gdx.utils.Array; public class World { /** The blocks making up the world **/ Array blocks = new Array(); /** Our player controlled hero **/ Bob bob; // Getters ----------- public Array getBlocks() { return blocks; } public Bob getBob() { return bob; } // -------------------- public World() { createDemoWorld(); } private void createDemoWorld() { bob = new Bob(new Vector2(7, 2)); for (int i = 0; i < 10; i++) { blocks.add(new Block(new Vector2(i, 0))); blocks.add(new Block(new Vector2(i, 7))); if (i > 2) blocks.add(new Block(new Vector2(i, 1))); } blocks.add(new Block(new Vector2(9, 2))); blocks.add(new Block(new Vector2(9, 3))); blocks.add(new Block(new Vector2(9, 4))); blocks.add(new Block(new Vector2(9, 5))); blocks.add(new Block(new Vector2(6, 3))); blocks.add(new Block(new Vector2(6, 4))); blocks.add(new Block(new Vector2(6, 5))); } }
これはゲーム内世界にあるエンティティ用の簡単なコンテナクラスです。現時点では、エンティティとはブロックとボブの二つのことです。
このクラスのコンストラクタ内では、ブロックがblocks
配列に追加され、 Bob
が作成されます。
時間の都合上、全てハードコーディングしています。
開始点(0,0)は左下隅にあることを忘れないでください。
ゲームない世界を画面に描写するには、そのための画面を作成してゲーム内世界を描写するよう指示する必要があります。
libgdxでは、Game
という名前の便利なクラスがあるので、 Game
を継承して StarAssault
クラスを再度記述しましょう。
ゲームは複数の Screenで構成されています。今回のゲームでも、3つの基本的なScreenを持ちます。 Start Game screenと Play ScreenとGame Over screenです。 各 screen は自身の内部で起こっている事象のみに関与し、他Screenについては無視します。 例えば、 Start Game screen には開始 と 終了のメニューオプションがあります。 このScreenは二つの要素(ボタンを)持っており、それら要素でのクリック/タッチ制御について関与します。 ただひたすら二つのボタンを描写し、Playボタンがクリック/タッチされた場合はメインGameに通知してPlay Screen を読み込み、現在のScreenを削除します。 Play Screen はゲームを動作させ、ゲーム描写に関わる全てを制御します。 ゲームオーバー状態になると、メインGameに対してGame Over screenへ遷移するよう指示を出します。 Game Over screenとは、ハイスコアを表示してリプレイボタンをクリックするかユーザーに聞くためだけの画面です。
それでは、コードをリファクタリングして、ゲーム用のメインScreenをとりあえず作成しましょう。ゲーム開始時のScreenとゲームオーバー のScreenは省略します。
GameScreen.java
package net.obviam.starassault.screens; import com.badlogic.gdx.Screen; public class GameScreen implements Screen { @Override public void render(float delta) { // TODO Auto-generated method stub } @Override public void resize(int width, int height) { // TODO Auto-generated method stub } @Override public void show() { // TODO Auto-generated method stub } @Override public void hide() { // TODO Auto-generated method stub } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { // TODO Auto-generated method stub } }
StarAssault.java
は非常にシンプルになります。
package net.obviam.starassault; import net.obviam.starassault.screens.GameScreen; import com.badlogic.gdx.Game; public class StarAssault extends Game { @Override public void create() { setScreen(new GameScreen()); } }
GameScreen
はApplicationListener
に良く似た Screen
インタフェースを実装しますが、Screen
には重要なメソッドが二つ追加されています。
show()
– これはメインゲームがこのScreenをアクティブ状態にした時に呼び出されます
hide()
– これはメインゲームが他のScreenをアクティブ状態にした時に呼び出されます
StarAssault
には実装されたメソッドが一つだけあります。
このcreate()
メソッドでは、GameScreen
のインスタンスを新規作成してそれを有効化しているだけです。
言い換えれば、GameScreen
を作成してshow()
メソッドを呼び出してそれからサイクル毎のrender()
メソッドを呼び出します。
ゲームが動く場所のため、次のパートでは GameScreen
が焦点になります。
ゲームループには render()
メソッドを使うということを忘れないでください。
しかし、何かを描写するためにはまずゲーム内世界を作成する必要があります。
ゲーム内世界は show()
メソッドでも作成できますが、ゲームプレイ画面以外の切り替え先となる画面がありません。
現時点では、 GameScreen
はゲーム開始時にのみ表示されます。
このクラスにメンバを二つ追加して、 render(float delta)
メソッドを実装します。
private World world; private WorldRenderer renderer; /** Rest of methods ommited **/ @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); renderer.render(); }
world
属性は、ブロックとボブを内包した World
インスタンスです。
renderer
は、ゲーム内世界を画面に描写/表現するクラスです (すぐ後で説明します)。
render(float delta)
それでは、WorldRenderer
クラスを作成しましょう。
WorldRenderer.java
package net.obviam.starassault.view; import net.obviam.starassault.model.Block; import net.obviam.starassault.model.Bob; import net.obviam.starassault.model.World; import com.badlogic.gdx.graphics.GL10; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; public class WorldRenderer { private World world; private OrthographicCamera cam; /** for debug rendering **/ ShapeRenderer debugRenderer = new ShapeRenderer(); public WorldRenderer(World world) { this.world = world; this.cam = new OrthographicCamera(10, 7); this.cam.position.set(5, 3.5f, 0); this.cam.update(); } public void render() { // render blocks debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.Line); for (Block block : world.getBlocks()) { Rectangle rect = block.getBounds(); float x1 = block.getPosition().x + rect.x; float y1 = block.getPosition().y + rect.y; debugRenderer.setColor(new Color(1, 0, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); } // render Bob Bob bob = world.getBob(); Rectangle rect = bob.getBounds(); float x1 = bob.getPosition().x + rect.x; float y1 = bob.getPosition().y + rect.y; debugRenderer.setColor(new Color(0, 1, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); debugRenderer.end(); } }
WorldRenderer
は一つだけ目的を持っています。現在のゲーム内世界の状態を取得して、画面に描写することです。
WorldRenderer
には、メインループ(GameScreen
)から呼び出されるpublic render()
メソッドが一つあります。
このレンダラーはworld
にアクセスする必要があるので、インスタンスを作成する時にworld
を引数として渡します。
最初の手順では、現在の状態が見えるようにするために各要素(ブロックとボブ)の境界ボックスを描写します。
OpenGLでプリミティブを描写するのは非常に面倒ですが、libgdxにはこの作業は非常に簡単にするShapeRendererがあります。
コード中の重要な行について説明します。
#14 – world
をメンバ変数として宣言します。
#15 – OrthographicCamera
を宣言します。 このカメラを使って正射影の視点からこのゲーム内世界を “見ます” 。
現時点ではゲーム内世界はとても小さく、一つの画面内に収まりますが、大規模なレベルになってボブがその中を動き回る場合は、カメラがボブを追いかける必要があります。
これは現実のカメラに似ています。正射影の詳細についてはここを参照してください。
#18 – ShapeRenderer
が宣言されます。これを使ってエンティティ用のプリミティブ (rectangle)を描写します。
これは直線や矩形や円のようなプリミティブの描写を手助けするレンダラーです。グラフィックベースのCanvasに精通した人にとって、これは簡単なものです。
#20 – 引数としてworld
を受け取るコンストラクタ。
#22 – 横幅が10単位、高さが7単位のviewportでカメラを作成します。これは、単位ブロック(横幅=1,高さ=1)で画面を埋め尽くすと、X軸上には10個のボックス、y軸上には7個のボックスが表示されるということです。
重要: これは解像度によって変わることはありません。画面解像度が 480×320の場合、10単位で480ピクセルが表されるため、ボックスの横幅は48ピクセルになります。また、7単位で 320ピクセルが表されるため画面上でのボックスの高さは 45.7 ピクセルになります。これでは完全な正方形になりません。これはアスペクト比が原因です。
今回のアスペクト比は 10:7 です。
#23 – この行では、部屋の中央を向くようにカメラの位置を設定します。既定では、カメラは部屋の隅である (0,0) を向いています。
普通のカメラから想像するように、(0,0)というのはカメラの視界の中央に当たります。以下の画像は、ゲーム内世界とカメラの設定座標を示します。
#24 – カメラの内部マトリックスを更新します。この update メソッドは、カメラがアクション(移動、ズーム、回転、など)を行う度に呼び出す必要があります。
OpenGL の部分はきれいに隠されています。
render()
メソッド:
#29 – カメラからレンダラーへ、マトリックスを適用します。 This is necessary as we positioned the camera and we want them to be the same.
#30 – Rectangleを描写するようレンダラーに指示します。
#31 – ブロックを描写するために、ゲーム内世界に存在する全てのブロックの描写を反復処理します。
#32 – #34 – 各ブロックの境界Rectangleの座標を抽出します。 OpenGL は頂点(点)を扱うので、Rectangleを描写するためにはその開始点と幅を割り出すために座標を知っている必要があります。 Notice that we work in camera coordinates which coincides with the world coordinates.
#35 – rectangleの色を赤に設定します。
#36 – 横幅
と 高さ
を指定して、x1, y1の位置にrectangleを描写します。
#39 – #44 – ボブに対しても同じ処理を行いますが、今回は rectangle の色を緑にします。
#45 – レンダラーにrectangleの描写が完了したことを知らせます。
renderer
と world
を GameScreen
(メインループ)に追加して、動作確認をする必要があります。
GameScreen
は以下のように編集します:
package net.obviam.starassault.screens; import net.obviam.starassault.model.World; import net.obviam.starassault.view.WorldRenderer; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL10; public class GameScreen implements Screen { private World world; private WorldRenderer renderer; @Override public void show() { world = new World(); renderer = new WorldRenderer(world); } @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); renderer.render(); } /** ... rest of method stubs omitted ... **/ }
render(float delta)
メソッドには3行の処理があります。 最初の 2 行では画面を黒でクリアし、3行目では単にレンダラーの render()
メソッドを呼び出しています。
World
と WorldRenderer
は画面が表示された時に作成されます。
これをデスクトップとAndroidの両方でテストするには、両方のプラットフォーム用のランチャーを作成する必要があります。
始めに、さらにもう二つプロジェクトを作成します。
これはstar-assault-desktop
プロジェクトと star-assault-android
プロジェクトです。後者は Android 用のプロジェクトです。
デスクトップの場合、プロジェクトはとても単純です。
プロジェクト内にmain
メソッドを持つクラスを作成する必要があります。このクラスで、libgdxアプリケーションのインスタンスを作成します。
デスクトップ用プロジェクトで StarAssaultDesktop.java
クラスを作成します。
package net.obviam.starassault; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; public class StarAssaultDesktop { public static void main(String[] args) { new LwjglApplication(new StarAssault(), "Star Assault", 480, 320, true); } }
これだけです。 #7 行目で全ての処理が行われています。
Game
を実装したStarAssault
インスタンスを引数として渡し、LwjglApplication
アプリケーションのインスタンスを新規作成しています
二つ目と三つ目の引数はウィンドウのサイズを指定しています。
480 x 320 の解像度は多くのAndroid携帯でサポートされており、デスクトップでもそれに合わせたいので、今回は480 x 320を設定しています。
最後の引数はlibgdxにOpenGL ES 2を使うよう指示しています。
通常のJavaプログラムとしてアプリケーションを実行すると以下のようになりますt:
いくつかエラーが発生した場合は、これまでの作業を一度振り返り、 star-guard プロジェクトの プロパティ-> ビルドパスと進んだ画面のエキスポートタブで gdx.jarにチェックを入れているかなど、設定は正しいか、全ての手順を行ったかを確認してください。
star-assault-android
プロジェクトには、StarAssaultActivity
という名前のJavaクラスが一つあります。
それを以下のように変更します:
StarAssaultActivity.java
package net.obviam.starassault; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; public class StarAssaultActivity extends AndroidApplication { /** Called when the activity is first created. */ @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration config = new AndroidApplicationConfiguration(); config.useAccelerometer = false; config.useCompass = false; config.useWakelock = true; config.useGL20 = true; initialize(new StarAssault(), config); } }
AndroidApplication
を継承した新しいActivityに注目してください。
#13 行目で AndroidApplicationConfiguration
オブジェクトを作成しています。
Androidプラットフォームに関する構成の全てを設定できます。
これらの設定は何をしているのか一目瞭然ですが、 Wakelock
を使用したい場合は AndroidManifest.xml
ファイルも編集する必要があるので注意してください。
これは、ユーザーが画面をタッチしない場合でも端末の状態を維持して画面を暗くしないよう、Androidにパーミッションを要求します。
AndroidManifest.xml
ファイルの <manifest>
タグ内のどこかに、以下の行を追加します。
また #17行目では、OpenGL ES 2を使うようAndroidに指示を出します。
これは、エミュレーターはOpenGL ES 2をサポートしていないのでこのコードは端末上でしかテストができないことを意味します。
この場合は問題が発生してしまうので、この設定値を false
に切り替えます。
#18行目では Android アプリケーションを初期化して起動しています。
Eclipseに接続された端末があれば、端末に直接アプリケーションを配布して以下の Nexus上で動作するアプリケーションのような画面を見ることができます。
これはデスクトップバージョンと同じ様な画面になります。
こんな短い時間でここまで来れたのは非常に感慨深いです。 ここでは MVC パターンについて簡単に記載します。
MVC パターンは非常に効率的でシンプルです。
MVC パターンにおけるモデルとは、画面に表示するエンティティのことです。MVC パターンにおけるビューとは、レンダラーのことです。ビューはモデルを画面上に描写します。
今、エンティティ(特にボブ)の操作をする必要があるため、いくつかコントローラーも実装します。
MVC パターンの詳細を知りたい場合は、他の記事を確認する かネットで検索してください。
これはとても便利です。
今のところ全て問題なく進んでいますが、そろそろ適切なグラフィックを使用したいです。
MVC を使うと便利なので、レンダラーを編集してrectangleではなく画像を描写するようにします。
OpenGL では、画像の表示は非常に複雑な処理です。
最初に画像を読み込み、その画像をTextureに変換し、いくつかの形状によって記述されたSurfeceに紐付ける必要があります。
libgdx を使うとこの処理が非常に簡単になります。
ディスクに保存されている画像をTextureに変換する処理が、一行でできます。
我々は画像を二つ使用するので、必然的にTextureを二つ使用することになります。
一つはボブ用のtextureで、一つはブロック用のtextureです。
私はブロックとボブ二つの画像を作成しました。ボブはStar Guardゲームのものを真似て作ったものです。
これらは簡単なpngファイルとなっており、assets/images
ディレクトリにコピーします。
画像は block.png
と bob_01.png
の二つがあります。
最終的にボブはアニメーションキャラクターにする予定なので、ファイル名の後ろに番号をつけています(将来的に計画しています)。
では、まず最初にWorldRenderer
を少し綺麗にしましょう。つまり、デバッグで使用するためにrectangleの描写処理を抽出して別々のメソッドに分けます。
テクスチャを読み込んで、それを画面に応じて描写する必要があります。
以下の新しい WorldRenderer.java
を見てみましょう
package net.obviam.starassault.view; import net.obviam.starassault.model.Block; import net.obviam.starassault.model.Bob; import net.obviam.starassault.model.World; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.graphics.Color; import com.badlogic.gdx.graphics.OrthographicCamera; import com.badlogic.gdx.graphics.Texture; import com.badlogic.gdx.graphics.g2d.SpriteBatch; import com.badlogic.gdx.graphics.glutils.ShapeRenderer; import com.badlogic.gdx.graphics.glutils.ShapeRenderer.ShapeType; import com.badlogic.gdx.math.Rectangle; public class WorldRenderer { private static final float CAMERA_WIDTH = 10f; private static final float CAMERA_HEIGHT = 7f; private World world; private OrthographicCamera cam; /** for debug rendering **/ ShapeRenderer debugRenderer = new ShapeRenderer(); /** Textures **/ private Texture bobTexture; private Texture blockTexture; private SpriteBatch spriteBatch; private boolean debug = false; private int width; private int height; private float ppuX; // pixels per unit on the X axis private float ppuY; // pixels per unit on the Y axis public void setSize (int w, int h) { this.width = w; this.height = h; ppuX = (float)width / CAMERA_WIDTH; ppuY = (float)height / CAMERA_HEIGHT; } public WorldRenderer(World world, boolean debug) { this.world = world; this.cam = new OrthographicCamera(CAMERA_WIDTH, CAMERA_HEIGHT); this.cam.position.set(CAMERA_WIDTH / 2f, CAMERA_HEIGHT / 2f, 0); this.cam.update(); this.debug = debug; spriteBatch = new SpriteBatch(); loadTextures(); } private void loadTextures() { bobTexture = new Texture(Gdx.files.internal("images/bob_01.png")); blockTexture = new Texture(Gdx.files.internal("images/block.png")); } public void render() { spriteBatch.begin(); drawBlocks(); drawBob(); spriteBatch.end(); if (debug) drawDebug(); } private void drawBlocks() { for (Block block : world.getBlocks()) { spriteBatch.draw(blockTexture, block.getPosition().x * ppuX, block.getPosition().y * ppuY, Block.SIZE * ppuX, Block.SIZE * ppuY); } } private void drawBob() { Bob bob = world.getBob(); spriteBatch.draw(bobTexture, bob.getPosition().x * ppuX, bob.getPosition().y * ppuY, Bob.SIZE * ppuX, Bob.SIZE * ppuY); } private void drawDebug() { // render blocks debugRenderer.setProjectionMatrix(cam.combined); debugRenderer.begin(ShapeType.Line); for (Block block : world.getBlocks()) { Rectangle rect = block.getBounds(); float x1 = block.getPosition().x + rect.x; float y1 = block.getPosition().y + rect.y; debugRenderer.setColor(new Color(1, 0, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); } // render Bob Bob bob = world.getBob(); Rectangle rect = bob.getBounds(); float x1 = bob.getPosition().x + rect.x; float y1 = bob.getPosition().y + rect.y; debugRenderer.setColor(new Color(0, 1, 0, 1)); debugRenderer.rect(x1, y1, rect.width, rect.height); debugRenderer.end(); } }
重要な行について指摘していきます:
#17 & #18 – viewportのサイズに使う定数を宣言します。これはカメラに使用します。
#27 & #28 – ボブとブロックで使用する textureを二つ宣言します。
#30 – SpriteBatch
を宣言します。 SpriteBatch
は私たちに代わって全てのTextureの紐付けや描写などを行います。
#31 – これは、デバッグ画面を表示する必要があるのかないのかを知らせるために、コンストラクタ内で設定される属性です。
デバッグレンダリングは、ゲームの各要素ごとに境界を描写するだけなので。覚えておいて下さい。
#32 – #35 – これらの変数は各要素を正しく表示するために必要です。width
と height
は画面サイズをピクセル単位で保持します。この値はresize
メソッドが呼び出された時にOSから渡されます。 ppuX
と ppuY
は単位ごとのピクセル数です。
カメラのview portを世界座標で10×7に設定し(横方向に10個の境界と縦方向に7個の境界が表示されるという意味)、最終的にはピクセルを処理しているので、
これらの値を実際のピクセル座標に紐付ける必要があります。
今回は解像度480×320を使用することに決めました。つまり水平方向では480ピクセルは10ユニットと同じ幅となり、画面上で1ユニットは48ピクセルの幅を持ちます。
同じユニット(48 ピクセル)を高さでも使おうとした場合、336ピクセル(48 * 7 = 336)の高さが必要になります。
320ピクセル分の高さしかないけど、ブロックは縦方向に7つ表示したい。
垂直方向でも同じことをするには、1ユニットの高さは320 / 7 = 45.71 ピクセルになります。
ゲーム内世界に収まるようにするには、全ての画像を全て歪ませる必要があります。
It’s perfectly fine and OpenGL does that very easily. This happens when we change the aspect ratio on our TV set and sometimes the image gets elongated or squashed to fit everything on the screen, or we just simply choose the option to cut the image off but maintain the aspect ratio.
注意: 画面解像度がint型で扱われる場合でも、この値には float
型を使用します。 OpenGL は float型の値を好むので、そのようにしています。
OpenGL will work out the dimensions and where to place pixels.
#36 – setSize (int w, int h)
メソッドは画面のサイズが変更される度に呼ばれ、そこでは単純にユニットの幅をピクセル単位で(再)計算します。
#43 – コンストラクタを少し変更しただけですが、これはとても重要なことです。コントラクタでは SpriteBatch
のインスタンスを作成して texture を読み込みます(#50行目)。
#53 – loadTextures()
はその名の通りのこと: テクスチャの読み込みをします.
とても簡単でしょう。 Textureを作成するにはファイルハンドラを引数として渡す必要があり、渡されたハンドラからTextureが作成されます。
libgdxのファイルハンドラはとても便利です。Androidとデスクトップのどちらでも同じ様に使えるので、使用する internal タイプのファイルを指定するだけで、ハンドラが勝手に読み込んでくれます。 assets
はソースディレクトリとして使用されているため、ファイルパスの assets
の部分は省略できるので覚えておいて下さい。
, meaning everything from that directory gets copied into the root of the final bundle.
そのため、assets
はルートディレクトリとして振舞います。
#58 – 新しい render()
メソッドには数行の記述があります。
#59 & #62 – SpriteBatch
の描写ブロック/セッションを囲みます。
SpriteBatch
を使ってOpenGLで画像を描写したい場合は、常に begin()
を実行し、必要な描写処理を行い、終わったらend()
を実行する必要があります。これはとても重要で、もしやらなかった場合は上手く動作しません。 詳細については ここのSpriteBatchを参照してください。
#60 & #61 – 単に2つのメソッドを実行し、最初のメソッドでブロックを描写してそれからボブを描写しています。
#63 & #64 – debug
が有効な場合は、このボックスを描写するメソッドが実行されます。drawDebug
メソッドについては前に説明しました。
#67 – #76 – drawBlocks
メソッドと drawBob
メソッドは似ています。各メソッドではspriteBatch
の draw
メソッドにtextureを引数として付けて実行しています。これを理解することは重要です。
一つ目の引数は texture (ディスクから読み込まれた画像)です。
二番目と三番目の引数で spriteBatch
に画像を表示する場所を指示します。
世界座標からスクリーン座標に変換した座標を使用しているので注意してください。
ここでppuX
と ppuY
を使用します。
手で計算すると画像が表示される場所が分かります。SpriteBatch
は、 既定では開始点(0,0)が左下隅にある座標系を使用します。
これで完了です。
最後にGameScreen
クラスを変更し、resize
メソッド内でレンダラーのsetSizeを呼び出し、show()メソッド内でレンダラーの debug
に true
を設定するようにしてください。
GameScreen
は以下のように少し変更します。
/** ... omitted ... **/ public void show() { world = new World(); renderer = new WorldRenderer(world, true); } public void resize(int width, int height) { renderer.setSize(width, height); } /** ... omitted ... **/
アプリケーションを事項すると以下のような画面が表示されます:
デバッグなし
以下はデバッグありでの描写
素晴らしい、Androidでも実行してみてどう表示されるか確認してください。
ずいぶんと長いこと作業をしてきましたが、現状でゲーム内世界は動くものが何もがなく、面白くありません。
これをゲームにするには、キー入力とタッチを受け取ってそれに基づいてなんらかのアクションを行う、入力処理を追加する必要があります。
デスクトップでの コントロールスキーム はとても簡単です。矢印 キーでボブを右と左へ動かし、 zキーでボブをジャンプさせ、xキーで武器を撃ちます。
Android ではやり方が異なります。こうした機能を持つボタンをいくつか用意し、画面の下に配置し、ボタンをタッチするとキーが押されたと見なします
MVC パターンに従うには、ボブとそれ以外の部分を操作するクラスを、モデルクラスやビュークラスと分けます。
net.obviam.starassault.controller
パッケージを作成して、全てのコントローラーをそこに記述します。
最初にまずキーを押してボブを操作します。ゲームをプレイするには、左移動、右移動、ジャンプ、攻撃の4つのキーの状態を追う必要があります。
二種類の入力方式(キーボードとタッチスクリーン)を使用するので、それぞれの入力方式で発生したイベントを、各動作を発生させるプロセッサーに渡す必要があります。
各動作はイベントによって引き起こされます。
左移動 動作は、左矢印 キーが押された時か画面上で特定のエリアがタッチされた時のイベントによって引き起こされます。
ジャンプ動作はz キーが押された時に引き起こされる、などです。
それでは、WorldController
という名前のとても簡単なコントローラーを作成しましょう。
WorldController.java
package net.obviam.starassault.controller; import java.util.HashMap; import java.util.Map; import net.obviam.starassault.model.Bob; import net.obviam.starassault.model.Bob.State; import net.obviam.starassault.model.World; public class WorldController { enum Keys { LEFT, RIGHT, JUMP, FIRE } private World world; private Bob bob; static Map<Keys, Boolean> keys = new HashMap<WorldController.Keys, Boolean>(); static { keys.put(Keys.LEFT, false); keys.put(Keys.RIGHT, false); keys.put(Keys.JUMP, false); keys.put(Keys.FIRE, false); }; public WorldController(World world) { this.world = world; this.bob = world.getBob(); } // ** Key presses and touches **************** // public void leftPressed() { keys.get(keys.put(Keys.LEFT, true)); } public void rightPressed() { keys.get(keys.put(Keys.RIGHT, true)); } public void jumpPressed() { keys.get(keys.put(Keys.JUMP, true)); } public void firePressed() { keys.get(keys.put(Keys.FIRE, false)); } public void leftReleased() { keys.get(keys.put(Keys.LEFT, false)); } public void rightReleased() { keys.get(keys.put(Keys.RIGHT, false)); } public void jumpReleased() { keys.get(keys.put(Keys.JUMP, false)); } public void fireReleased() { keys.get(keys.put(Keys.FIRE, false)); } /** The main update method **/ public void update(float delta) { processInput(); bob.update(delta); } /** Change Bob's state and parameters based on input controls **/ private void processInput() { if (keys.get(Keys.LEFT)) { // left is pressed bob.setFacingLeft(true); bob.setState(State.WALKING); bob.getVelocity().x = -Bob.SPEED; } if (keys.get(Keys.RIGHT)) { // left is pressed bob.setFacingLeft(false); bob.setState(State.WALKING); bob.getVelocity().x = Bob.SPEED; } // need to check if both or none direction are pressed, then Bob is idle if ((keys.get(Keys.LEFT) && keys.get(Keys.RIGHT)) || (!keys.get(Keys.LEFT) && !(keys.get(Keys.RIGHT)))) { bob.setState(State.IDLE); // acceleration is 0 on the x bob.getAcceleration().x = 0; // horizontal speed is 0 bob.getVelocity().x = 0; } } }
#11 – #13 – ボブが実行するアクションの enum
型を定義します。各キー入力/タッチでは、これらのうち一つの動作を引き起こすことができます。
#15 – ゲーム内のWorld
を宣言します。このゲーム内世界にあるエンティティを操作します。
#16 – Bob
を private メンバとして宣言します。これはゲーム内世界で Bob
を参照するだけものですが、必要になる度に毎回取得するよりは簡単になるので必要な処理です。
#18 – #24 – キーの静的 ハッシュマップ
とそのステータスです。キーが押された場合は true
となり、そうでない場合はfalse
となります。これは静的な値で初期化されます。このマップは、コントローラーのupdate
メソッド内でボブを意図したように動かすために使われます。
#26 – これは引数として World
を取得するコンストラクタです。同様に ボブへの参照も取得します。
#33 – #63 – これらのメソッドは、アクションボタンが押されたりボタン領域がタッチされた時に呼び出されるシンプルなコールバックです。
これらメソッドは、入力が行われると呼びだされます。
押されたキーそれぞれの値をMapに設定するだけです。
見て分かるようにコントローラーは有限オートマトンでもあり、その状態はkeys
マップによって与えられます。
#66 – #69 – update
メソッドはメインループのサイクル毎に呼び出されます。現在、ここでは二つのことを行っています。 1つ目は–入力の処理で、 2つ目は – ボブの更新です。ボブには専用の update
メソッドがあります。このメソッドについては後で説明します。
#72 – #92 – processInput
メソッドではキーを keys
マップに確認して、それに応じてボブに値を設定します。
例えば、 #73 – #78 行目では左移動のキーが押されていないか確認し、押されていた場合はボブを左へ向かせて状態を State.WALKING
にして、
ボブのSPEEDにマイナス符号を付けた値を加速値に設定します。
マイナス符号をつけるのは、画面上では左はマイナスの方向だからです(開始点は画面に左下にあり、右へ向かって進みます)。
右方向への移動も同じです。キーが両方押されていないか、何も押されていないかの追加チェックもあり、これに合致する場合 Bob は State.IDLE
状態になって水平方向の加速値は 0
となります。
それでは、 Bob.java
で変更した内容について見てみましょう。
public static final float SPEED = 4f; // unit per second public void setState(State newState) { this.state = newState; } public void update(float delta) { position.add(velocity.cpy().scl(delta)); }
SPEED
定数の値を毎秒 4 ユニット(ブロック)に変更しただけです。
また、以前追加するのをすっかり忘れてしまっていたので setState
メソッドを追加しました。
最も興味深いのは新たに追加された update(float delta)
メソッドです。これは WorldController
から呼び出されます。
このメソッドは加速度に基づいてボブの位置を更新するだけです。
コントローラー側でボブの向きや状態に応じて加速度の設定を実行しているので、簡略化のためにボブの状態確認はせずに処理だけ行っています。
ここではVectorを使って計算をしており、ほとんどの計算はlibgdx がやってくれます。
我々がやるのは、delta
秒の間にボブが現在のposition
まで移動した距離を追加することだけです。
cpy()
メソッドで同じ値を持ったvelocity
オブジェクトを新規に作成してそのオブジェクトの値と経過時間delta
を乗算するので、
velocity.cpy()
を使用しています
velocity と position は両方 Vector2オブジェクトを使用しているので、 Java では参照の扱い方に気をつけてください。
Vectorの詳細については ここhttp://en.wikipedia.org/wiki/Euclidean_vectorを参照してください。
これでほぼ全てが完了し、後はキーが押されたときに正しいイベントを呼び出す必要があります。libgdx にはいくつかのコールバックメソッドを持つ入力プロセッサーがあります。
ゲームプレイ時の画面表示にGameScreen
を使用しているため、それを入力プロセッサーとしても使用すると良いでしょう。
これをするためには、GameScreen
でlibgdxのInputProcessor
を実装します。
以下が新たに修正した GameScreen.java
です。
package net.obviam.starassault.screens; import net.obviam.starassault.controller.WorldController; import net.obviam.starassault.model.World; import net.obviam.starassault.view.WorldRenderer; import com.badlogic.gdx.Gdx; import com.badlogic.gdx.Input.Keys; import com.badlogic.gdx.InputProcessor; import com.badlogic.gdx.Screen; import com.badlogic.gdx.graphics.GL10; public class GameScreen implements Screen, InputProcessor { private World world; private WorldRenderer renderer; private WorldController controller; private int width, height; @Override public void show() { world = new World(); renderer = new WorldRenderer(world, false); controller = new WorldController(world); Gdx.input.setInputProcessor(this); } @Override public void render(float delta) { Gdx.gl.glClearColor(0.1f, 0.1f, 0.1f, 1); Gdx.gl.glClear(GL10.GL_COLOR_BUFFER_BIT); controller.update(delta); renderer.render(); } @Override public void resize(int width, int height) { renderer.setSize(width, height); this.width = width; this.height = height; } @Override public void hide() { Gdx.input.setInputProcessor(null); } @Override public void pause() { // TODO Auto-generated method stub } @Override public void resume() { // TODO Auto-generated method stub } @Override public void dispose() { Gdx.input.setInputProcessor(null); } // * InputProcessor methods ***************************// @Override public boolean keyDown(int keycode) { if (keycode == Keys.LEFT) controller.leftPressed(); if (keycode == Keys.RIGHT) controller.rightPressed(); if (keycode == Keys.Z) controller.jumpPressed(); if (keycode == Keys.X) controller.firePressed(); return true; } @Override public boolean keyUp(int keycode) { if (keycode == Keys.LEFT) controller.leftReleased(); if (keycode == Keys.RIGHT) controller.rightReleased(); if (keycode == Keys.Z) controller.jumpReleased(); if (keycode == Keys.X) controller.fireReleased(); return true; } @Override public boolean keyTyped(char character) { // TODO Auto-generated method stub return false; } @Override public boolean touchDown(int x, int y, int pointer, int button) { if (x < width / 2 && y > height / 2) { controller.leftPressed(); } if (x > width / 2 && y > height / 2) { controller.rightPressed(); } return true; } @Override public boolean touchUp(int x, int y, int pointer, int button) { if (x < width / 2 && y > height / 2) { controller.leftReleased(); } if (x > width / 2 && y > height / 2) { controller.rightReleased(); } return true; } @Override public boolean touchDragged(int x, int y, int pointer) { // TODO Auto-generated method stub return false; } @Override public boolean mouseMoved(int x, int y) { // TODO Auto-generated method stub return false; } @Override public boolean scrolled(int amount) { // TODO Auto-generated method stub return false; } }
変更内容:
#13 – このクラスで InputProcessor
を実装します。
#19 – Androidのタッチイベントで使用される画面の横幅と高さ。
#25 – worldを引数として渡して WorldController
のインスタンスを作成します。
#26 – このScreenを、アプリケーションの現在の入力プロセッサとして設定します。 libgdx はこれをグローバル入力プロセッサとして扱うので、同じ入力プロセッサをScreenごとに使い回さない場合は、各Screenで異なる入力プロセッサを設定する必要があります。今回の場合、Screen自体が入力を制御します。
#47 & #62 – クリアのためにアクティブグローバル入力プロセッサに null
を設定します。
#68 – 物理キーボードのキーが押された時に keyDown(int keycode)
メソッドが引き起こされます。 keycode
引数は押されたキーの値を表し、これを使ってキーの問い合わせができ、該当キーだった場合は処理を行います。これがここで行われている処理の詳細です。
目当てのキーに基づいて、イベントをコントローラーに渡します。このメソッドでは戻り値 true
を返す処理も行い、入力プロセッサに入力が制御されたことを知らせます。
#81 – keyUp
メソッドは keyDown
メソッドと真逆です。キーが離された時、 WorldController
へイベントを渡します。
#111 – #118 – ここは面白いところです。
これはタッチスクリーン上でのみ発生し、座標情報がポインター情報とボタン情報と一緒に引数として渡されます。
ポインターとはマルチタッチに使われるもので、取得したタッチのIDを表します。
今回の制御処理は非常にシンプルなもので、簡単に例示をするためだけに作成しました。
画面が4つに分割され、画面の左下部分がタッチされるとそれは左へ移動する動作を発生させる操作としてあつかわれ、controller
へはデスクトップと同じイベントを通知します。
touchUpについてもまったく同じです。
警告: – 上記のコードはバグを孕んでおり、不安定な挙動をします。touchDragged メソッド内には何も処理を記述していないので、指が4分割された範囲をまたがって移動した時に意図しない動作をしてしまうのです。 もちろん修正が必要ですが、このページでの目的は複数のハードウェア入力とそれらの紐付けについて説明することなので今回は割愛します。
デスクトップと Androidの両方でアプリケーションを実行して、制御処理の動作確認をします。デスクトップ環境では矢印キーを、Android環境では画面の左下部分をタッチすることでボブが移動します。
デスクトップでは、マウスを使用するとタッチ制御の部分でも動作することに気づいたでしょう。これは、デスクトップでは touchXXX
はマウス入力も制御するためです。
これを修正するには、touchDown
メソッドとtouchUp
メソッドの始めに以下の行を追加します:
if (!Gdx.app.getType().equals(ApplicationType.Android)) return false;
アプリケーションがAndroidの場合は false
を返し、メソッドの以降の部分を実行させません。
false
は入力が制御されないことを意味するので忘れないでください。
ご覧の通り、ボブが移動しました。
ここまででゲーム開発についてのかなり部分を説明したので、画面に表示できるものが既にできてます。
少しずつ機能を加えていき、徐々にゲームを完成させました。
まだ、以下を追加する必要があります。:
上記一覧の中のアニメーションを実装するためにPart 2を確認しましょう。
次のページに進んで内容を実践し、ご意見や感想などをいただけるとありがたいです。
またlibgdx とその 素晴らしいコミュニティも確認してください。
このプロジェクトのソースコードはここにあります: https://github.com/obviam/star-assault
このブランチはpart1の記事のものです
gitを使ってチェックアウトするには:
git clone -b part1 git@github.com:obviam/star-assault.git
また zip ファイルとしてダウンロードすることもできます。
ボブにアニメーションを追加する、この連載の次の記事にも進めます。