過去数か月間、暇なときに、Android OS用の有名なパックマンゲームのクローンを開発してきました。 それがどうだったのか、それが何をもたらしたのかを伝えたい。
なぜパックマンなのか?
この質問への答えは非常に簡単で、おそらく読者の一人がすでに考えにちらついています。 はい、そうです、それはZeptoLabのテストケースでした。 この割り当ての要件から、開発で使用されるツールも明確です:Android NDK、C ++、OpenGL。
パックマン:始まり
したがって、決定が下され、ツールが選択されます。 次は? Android NDKの経験はありません。OpenGLの経験は、コンピューターグラフィックスの2011 コースの理論と実験室の作業に限られています。 C ++開発経験はありますが、ゲーム開発者ではありません。 さあ、始めましょう。
私が最初にしたことは、Android NDKをインストールし(インストールはさまざまなリソース(外部およびrunetの両方)で詳しく説明されています)、その配信からいくつかの例を実行しました:最初にGL2JNIの例、次にSanAngelesです。 最初はOpenGL ES 2.0を使用して三角形を描画し、2番目はOpenGL ESを使用してプリミティブから組み立てられた3Dムービーを表示します。 コードは一見不気味に見えます。 はい、2番目もです。 実際、GL2JNIプロジェクトのgl_code.cppファイルは、私にとっての出発点でした。
OpenGL ES 2.0を選ぶ理由
もちろん、単純な2Dゲームの場合、OpenGL ES静的パイプラインで十分であり、シェーダーは必要ありません。これはなぜですか? 回答:試してみたかったです。 コンピューターグラフィックスの実験室での作業では、シェーダーに触れることはできませんでした。
OpenGL ES 2.0の研究は、ここから始まり、habrahabrの記事「 OpenGL ES 2.xについてのすべて-(パート1/3)」の翻訳から始まりました。 残念ながら、私は今翻訳を見つけることができません。おそらく著者はそれをドラフトコピーに入れました。 この記事の著者は、iOSに関連するOpenGL ES 2.xについて語っていますが、Androidについてもほぼ同じことが言えます。 ロシア語の最初の部分を読んで、これで十分ではないことに気付いた後、私は英語のリソース(主に上記の記事のパート2と3ですが、他のソースも使用しました)に急いで行き、そこでOpenGL ES 2.0に関するすべての知識を得ました。 。
スクリーンショットの時間です

