はじめに
@yamacraftです。普段はプライベートでMSI U100という3年以上前のネットブックにxUbuntu(Ubuntuだと重くて辛い)を入れて、無理矢理Androidアプリ開発をしています。
http://yamacraft2.exblog.jp/
blogもやってます!自己紹介は以上です!本題です!
一般的にスマホゲーム開発といえば、cocos2d-xやUnityといったクロスプラットフォームを利用した開発が主流となっていますが、デメリットとして、例えば広告やトラッキング、バグレポート系のライブラリが使えなくなったり、または使いづらくなったり、そもそもJava以外の言語を使うことになって、これまでのアプリ開発で習得したノウハウが活かしづらいといった部分があるかと思います。
これから紹介するAndEngineは、名前のとおりandroid専門の2D OpenGLエンジンライブラリです。
android専門として作られているため
- 普通のライブラリとして組み込める
- java言語のままで開発ができる
- 既存のアプリに組み込みやすい
といったメリットがあります。
あと何より、ライブラリレベルなのでビルドが非常に軽快です(このライブラリ自体は)。
今回はそんなAndEngineの普及を目的に、導入方法を簡単に紹介します。
AndEngine公式サイト
準備
https://github.com/nicolasgramlich/AndEngine
こちらにソースファイルがありますので、git cloneするなり、Download zipでソースを落とすなりしてください。
masterとGLES2+αのブランチがありますが、自分はGLES2の方を使っています。
落ちてきたソースの中身は普通の(eclipse)プロジェクトなので、そのままインポートしてください。
ちなみにこの記事は、これ以降もeclipse前提で話をすすめます。
MSI U100ではandroid studio(の方がより)が重いのと、WSVGA解像度ではいろんなダイアログの下の部分(ちょうどボタンがある場所)が見切れてしまってかなり辛いので、eclipseを使い続けているためです。
ちなみに普通にeclipseでも普通に見切れることがあるので、外部ディスプレイ出力推奨です。AndEngineには関係のない話でした。
プロジェクトのインポートが完了したら、あとは普通に各androidプロジェクトの「プロパティ - ライブラリ」から、AndEngineのライブラリを追加してあげてください。
今回は、みんなが大好きなjellybeanくんが、画面上で動き続けるサンプルアプリを作ることにしました。
実装と解説
というわけで、いきなり実装部分のソースを公開します。
SimpleLayoutGameActivityを継承したAndEngineActivity.java(+activity_andengine.xml)の上に、メインの描画部分であるAndEngineScene.javaを乗せています。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
>
<org.andengine.opengl.view.RenderSurfaceView
android:id="@+id/renderSurfaceView"
android:layout_width="match_parent"
android:layout_height="match_parent"
/>
</LinearLayout>
public class AndEngineActicity extends SimpleLayoutGameActivity {
@Override
public EngineOptions onCreateEngineOptions() {
// onCreate()より前に、このメソッドが呼ばれます
// ここで端末解像度の大きさを取得
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
// 画面サイズの設定
final Camera camera = new Camera(0, 0, metrics.widthPixels,
metrics.heightPixels);
EngineOptions options = new EngineOptions(true,
ScreenOrientation.PORTRAIT_FIXED,
new RatioResolutionPolicy(
metrics.widthPixels, metrics.heightPixels), camera);
return options;
}
@Override
protected void onCreateResources() {
}
@Override
protected Scene onCreateScene() {
// 表示するSceneクラスの取り込み
AndEngineScene scene = new AndEngineScene(this);
return scene;
}
/**
* 利用するレイアウトファイルのリソースIDを設定
*/
@Override
protected int getLayoutID() {
return R.layout.activity_andengine;
}
/**
* RenderSurfaceViewのリソースIDを設定
*/
@Override
protected int getRenderSurfaceViewID() {
return R.id.renderSurfaceView;
}
}
public class AndEngineScene extends Scene {
private SimpleLayoutGameActivity mActivity;
private Sprite mSprite;
/**
* コンストラクタ
*/
public AndEngineScene(SimpleLayoutGameActivity activity) {
super();
this.mActivity = activity;
// ----------------------------------------------------
// スプライトを設定して、画面中央に表示
// ----------------------------------------------------
// スプライト元になる画像ファイルの読み込み
// ただし使うのは画像のサイズだけ
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
InputStream is = null;
try {
is = mActivity.getResources().getAssets().open("img/jellybean.png");
} catch (IOException e) {
e.printStackTrace();
}
BitmapFactory.decodeStream(is, null, options);
// Spriteに貼り付けるテクスチャーの生成
BitmapTextureAtlas bta = new BitmapTextureAtlas(
mActivity.getTextureManager(),
getTwoPowerSize(options.outWidth),
getTwoPowerSize(options.outHeight),
TextureOptions.BILINEAR_PREMULTIPLYALPHA);
mActivity.getEngine().getTextureManager().loadTexture(bta);
ITextureRegion btr = BitmapTextureAtlasTextureRegionFactory
.createFromAsset(bta, mActivity, "img/jellybean.png", 0, 0);
// Spriteの作成と画面中央に配置
mSprite = new Sprite(0, 0, btr,
mActivity.getVertexBufferObjectManager());
mSprite.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
mSprite.setPosition(
(mActivity.getEngine().getCamera().getWidth() - mSprite
.getWidth()) / 2, (mActivity.getEngine().getCamera()
.getHeight() - mSprite.getHeight()) / 2);
attachChild(mSprite);
// タイマーハンドラーの設定
registerUpdateHandler(timerHandler);
}
/*
* 1/60秒単位で呼ばれるTimerHandler
*/
private TimerHandler timerHandler = new TimerHandler(1f / 60, true,
new ITimerCallback() {
@Override
public void onTimePassed(TimerHandler pTimerHandler) {
// Spriteを上に移動させる
mSprite.setPosition(mSprite.getX(), mSprite.getY() - 5);
// Spriteが画面から完全に消えたら、画面最下部に移動させる
if (mSprite.getY() <= 0 - mSprite.getHeight()) {
mSprite.setPosition(mSprite.getX(), mActivity
.getEngine().getCamera().getHeight()
+ mSprite.getHeight());
}
}
});
/**
* 指定したサイズより1つ上の2のべき乗の値を返す
*
* @param size
* @return
*/
private int getTwoPowerSize(float size) {
int value = (int) (size + 1);
int pow2value = 64;
while (pow2value < value)
pow2value *= 2;
return pow2value;
}
}
だいたいの部分はコメントやメソッド名など判断できるかと思いますが、一部解説を書きます。
解説
@Override
public EngineOptions onCreateEngineOptions() {
// onCreate()より前に、このメソッドが呼ばれます
// ここで端末解像度の大きさを取得
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
// 画面サイズの設定
final Camera camera = new Camera(0, 0, metrics.widthPixels,
metrics.heightPixels);
EngineOptions options = new EngineOptions(true,
ScreenOrientation.PORTRAIT_FIXED,
new RatioResolutionPolicy(
metrics.widthPixels, metrics.heightPixels), camera);
return options;
}
コメントにも書いていますが、onCreateEngineOptions()はonCreateより先に呼ばれます。
「//画面サイズの設定」以降に書かれている処理は、RenderViewで実際に描画する領域の確保を行っています。
今回は端末の画面全体に描画したいため、DisplayMetricsクラスを使って端末の解像度を取得し、EngineOptionsクラスのコンストラクタの引数1番目をtrueにすることで、画面全体への描画を行うように設定しています。
2番目の引数は、たとえばCameraクラスに設定した値が端末の解像度以下の場合、引き伸ばすかどうか+画面は縦表示か横表示かを設定しています。今回は「縦表示+端末の解像度の方が大きい場合は、内側にFIXするまで描画領域を拡大する」という内容の設定となっています。端末解像度で設定しているので、小さい場合とかはありえないんですけどね。
たとえば480x720+PORTRAIT_FIXEDで設定したRenderViewが960x1440の端末で表示された場合、縦横2倍に拡大されて表示されます。
拡大するだけなので内部の座標に変化はありません。描画領域の右下の座標は常に(480,720)のままということです。
// スプライト元になる画像ファイルの読み込み
// ただし使うのは画像のサイズだけ
BitmapFactory.Options options = new BitmapFactory.Options();
options.inJustDecodeBounds = true;
InputStream is = null;
try {
is = mActivity.getResources().getAssets().open("img/jellybean.png");
} catch (IOException e) {
e.printStackTrace();
}
BitmapFactory.decodeStream(is, null, options);
// Spriteに貼り付けるテクスチャーの生成
BitmapTextureAtlas bta = new BitmapTextureAtlas(
mActivity.getTextureManager(),
getTwoPowerSize(options.outWidth),
getTwoPowerSize(options.outHeight),
TextureOptions.BILINEAR_PREMULTIPLYALPHA);
mActivity.getEngine().getTextureManager().loadTexture(bta);
ITextureRegion btr = BitmapTextureAtlasTextureRegionFactory
.createFromAsset(bta, mActivity, "img/jellybean.png", 0, 0);
// Spriteの作成と画面中央に配置
mSprite = new Sprite(0, 0, btr,
mActivity.getVertexBufferObjectManager());
mSprite.setBlendFunction(GL10.GL_SRC_ALPHA, GL10.GL_ONE_MINUS_SRC_ALPHA);
mSprite.setPosition(
(mActivity.getEngine().getCamera().getWidth() - mSprite
.getWidth()) / 2, (mActivity.getEngine().getCamera()
.getHeight() - mSprite.getHeight()) / 2);
attachChild(mSprite);
こちらがSceneクラスの肝である、スプライトの表示処理となります。
今回はassetsにimgフォルダを作成し、その下にjellybean.pngを置いて読み込んでいますが、リソースフォルダから画像を読み込む方法でも可能です。
BitmapFactory.decodeResource(mActivity.getResources(),
R.drawable.jellybean, options);
// --(中略)--
ITextureRegion btr = BitmapTextureAtlasTextureRegionFactory
.createFromResource(bta, mActivity.getResources(), R.drawable.jellybean,
0, 0);
このように、Bitmapの読み込みとテクスチャ生成の部分を変えるだけで済みます。
setBlendFunctionなどは、OpenGLのアルファブレンディング設定そのままです。詳細は省きます(自分もよくわかっていないので)。
テクスチャ生成処理のところで、わざわざ読み込んだ画像サイズより1回り大きい2のべき乗の値を指定していますが、これはテクスチャ自体が2のべき乗でないとサイズ指定ができないためです。
「テクスチャとはそういうものだ」と思っておいてください。
private TimerHandler timerHandler = new TimerHandler(1f / 60, true,
new ITimerCallback() {
タイマーハンドラーの部分は見たままです、としか書けないのですが、いちおう補足。
引数1番目は繰り返し呼ばれる際の間隔を指定しています。
基本的にゲームは1秒を60f(フレーム)に分けている場合が多いので、1f/60にしています。
あとregisterUpdateHandler()自体は複数設定が可能ですので、必要に応じてタイマーハンドラーを複数にわけるのも手かと思います。
といわけでざっくりとした解説でしたが、これでjellybeanくんが、下から上に動き続ける描画が完成しました。
スプライト生成の部分に若干敷居を感じるかもしれませんが、こちらの処理自体は共通メソッド化できてしまうので、一度下地の処理を作ってしまえばいいと思います。
その他の簡単なAndEngineにおける実装は、国内にも解説書籍があるので参照してみてください。自分もこの書籍で使い方を覚えました。
Amazon.co.jp: AndEngineでつくるAndroid 2Dゲーム (SMART GAME DEVELOPER): 立花 翔: 本
あとはあまり国内の解説記事は少なく、公式サイトのフォーラムから検索して調べた方が早いという場合もあります。英語、辛いですね。
AndEngineは基本的にはAndroidSDKのOpenGLを使いやすくしてくれているライブラリにすぎないので、いままでのandroidで培ってきたノウハウを利用することができます。Google AnalyticsもBugSenseも、あの広告SDKも、いままでのようにいつものライフサイクルの中に組み込むだけで実装できます。
(ただし広告SDKは別途xmlをどう表示させるかという話になってきますが)。
またゲーム開発に限っていえば、基本的な設計思想(ゲームとしての見せ方、スプライトやテクスチャなど)は他のクロスプラットフォーム開発でも同じなので、はじめにここから基本的なゲーム開発の実装方法を覚えてからcocos2d-xなどに手を出していくのもありなんじゃないかなあと思います。
あと個人的にクロスプラットフォームは、クロスプラットフォームという部分でみれば非常に有用ではあると思いますが、あくまでも「複数のプラットフォームで運用すること」が前提にあると思います。
「いまはandroidでしか作ってないけど、いつかiOSでも移植できるようにXPFを使うんだフフン」と思いながら、結局「いつかって、いつよ!」と言われて答えられないのであれば、最初からこうしてAndEngineを利用してAndroidの機能を存分に使うのも手ではないでしょうか。
あとは、既存のネイティブアプリにライブ壁紙を追加したい、OpenGLを利用した、いい感じにアニメーションするチュートリアル画面を追加したいなどの話が出た時には利用価値でてくるんじゃないかと思います。
あとはAndEngineには物理演算エンジンを追加させるための拡張プラグイン(ライブラリ)などがあるんですが、これを紹介していくともっと長くなるので、今回は省略します。
おつかれさまでした。最後におまけを残しておきます。
おまけ:複数解像度の対応方法
上記の内容では、端末解像度に関係なく同じ画像を利用するため、端末によっては表示内容が大きく変わってしまいます。
最初から1つのサイズで開発するのであれば問題ありませんが(例えば上記で紹介した書籍は、480x800のサイズ固定を前提にして書かれています)、やはりある程度、端末の解像度別で使用する画像サイズやCameraサイズを切り分けたくなってくると思います(拡大が大きいと、その分ピンぼけがかってくるようになるので)。
対応方法としては、複数のCameraサイズの定数を用意+複数のサイズの画像ファイルを用意して、内部の処理で切り替えていくしかないようです。
もしAndEngine自体に自動切り替えの処理が入っているのをご存知の方がいたら教えて下さい!
自分はだいたい、こんな感じで解像度をきりかえています。
1. 端末の解像度を見て切り替える
先ほどのソースにもかきましたがDisplayMetricsクラスが使えるので、そこから利用するフォルダを切り分けるという方法が1つ浮かびます。
// assets/img/
// + 480/
// + a.png
// + 640/
// + a.png
//
DisplayMetrics metrics = new DisplayMetrics();
getWindowManager().getDefaultDisplay().getMetrics(metrics);
String folderPath = "img/480/";
if(metrics.widthPixels >= 640){
folderPath = "img/640/";
}
is = mActivity.getResources().getAssets().open(folderPath + "jellybean.png");
当然ながら、どのサイズから切り替えるかどうかは各々で判断しなければいけません。
2. リソースを利用する
手っ取り早く、画像を全部res/drawableで管理するという方法があります。
Cameraサイズの設定は、/res/values-◯dpi/andengine.xmlでも作って
<?xml version="1.0" encoding="utf-8"?>
<resources>
<string name="andengine_dpi">hdpi</string>
</resources>
こんな感じで、同様にリソースファイルからCameraサイズの切替をできるようにしちゃえばいいと思います。
ただこちらの場合は画像をリソースフォルダで管理することになるので、命名規約やサブフォルダの都合で管理しづらくなるかなと思います。
リソースIDも無限ではないようなので注意しましょう。