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 は次の機能を提供します。

GameActivityAndroid Archive(AAR)として配布されます。この AAR には、AndroidManifest.xml で使用する Java クラスと、GameActivity の Java 側をアプリの C/C++ 実装に接続する C および C++ ソースコードが含まれています。GameActivity 1.2.2 以降を使用している場合は、C/C++ 静的ライブラリも提供されます。該当する場合は、ソースコードではなく静的ライブラリを使用することをおすすめします。

ネイティブ ライブラリとソースコードを CMake プロジェクトまたは NDK ビルドに公開する Prefab を通じて、これらのソースファイルまたは静的ライブラリをビルドプロセスの一部として組み込みます。

  1. Jetpack Android Games のページに記載されている手順に沿って、GameActivity ライブラリの依存関係をゲームの build.gradle ファイルに追加します。

  2. Android プラグイン バージョン(AGP)4.1 以上で次の操作を行って Prefab を有効にします。

    • モジュールの build.gradle ファイルの android ブロックに次の行を追加します。
    buildFeatures {
        prefab true
    }
    
    android.prefabVersion=2.0.0
    

    以前の AGP バージョンを使用している場合は、Prefab のドキュメントで対応する設定手順をご確認ください。

  3. 次のように、C/C++ 静的ライブラリまたは C/C++ ソースコードをプロジェクトにインポートします。

    静的ライブラリ

    プロジェクトの CMakeLists.txt ファイルで、game-activity 静的ライブラリを game-activity_static prefab モジュールにインポートします。

    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.txtGameActivity.cppGameTextInput.cppandroid_native_app_glue.c ファイルを含めます。

Android がアクティビティを起動する仕組み

Android システムは、アクティビティ ライフサイクルの特定の段階に対応するコールバック メソッドを呼び出すことにより、アクティビティ インスタンス内でコードを実行します。Android がアクティビティを起動してゲームを開始するためには、Android マニフェストで適切な属性を使用してアクティビティを宣言する必要があります。詳しくは、アクティビティの概要をご覧ください。

Android マニフェスト

すべてのアプリ プロジェクトでは、プロジェクトのソースセットのルートに AndroidManifest.xml ファイルを配置する必要があります。マニフェスト ファイルは、アプリに関する重要な情報を Android ビルドツール、Android オペレーティング システム、Google Play に提供します。これには、以下が含まれます。

  • パッケージ名とアプリ ID。Google Play でゲームを一意に識別するために使用されます。

  • アプリ コンポーネント。アクティビティ、サービス、ブロードキャスト レシーバ、コンテンツ プロバイダなどがあります。

  • 権限。システムまたは他のアプリの保護された領域にアクセスするために使用されます。

  • デバイスの互換性。これによってゲームのハードウェア要件とソフトウェア要件が規定されます。

  • GameActivityNativeActivity のネイティブ ライブラリ名(デフォルトは libmain.so)。

ゲームに GameActivity を実装する

  1. メイン アクティビティ Java クラス(AndroidManifest.xml ファイル内の activity 要素で指定されたクラス)を作成または指定します。このクラスを変更して、com.google.androidgamesdk パッケージからの GameActivity を拡張します。

    import com.google.androidgamesdk.GameActivity;
    
    public class YourGameActivity extends GameActivity { ... }
    
  2. 静的ブロックを使用してネイティブ ライブラリが起動時に読み込まれることを確認します。

    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");
      }
      ...
    }
    
  3. ネイティブ ライブラリ名がデフォルトの名前(libmain.so)以外の場合は、AndroidManifest.xml に追加します。

    <meta-data android:name="android.app.lib_name"
     android:value="android-game" />
    

android_main を実装する

  1. 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;
    }
    
  2. メインのゲームループで android_app を処理し、NativeAppGlueAppCmd で定義されているアプリ サイクル イベントのポーリングや処理などを行います。 たとえば、次のスニペットは関数 _hand_cmd_proxyNativeAppGlueAppCmd ハンドラとして登録し、アプリ サイクル イベントをポーリングして、登録したハンドラ(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_pollAll(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();
        }
      }
    }
    
  3. 詳細については、エンドレス トンネル NDK の実装例をご覧ください。次のセクションで説明するように、主な違いはイベントの処理方法にあります。

イベントを処理する

入力イベントがアプリに届くようにするには、イベント フィルタを作成し、android_app_set_motion_event_filterandroid_app_set_key_event_filter で登録します。デフォルトでは、native_app_glue ライブラリは SOURCE_TOUCHSCREEN 入力からのモーション イベントのみを許可します。詳しくは、リファレンス ドキュメントandroid_native_app_glue の実装コードをご確認ください。

入力イベントを処理するには、ゲームループで android_app_swap_input_buffers() を使用して android_input_buffer への参照を取得します。入力イベントには、前回のポーリング以降に発生したモーション イベントキーイベントが含まれます。含まれるイベントの数は、それぞれ motionEventsCountkeyEventsCount に格納されています。

  1. ゲームループ内で反復処理により各イベントを処理します。次のコード例では、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 サンプルをご覧ください。

  2. 完了したら、処理したイベントのキューを忘れずにクリアします。

    android_app_clear_motion_events(mApp);
    

参考情報

GameActivity について詳しくは、以下をご覧ください。

GameActivity に関連するバグの報告または機能のリクエストを行うには、GameActivity の Issue Tracker を使用してください。