これは私の将来のゲームの最初のスクリーンショットの1つです。 一番最初のものはあらゆる種類のスクリーンで、三角形とラスタマンの三角形で舗装されており、色が幻想的に変化しています。
この一見原始的なスクリーンショットで何が見えますか? 十分です:
- テクスチャの読み込み。 はい、それは重要です、それは大きな一歩でした。
- 地図。 それは、壁の上に食べ物と空の場所が区別されています。
- GUI 左下隅にボタンが表示されます。 はい、それが最初のボタンでした。
各アイテムの詳細:
テクスチャの読み込み
Android NDKでのテクスチャの読み込みは、いくつかの方法で可能です。
- libpngとlibzipを使用してapkリソースファイルに直接アクセスします。
- AAssetManagerを使用して、ファイルを読み取り、その内容を画像として手動で解釈します。
- jniを使用してandroid.graphics.Bitmapにアクセスします。
おそらくまだ方法がありますが、私はそれらを見つけていません。 3番目のオプションが最も受け入れられるように思えたので、実装しました。
少しのJavaコード
import android.content.res.AssetManager; import android.graphics.Bitmap; import android.graphics.BitmapFactory; /* Bitmap, jni*/ public class PngManager { private AssetManager amgr; public PngManager(AssetManager manager){ amgr = manager; } public Bitmap open(String path){ try{ return BitmapFactory.decodeStream(amgr.open(path)); }catch (Exception e) { } return null; } public int getWidth(Bitmap bmp) { return bmp.getWidth(); } public int getHeight(Bitmap bmp) { return bmp.getHeight(); } public void getPixels(Bitmap bmp, int[] pixels){ int w = bmp.getWidth(); int h = bmp.getHeight(); bmp.getPixels(pixels, 0, w, 0, 0, w, h); } public void close(Bitmap bmp){ bmp.recycle(); } }
少しのC ++コード
/* */ struct Texture{ char* pixels; /*should be allocated with new[width*height*4]; RGBA*/ int width; int height; Texture(): pixels(NULL), width(0), height(0){} Texture(char* p, int w, int h): pixels(p), width(w), height(h){}; ~Texture(){ if(pixels){ delete[] pixels; pixels = NULL; } } }; /* , . jni Java */ void Art::init(JNIEnv* env, jint _screenWidth, jint _screenHeight, jobject _pngManager, jobject javaAssetManager){ LOGI("Art::init"); free(env); /* - */ pngManager = env->NewGlobalRef(_pngManager); pmEnv = env; pmClass = env->GetObjectClass(pngManager); pmOpenId = env->GetMethodID(pmClass, "open", "(Ljava/lang/String;)Landroid/graphics/Bitmap;"); pmCloseId = env->GetMethodID(pmClass, "close", "(Landroid/graphics/Bitmap;)V"); pmGetWidthId = env->GetMethodID(pmClass, "getWidth", "(Landroid/graphics/Bitmap;)I"); pmGetHeightId = env->GetMethodID(pmClass, "getHeight", "(Landroid/graphics/Bitmap;)I"); pmGetPixelsId = env->GetMethodID(pmClass, "getPixels", "(Landroid/graphics/Bitmap;[I)V"); /*...*/ } /* jni */ Texture* Art::loadPng(const char* filename){ LOGI("Art::loadPng(%s)", filename); Texture* result = new Texture(); jstring name = pmEnv->NewStringUTF(filename); jobject png = pmEnv->CallObjectMethod(pngManager, pmOpenId, name); pmEnv->DeleteLocalRef(name); pmEnv->NewGlobalRef(png); jint width = pmEnv->CallIntMethod(pngManager, pmGetWidthId, png); jint height = pmEnv->CallIntMethod(pngManager, pmGetHeightId, png); jintArray array = pmEnv->NewIntArray(width * height); pmEnv->NewGlobalRef(array); pmEnv->CallVoidMethod(pngManager, pmGetPixelsId, png, array); jint *pixels = pmEnv->GetIntArrayElements(array, 0); result->pixels = argb2rgba((unsigned int*)pixels, width, height); result->width = width; result->height = height; pmEnv->ReleaseIntArrayElements(array, pixels, 0); pmEnv->CallVoidMethod(pngManager, pmCloseId, png); return result; }
Art
クラスに立ち寄るのは少し価値があります。 静的メソッドとメンバーを使用したこのクラスのアイデアは、クラスの名前と同じく、 Notch (彼のオープンソースゲームの1つから)から取ったものです。 テクスチャ、音楽、サウンドなど、アートに関連するすべてを保存します。 このクラスには、静的メソッド
init(), free(), getTexture(int id)
、および
init(), free(), getTexture(int id)
ます。
地図
開発の最初から、レベルをロードするメカニズムを作成する方法を考えていました。 「ハードコード」および「テキストファイルから読み取る」オプションが思い浮かびます。 もちろん、これは可能ですが、カードの簡単な編集について話す必要はありません。 レベルエディターについての考えが頭に浮かぶと、最近私はおいしい記事を見ました...いいえ! そのため、森の木々は見えなくなります。 目標はパックマンとできるだけ早く働くことです。
しかし、私はPNGをダウンロードする方法を学びました! 異なる色のピクセルは、壁、食物、空きスペース、パックマンなどの細胞を示します。 また、ペイントはレベルエディターとして非常に適しています。 カードのサイズは、最大32x32までさまざまです。
このアプローチは、100%実証されています。 私はほとんど無料で非常に簡単に編集可能なレベルを得ました。
GUI
私にとって別の問題は、グラフィカルユーザーインターフェイスでした。 ユーザーがボタンをクリックしたり、スワイプしたりできるようにする方法は? これにどのように反応しますか? ソフトウェア部分を整理することはどれほど便利ですか? 私自身は、これらの質問に自分で答えました。 クラス図では、私の答えは次のようになります。

