GameActivity を使ってみる Android Game Development Kit の一部。
このガイドでは、Android ゲームで GameActivity をセットアップして統合し、イベントを処理する方法について説明します。
GameActivity は、重要な API を使用するプロセスを単純化することにより、C または C++ のゲームを Android に組み込む作業を支援します。以前は、ゲーム用として NativeActivity クラスが推奨されていました。GameActivity はそれに代わるゲーム用の推奨クラスであり、API レベル 19 と下位互換性があります。
GameActivity を統合するサンプルについては、games-samples リポジトリをご覧ください。
準備
ディストリビューションの入手については、GameActivity のリリースをご覧ください。
ビルドをセットアップする
Android では、Activity はゲームのエントリ ポイントとして機能するだけでなく、内部に描画できる Window も提供します。多くのゲームは、独自の Java クラスまたは Kotlin クラスでこの Activity を拡張して NativeActivity の制限を克服する一方で、JNI コードを使用して C または C++ ゲームのコードにブリッジします。
GameActivity は次の機能を提供します。
AppCompatActivityからの継承により、Android Jetpack アーキテクチャ コンポーネントを使用できるようにします。他の Android UI 要素とのやり取りを可能にする
SurfaceViewへのレンダリングを行います。Java アクティビティ イベントを処理します。これにより、C インターフェースを介して任意の Android UI 要素(
EditText、WebView、Adなど)をゲームに統合できます。NativeActivityに似た C API とandroid_native_app_glueライブラリを提供します。
GameActivity は Android Archive(AAR)として配布されます。この AAR には、AndroidManifest.xml で使用する Java クラスと、GameActivity の Java 側をアプリの C/C++ 実装に接続する C および C++ ソースコードが含まれています。GameActivity 1.2.2 以降を使用している場合は、C/C++ 静的ライブラリも提供されます。該当する場合は、ソースコードではなく静的ライブラリを使用することをおすすめします。
ネイティブ ライブラリとソースコードを CMake プロジェクトまたは NDK ビルドに公開する Prefab を通じて、これらのソースファイルまたは静的ライブラリをビルドプロセスの一部として組み込みます。
Jetpack Android Games のページに記載されている手順に沿って、
GameActivityライブラリの依存関係をゲームのbuild.gradleファイルに追加します。Android プラグイン バージョン(AGP)4.1 以上で次の操作を行って Prefab を有効にします。
- モジュールの
build.gradleファイルのandroidブロックに次の行を追加します。
buildFeatures { prefab true }- Prefab のバージョンを選び、
gradle.propertiesファイルに設定します。
android.prefabVersion=2.0.0以前の AGP バージョンを使用している場合は、Prefab のドキュメントで対応する設定手順をご確認ください。
- モジュールの
次のように、C/C++ 静的ライブラリまたは C/C++ ソースコードをプロジェクトにインポートします。
静的ライブラリ
プロジェクトの
CMakeLists.txtファイルで、game-activity静的ライブラリをgame-activity_staticprefab モジュールにインポートします。find_package(game-activity REQUIRED CONFIG) target_link_libraries(${PROJECT_NAME} PUBLIC log android game-activity::game-activity_static)ソースコード
プロジェクトの
CMakeLists.txtファイルで、game-activityパッケージをインポートしてターゲットに追加します。game-activityパッケージにはlibandroid.soが必要なため、ない場合はこれもインポートする必要があります。find_package(game-activity REQUIRED CONFIG) ... target_link_libraries(... android game-activity::game-activity)また、プロジェクトの
CmakeLists.txtにGameActivity.cpp、GameTextInput.cpp、android_native_app_glue.cファイルを含めます。
Android がアクティビティを起動する仕組み
Android システムは、アクティビティ ライフサイクルの特定の段階に対応するコールバック メソッドを呼び出すことにより、アクティビティ インスタンス内でコードを実行します。Android がアクティビティを起動してゲームを開始するためには、Android マニフェストで適切な属性を使用してアクティビティを宣言する必要があります。詳しくは、アクティビティの概要をご覧ください。
Android マニフェスト
すべてのアプリ プロジェクトでは、プロジェクトのソースセットのルートに AndroidManifest.xml ファイルを配置する必要があります。マニフェスト ファイルは、アプリに関する重要な情報を Android ビルドツール、Android オペレーティング システム、Google Play に提供します。これには、以下が含まれます。
パッケージ名とアプリ ID。Google Play でゲームを一意に識別するために使用されます。
アプリ コンポーネント。アクティビティ、サービス、ブロードキャスト レシーバ、コンテンツ プロバイダなどがあります。
権限。システムまたは他のアプリの保護された領域にアクセスするために使用されます。
デバイスの互換性。これによってゲームのハードウェア要件とソフトウェア要件が規定されます。
GameActivityとNativeActivityのネイティブ ライブラリ名(デフォルトは libmain.so)。
ゲームに GameActivity を実装する
メイン アクティビティ Java クラス(
AndroidManifest.xmlファイル内のactivity要素で指定されたクラス)を作成または指定します。このクラスを変更して、com.google.androidgamesdkパッケージからのGameActivityを拡張します。import com.google.androidgamesdk.GameActivity; public class YourGameActivity extends GameActivity { ... }静的ブロックを使用してネイティブ ライブラリが起動時に読み込まれることを確認します。
public class EndlessTunnelActivity extends GameActivity { static { // Load the native library. // The name "android-game" depends on your CMake configuration, must be // consistent here and inside AndroidManifect.xml System.loadLibrary("android-game"); } ... }ネイティブ ライブラリ名がデフォルトの名前(
libmain.so)以外の場合は、AndroidManifest.xmlに追加します。<meta-data android:name="android.app.lib_name" android:value="android-game" />
android_main を実装する
android_native_app_glueライブラリは、メインスレッドでのブロックを防止する目的で、ゲームがGameActivityのライフサイクル イベントを別のスレッドで管理するために使用するソースコード ライブラリです。このライブラリを使用する際は、タップ入力イベントなどのライフサイクル イベントを処理するコールバックを登録します。GameActivityアーカイブには独自のバージョンのandroid_native_app_glueライブラリが含まれているため、NDK リリースに含まれるバージョンは使用できません。NDK に含まれるandroid_native_app_glueライブラリをゲームで使用している場合は、GameActivityバージョンに切り替えます。プロジェクトに
android_native_app_glueライブラリのソースコードを追加すると、このライブラリはGameActivityとやり取りします。android_mainという関数を実装します。この関数は、ライブラリによって呼び出され、ゲームのエントリ ポイントとして使用されます。この関数にはandroid_appという構造体が渡されます。これは、ゲームやエンジンによって異なる場合があります。次の例をご覧ください。#include <game-activity/native_app_glue/android_native_app_glue.h> extern "C" { void android_main(struct android_app* state); }; void android_main(struct android_app* app) { NativeEngine *engine = new NativeEngine(app); engine->GameLoop(); delete engine; }メインのゲームループで
android_appを処理し、NativeAppGlueAppCmd で定義されているアプリ サイクル イベントのポーリングや処理などを行います。 たとえば、次のスニペットは関数_hand_cmd_proxyをNativeAppGlueAppCmdハンドラとして登録し、アプリ サイクル イベントをポーリングして、登録したハンドラ(android_app::onAppCmd内)にそれらのイベントを処理のために送信します。void NativeEngine::GameLoop() { mApp->userData = this; mApp->onAppCmd = _handle_cmd_proxy; // register your command handler. mApp->textInputState = 0; while (1) { int events; struct android_poll_source* source; // If not animating, block until we get an event; // If animating, don't block. while ((ALooper_pollOnce(IsAnimating() ? 0 : -1, NULL, &events, (void **) &source)) >= 0) { if (source != NULL) { // process events, native_app_glue internally sends the outstanding // application lifecycle events to mApp->onAppCmd. source->process(source->app, source); } if (mApp->destroyRequested) { return; } } if (IsAnimating()) { DoFrame(); } } }詳しくは、エンドレス トンネル NDK の実装例をご覧ください。次のセクションで説明するように、主な違いはイベントの処理方法にあります。
イベントを処理する
入力イベントがアプリに届くようにするには、イベント フィルタを作成し、android_app_set_motion_event_filter と android_app_set_key_event_filter で登録します。デフォルトでは、native_app_glue ライブラリは SOURCE_TOUCHSCREEN 入力からのモーション イベントのみを許可します。詳しくは、リファレンス ドキュメントと android_native_app_glue の実装コードをご確認ください。
入力イベントを処理するには、ゲームループで android_app_swap_input_buffers() を使用して android_input_buffer への参照を取得します。入力イベントには、前回のポーリング以降に発生したモーション イベントとキーイベントが含まれます。含まれるイベントの数は、それぞれ motionEventsCount と keyEventsCount に格納されています。
ゲームループ内で反復処理により各イベントを処理します。次のコード例では、
motionEventsに対する反復処理により、それらをhandle_eventで処理しています。android_input_buffer* inputBuffer = android_app_swap_input_buffers(app); if (inputBuffer && inputBuffer->motionEventsCount) { for (uint64_t i = 0; i < inputBuffer->motionEventsCount; ++i) { GameActivityMotionEvent* motionEvent = &inputBuffer->motionEvents[i]; if (motionEvent->pointerCount > 0) { const int action = motionEvent->action; const int actionMasked = action & AMOTION_EVENT_ACTION_MASK; // Initialize pointerIndex to the max size, we only cook an // event at the end of the function if pointerIndex is set to a valid index range uint32_t pointerIndex = GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT; struct CookedEvent ev; memset(&ev, 0, sizeof(ev)); ev.motionIsOnScreen = motionEvent->source == AINPUT_SOURCE_TOUCHSCREEN; if (ev.motionIsOnScreen) { // use screen size as the motion range ev.motionMinX = 0.0f; ev.motionMaxX = SceneManager::GetInstance()->GetScreenWidth(); ev.motionMinY = 0.0f; ev.motionMaxY = SceneManager::GetInstance()->GetScreenHeight(); } switch (actionMasked) { case AMOTION_EVENT_ACTION_DOWN: pointerIndex = 0; ev.type = COOKED_EVENT_TYPE_POINTER_DOWN; break; case AMOTION_EVENT_ACTION_POINTER_DOWN: pointerIndex = ((action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); ev.type = COOKED_EVENT_TYPE_POINTER_DOWN; break; case AMOTION_EVENT_ACTION_UP: pointerIndex = 0; ev.type = COOKED_EVENT_TYPE_POINTER_UP; break; case AMOTION_EVENT_ACTION_POINTER_UP: pointerIndex = ((action & AMOTION_EVENT_ACTION_POINTER_INDEX_MASK) >> AMOTION_EVENT_ACTION_POINTER_INDEX_SHIFT); ev.type = COOKED_EVENT_TYPE_POINTER_UP; break; case AMOTION_EVENT_ACTION_MOVE: { // Move includes all active pointers, so loop and process them here, // we do not set pointerIndex since we are cooking the events in // this loop rather than at the bottom of the function ev.type = COOKED_EVENT_TYPE_POINTER_MOVE; for (uint32_t i = 0; i < motionEvent->pointerCount; ++i) { _cookEventForPointerIndex(motionEvent, callback, ev, i); } break; } default: break; } // Only cook an event if we set the pointerIndex to a valid range, note that // move events cook above in the switch statement. if (pointerIndex != GAMEACTIVITY_MAX_NUM_POINTERS_IN_MOTION_EVENT) { _cookEventForPointerIndex(motionEvent, callback, ev, pointerIndex); } } } android_app_clear_motion_events(inputBuffer); }_cookEventForPointerIndex()とその他の関連する関数の実装については、GitHub サンプルをご覧ください。完了したら、処理したイベントのキューを忘れずにクリアします。
android_app_clear_motion_events(mApp);
参考情報
GameActivity について詳しくは、以下をご覧ください。
- GameActivity と AGDK のリリースノート
- GameActivity で GameTextInput を使用する
- NativeActivity の移行ガイド
- GameActivity リファレンス ドキュメント
- GameActivity の実装
GameActivity に関連するバグの報告または機能のリクエストを行うには、GameActivity の Issue Tracker を使用してください。