こんにちは、Habr! 記事の第2部では、ゲームPacmanのクローンをどのように作成したかについての話を続けます。 最初の部分はここで読むことができます 。
前回パックマンに取り組んでから、約3週間が経過しました。 セッションのほとんどが終了し、少し長くなったので、続行することにしました。 この時点で、ゲームをGoogle Playマーケットに置くことができる状態にしたいという要望がありましたが、開発の最初の段階では考えさえしませんでした。 さらに、プレイ可能な状態に仕上げることは良いトレーニングです。 どこかで、ゲーム(そして実際にアプリケーション)を完成させるべきだと聞いた。
ゲームの開発はAndroid NDK(C ++)とOpenGL ES 2.0を使用して行われたことを思い出させてください。
最初に、ゲームの作業を完了するために必要だと思ったもののリストを作成しました。
- ボーナス
- テキスト出力
- 音楽と音
- 永続的なデータストレージ
- より美しいアニメーションとデザイン
より詳細に、ポイントごとに:
ボーナス
変更にはゲーム内のボーナスが必要です。 それらに多くの時間を費やさないために、新しい抽象クラス
Bonus
を導入し、そこからすぐに
LifeBonus
継承し
LifeBonus
。 ご
LifeBonus
、
LifeBonus
はプレイヤーに1つのライフを与えます。 ボーナスは、既存の階層に非常に有機的に適合していると言わざるを得ません。
これは私がやめた場所です。 他のボーナスの作成は非常に簡単で、ボーナスから継承するだけです。
ボーナスに関しては、
Statistics
クラスに言及する価値があります。 このクラスは、入場/退場/一時停止レベル、レベル内のポイントと時間のカウントなど、さまざまな統計を収集するために必要です。 これらの統計はすべて収集され、実績テーブルまたはオンラインの高得点テーブルを作成するために使用できます。 内部的に、
Statistics
クラスは決定論的な有限状態マシンとして実装されています。
テキスト出力
(IMHO)テキストの埋め込みには松葉杖が必要であるため、最初はテキスト情報なしでやりたいと思いました。 テキストなしで行うことは困難であることが判明し、その結論を実装するのが簡単でした。
テキストを表示するために簡単なトリックを使用しました。等幅フォントの文字のグラフィック表現は、図とほぼ同じタイプのテクスチャから長方形によって取得されます。
最初の文字はスペースで、残りは連続しています。 図の罫線は、便宜上のみ必要です(ベースラインと、すべての文字が整列しているという事実を確認できます)。 アプリケーションでは、テクスチャは同じですが、背景が透明です。 静的テクスチャを保存するのではなく、実行時にフォントをテクスチャにレンダリングする方がより正確ですが、これにより、複雑さが増します。 文字を長方形に配置する方法は明確ではありません。
Control
'aの子孫である特別なGUI要素である
Label
は、テキスト出力用に開発されました。 これは、ゲームウィンドウのタイトルバーで、ゲームの統計情報を表示するために使用されます。Win/ GameOverメニューでは、それぞれ勝ち負けについてプレイヤーに通知します。
音
まれなゲームでは音が出ないことがあります(おそらく、そのようなゲームにすぐに名前を付けることはできません)。 そこで、私もゲームにバックグラウンドミュージックとゲームサウンドを追加することにしました。
技術部
それ以前は、音の経験はありませんでした。 ここには少なくとも3つのオプションがあります。
- jniを使用し、Android SDKが提供するAPIを使用してサウンドを再生します
- OpenSL ESを使用する
- OpenALを使用する
私はそれが非常にエレガントなソリューションではないと思ったため、私はすぐに最初のオプションを拒否しました。 残りの2つの選択は、OpenSL ESを支持して行われました(これについての記事を書いたので、ここで招待を獲得しました)。
音楽を処理するために、
Audio
クラスが開発されました。このクラスには、このバックグラウンドミュージックをオンにし、サウンドをすばやく再生し、音楽とサウンドの可聴性を制御する静的メソッドのセットがあります。
ユーザーは、ゲームのメインメニューからコントロールします。このメニューには、コントロールの状態とボタンの類似性があり
CheckBox
。これは、
Control
'aから継承された
CheckBox
です。
作曲家パート
最初は、膨大な数の音楽サイトで公開されている音楽とサウンドを選択したかったのです。 しかし、このベンチャーは失敗しました。音楽を拾うのが私にとって問題だったからです。
幸いなことに、私の音楽家の友人ティムール・ラマザノフは私の助けに来て、私のために曲を書くことに同意しました。 個人的には、音楽はゲームのデザインと雰囲気に非常に適しているようです。 彼の他の作品に興味がある人は、 VKontakteまたはsoundcloudでそれらに慣れることができます
バックグラウンドミュージックは、ゲームミュージックとメニューの2つの部分に分かれています。 ループされ、ogg形式で保存されます。 ゲームサウンドはwav形式で保存されます。
情報を保存する
ゲーム中、さまざまな情報を永続的に保存する必要があります。 これは、たとえば、プレーヤーのレコードまたは彼のサウンド設定です。
このため、ラッパーはandroid.content.SharedPreferencesに上書きされます。 ラッパーにはjniを介してアクセスします。
ラッパーコード
public class StoreManager { public static final String PACMAN_PREFERENCES = "com_zagayevskiy_pacman_store"; private Context context; /* */ public StoreManager(Context _context){ context = _context; } /* . */ public void saveBoolean(String key, boolean value){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putBoolean(key, value); editor.commit(); } public boolean loadBoolean(String key, boolean defValue){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); return sp.getBoolean(key, defValue); } public void saveInt(String key, int value){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); SharedPreferences.Editor editor = sp.edit(); editor.putInt(key, value); editor.commit(); } public int loadInt(String key, int defValue){ SharedPreferences sp = context.getSharedPreferences(PACMAN_PREFERENCES, Context.MODE_PRIVATE); return sp.getInt(key, defValue); } }
jniを介してStoreManagerにアクセスするためのC ++コード
Store.h:
Store.cpp:
#include <stdlib.h> #include <stdio.h> #include <jni.h> class Store { public: static void init(JNIEnv* env, jobject _storeManager); static void saveBool(const char* name, bool value); static bool loadBool(const char* name, bool defValue); static void saveInt(const char* name, int value); static int loadInt(const char* name, int defValue); private: static JavaVM* javaVM; static jobject storeManager; static jclass storeManagerClass; static jmethodID saveBoolId; static jmethodID loadBoolId; static jmethodID saveIntId; static jmethodID loadIntId; static JNIEnv* getJNIEnv(JavaVM* jvm); };
Store.cpp:
/*env _storeManager */ void Store::init(JNIEnv* env, jobject _storeManager){ /* Java-, */ if(env->GetJavaVM(&javaVM) != JNI_OK){ LOGE("Can not Get JVM"); return; } storeManager = env->NewGlobalRef(_storeManager); if(!storeManager){ LOGE("Can not create NewGlobalRef on storeManager"); return; } storeManagerClass = env->GetObjectClass(storeManager); if(!storeManagerClass){ LOGE("Can not get StoreManager class"); return; } saveBoolId = env->GetMethodID(storeManagerClass, "saveBoolean", "(Ljava/lang/String;Z)V"); if(!saveBoolId){ LOGE("Can not find method saveBoolean"); return; } /* */ } } void Store::saveBool(const char* name, bool value){ LOGI("Store::saveBool(%s, %d)", name, value); JNIEnv* env = getJNIEnv(javaVM); if(!env){ LOGE("Can not getJNIEnv"); return; } jstring key = env->NewStringUTF(name); if(!key){ LOGE("Can not create NewStringUTF"); } env->CallVoidMethod(storeManager, saveBoolId, key, value); } bool Store::loadBool(const char* name, bool defValue){ LOGI("Store::loadBool(%s, %d)", name, defValue); JNIEnv* env = getJNIEnv(javaVM); if(!env){ LOGE("Can not getJNIEnv"); return defValue; } jstring key = env->NewStringUTF(name); if(!key){ LOGE("Can not create NewStringUTF"); } return env->CallBooleanMethod(storeManager, loadBoolId, key, defValue); } /* load/saveInt()*/ /* JNIEnv , Java-*/ JNIEnv* Store::getJNIEnv(JavaVM* jvm){ JavaVMAttachArgs args; args.version = JNI_VERSION_1_6; args.name = "PacmanNativeThread"; args.group = NULL; JNIEnv* result; if(jvm->AttachCurrentThread(&result, &args) != JNI_OK){ result = NULL; } return result; }
より美しいアニメーションとデザイン
最初は、パックマンだけが一緒にアニメ化されました。 アニメーションを(4フレームではなく)より美しくし、ボーナスと敵のアニメーションを作成したかったのです。 これらすべてを1つのスタイルで。
ある時点で、Pacman'aを火の玉の形にし、敵を水滴の形にするというアイデアが生まれました。
私にとって最も理想的なオプションは、美しいフレームごとのアニメーションを作成することでした。 プログラム計画には問題はありませんが、フレームの描画には問題があります。 デザイナーを見つけて、自分が欲しいものを正確に説明するという問題にぶつかりました。 私はこの問題を解決しませんでした。 その後、しばらく考えて、完全にプログラムされたアニメーションを作成することにしました。 そして、デザイナーは異なるサイズのタイルのみを注文しました。これには50ドルかかりました。
プログラムアニメーション
アニメーションを使いやすくするために、前述
IRenderable
2つの
IRenderable
を実装しました。「ループ」をアニメーション化するための
Plume
と「脈動」のための脈動です。
スクリーンショットでは、長さの異なるキャラクターはパックマンとモンスターであり、波紋は心臓の中心にある大きなポイントです。 そのため、マップ上に余命が表示されます。
両方のクラスの概念は、ブラシ効果に基づいています。 各ステップで、
Plume
クラスのオブジェクトは、アニメーション化されたオブジェクトの座標を受け取り、コンテナーキュー内にあることを記憶します(または、ループの目的の長さに応じて記憶しない-メモリーが多いほど、ループが短くなります)。 次に、既に保存されている座標を使用して、以下のようなテクスチャを使用して円が描画されます。
緑黒のグラデーションは、アルファチャネルグラデーションに対応しています。 緑は完全な不透明度、黒は完全な透明度です。 このテクスチャは、フラグメントシェーダを使用してゲームが初期化され、テクスチャにレンダリングされるときに生成されます。
座標が古いほど、描画される円の半径は小さくなります。 円は、ループオブジェクトの作成時に指定されたテクスチャオーバーレイで描画されます。 この場合、描画される座標に応じて、さらに、 アルキメデスのスパイラルの式に従って、テクスチャ座標がシフトされます(キャラクターが停止してもアニメーションがフリーズしないように)。
パックマンとモンスターのアニメーションには、異なるテクスチャを持つ異なる長さの列車が使用されます。 水と炎のテクスチャの追加要件は、それらを「ループ」することです。 関節は見えないはずです。 パックマン自身も、顎の動きのフレームごとのアニメーションを使用しています。
リップルは同様の方法で実装され、さまざまなサイズの勾配円が特定の周波数で単純に置き換えられます。
アプリケーション名とアイコン
名前を選ぶとき、私はこのゲームがパックマンのクローンであり、パックマンが燃えるという事実を打ち負かしたかった。 この場合、ナムコを怒らせないことが必要でした。 さまざまなオプションがありました:消防士、消防士、パイロマン、パックマン:ジョーズオブファイア。 最終的には、 Pyroman:Jaws of Fireに落ち着きました。 ゲームパックマンへの参照は、説明に残っています。
彼は、Photoshopでアプリケーションアイコンをペイントし、パックマンの燃えるようなものを打ち負かしました。 それは金魚のようになり、私の意見では、面白い=)
また、前回のThe GameノミネートのThe Tactrick Android Developer Cupコンテストへの参加についても話したいと思いました。 しかし、実際には、コンテストは突然終了したため、「WINNERS」のサイコロを勝者にぶら下げ、「Thank youありがとう」を他の人に送ることで、何も伝えることができません。 私は賞品の場所を請求しませんでしたが、私が順位表のどの場所になるかは面白かったです。 66のうち66にしましょうが、どういうわけかプログラムが評価されたことは明らかです。
ゲームはgithubで入手できます。
謝辞
理解とサポートをしてくれた彼女のユリアに感謝します。 最初のレベルは彼女に敬意を表して描かれています。
指導者と指導者にも感謝します-Bulat Tanirbergenは、すべてが私の力にあるという友好的な支持と確信を与えてくれました
ゲームのトラックについては、Ramazanov Timur-ありがとう。
ZeptoLabに感謝します。AndroidNDKを非常によく知っているおかげで、Sylvain RethabowilのNDKに関する本とStefan Duhurstの本「Slippery C ++ Places」を読んで、プログラミングレベルを上げてください。