Control
が継承され、
Label, Button, CheckBox
および
Menu
(
Contro
リストを含む)が継承される基本的な抽象クラス
IRenderable
があり
Label, Button, CheckBox
。 他のすべてのメニュー(
Pause/Game/GameOver
など)は
Menu
から継承され
Menu
。 必要なことはすべて-作成時に、メニューがアクティブになったときに表示されるコントロール(
Button, Label
など)のリストを定義します。 さらに、イベントに対する反応を判断できます(たとえば、
GameMenu
はスワイプを追跡します)。
render()
メソッドは、前のクラスから次の各クラスを呼び出します。
さらに、ゲームのグローバルロジックを担当する
Engine
クラスがあります。 ダウンロード、メニューの切り替え、ユーザーアクションに関するメッセージの受信など。 これには
Menu* currentMenu
があり、ユーザーのアクションに対する反応を要求します。
Engine
は
Menu::onShow()
も呼び出し
Menu::onShow()
。
ゲームロジック
ゲームでは、パックマンに加えて、敵のモンスターがいます。 彼らはそれぞれ、迷路を通って移動し、壁と衝突し、最後にはお互いを食べることができなければなりません。 それらのロジックは、ステートマシン(StateMachine)として実装されます。 統一のために、階層が構築されました。

StupidMonster
と
CleverMonster
は、
newDirectionEvent()
メソッドによって定義される動作が異なります
StupidMonster
、
Pacman
'aに注意を払わずに、迷路をランダムに歩きます。
CleverMonster
、最適なルートで
Pacman
を追いかけています。 この時点で、実際に自転車を実装しました。これは科学的に「戦略」デザインパターンと呼ばれています
最良の方法を探してください。
マップは配列で表されるため、パス検索を行うことは難しくありません。 波面を保存した、わずかに修正された波アルゴリズムを実装しました。 迷路に沿って移動できるのは方向ではなく4方向だけなので、実装は非常に簡単です。
CleverMonster
クラスには、マップ
CleverMonster
静的フィールド
CleverMonster
ます。これは、カードの配列(サイズはマップのサイズです)です。
CleverMonster
の最初のインスタンスを作成すると、この配列にメモリが割り当てられます。 最後のインスタンスが破棄されると、メモリはクリアされます。 これは、作成されたオブジェクトの数をカウントすることにより実装されます。

あらゆる瞬間にパスを見つけるためのアルゴリズム:
- パックマンの座標(pX、pY)を調べます。
- 座標でマップの配列を調べてください:maps [pX、pY];
- 対応するマップがまだ作成されていない場合(NULL)、手順3に進みます。
- それ以外の場合は、手順5に進みます
- オリジナルと同じサイズのマップを作成し、マップ[pX、pY]に保存します。
- ウェーブアルゴリズムを使用して、作成されたマップをセル(pX、pY)から完全に埋めます。
- マップマップ[pX、pY]に基づいてパスを作成します。
これらのトリックは、同じルートを繰り返しカウントしないために必要です。 すべてのスマートモンスターは同じカードを使用します。 各マップは複数回作成されません。 非常に多くのカードが作成されることはありません(パックマンは壁を通り抜けられないため)。
最悪の場合に20x20セルを測定する壁のないカードの場合、20 * 20 * 20 * 20 * 4 = 640,000バイトまたは625キロバイトのメモリが消費されます。 実際には、壁の存在により、この数ははるかに小さくなっています。
最初の部分の結果
それは、ほぼ3週間の開発の結果でした。 モンスターはパックマンを追いかけ、パックマンは食べ物を食べ、3回死ぬ方法を知っています。 ユーザーはゲームを一時停止したり、勝ったときに次のレベルに移動したり、メニューに戻って別のレベルを選択したりできます。 パックマンはアニメーション化されていますが、モンスターはアニメーション化されていません。 パックマンアニメーションについては、次のパートで詳しく説明します。
このフォームでは、課題はレビューのためにZeptoLabに送信され、その後インタビューに招待されました。 率直に言って-その前でも後でも、私はそれほど単純な質問にそれほど心配もせず、愚かでもありませんでした。 それは壮大な失敗のように感じました。 2月に再会することを申し出たアルゴリズムとC ++に関する本をいくつか読むことを勧められました。 ゲームに関するHRレビューがありました:「最高の提出物の1つ」。 ところで、私はまだ仕事を探しています。
このプロジェクトはgithubで公開されています。
2番目の部分へのリンク 。