ゲームループ テストを使ってみる

ゲームアプリが異なる UI フレームワーク上に構築されている場合、ゲームのテストを自動化するのは簡単なことではありません。ゲームループ テストでは、ネイティブ テストを Test Lab と統合し、選択したデバイスで簡単に実行できます。ゲームループ テストでは、実際のプレーヤーの動作をシミュレートしながら、ゲームアプリでテストを行うことができます。このガイドでは、ゲームループ テストを実行し、Firebase コンソールにテスト結果を表示して管理する方法について説明します。

ゲームエンジンに応じて、1 つまたは複数のループを含むテストを実装できます。ループとは、ゲームアプリのテスト全体またはその一部を指します。ゲームループは次の目的で使用できます。

  • エンドユーザーがプレイするのと同じ方法でゲームのレベルを実行する。ユーザーの入力はスクリプト化できます。また、ユーザーをアイドル状態にすることもできます。ゲームに AI を実装している場合は、ユーザーを AI で置き換えることも可能です(たとえば、カーレース ゲームに AI を実装している場合は、ユーザーの入力の代わりに AI のドライバを簡単に配置できます)。
  • 最高品質の設定でゲームを実行し、デバイスの対応状況を確認する。
  • 技術的なテストを実行する。たとえば、複数のシェーダーをコンパイルして実行し、予期した結果が出力されるかどうか確認します。

ゲームループ テストは、1 つのテストデバイスまたは一連のテストデバイスで行うことができます。また、Test Lab で実施することもできます。ただし、グラフィック フレームレートが実機よりも低いため、仮想デバイス上でゲームループ テストを行うことはおすすめしません。

始める前に

テストを実装する前に、ゲームループ テスト用にアプリを構成する必要があります。

  1. アプリ マニフェストで、アクティビティに新しいインテント フィルタを追加します。

    <activity android:name=".MyActivity">
       <intent-filter>
           <action android:name="com.google.intent.action.TEST_LOOP"/>
           <category android:name="android.intent.category.DEFAULT"/>
           <data android:mimeType="application/javascript"/>
       </intent-filter>
       <intent-filter>
          ... (other intent filters here)
       </intent-filter>
    </activity>
    

    これにより、Test Lab が特定のインテントでゲームを開始できるようになります。

  2. コードに以下のものを追加します(onCreate メソッド宣言内に追加することをおすすめします)。

    Kotlin+KTX

    val launchIntent = intent
    if (launchIntent.action == "com.google.intent.action.TEST_LOOP") {
        val scenario = launchIntent.getIntExtra("scenario", 0)
        // Code to handle your game loop here
    }

    Java

    Intent launchIntent = getIntent();
    if(launchIntent.getAction().equals("com.google.intent.action.TEST_LOOP")) {
        int scenario = launchIntent.getIntExtra("scenario", 0);
        // Code to handle your game loop here
    }

    これにより、アクティビティは起動するインテントを確認できます。このコードは後で追加することもできます(ゲームエンジンを最初に読み込んだ後など)。

  3. 推奨: テストの最後に以下のものを追加します。

    Kotlin+KTX

    yourActivity.finish()

    Java

    yourActivity.finish();

    これにより、ゲームループ テストの完了時点でアプリが終了します。このテストは、アプリの UI フレームワークを使用して次のループを開始します。アプリを終了すると、テストの終了を通知します。

ゲームループ テストを作成して実行する

ゲームループ テスト用にアプリを構成したら、すぐにテストを作成してゲームアプリで実行できます。Test Lab のテストは、Firebase コンソールまたは gcloud コマンドライン インターフェース(CLI)で実施できます。また、Test Loop Manager を使用してローカル デバイスでテストを行うこともできます。

ローカル デバイスで実行する

Test Lab の Test Loop Manager は、ゲームループ テストを統合してローカル デバイスでテストを実施できるオープンソース アプリです。品質保証チームは自身のデバイスで同じゲームループを実行できます。

Test Loop Manager を使用してローカル デバイスでテストを行うには:

  1. スマートフォンまたはタブレットに Test Loop Manager をダウンロードして、次のコマンドでインストールします。
    adb install testloopmanager.apk
  2. スマートフォンまたはタブレットで Test Loop Apps アプリを開きます。デバイス上でゲームループを実行できるアプリのリストが表示されます。ここにゲームアプリが表示されていない場合は、始める前にの最初のステップで説明したインテント フィルタを使用しているかどうか確認してください。
  3. ゲームアプリを選択し、実行するループの数を選択します。注: このステップでは、1 つのループではなく、ループのサブセットを実行するように選択できます。複数のループを同時に実行する方法については、オプション機能をご覧ください。
  4. [Run Test] をクリックします。テストがすぐに始まります。

Test Lab で実行する

Test Lab でゲームループ テストを行うには、Firebase コンソールまたは gcloud CLI を使用します。テストを始める前に、Firebase コンソールを開いてプロジェクトを作成します。

Firebase コンソールを使用する

  1. Firebase コンソールで、左パネルから [Test Lab] をクリックします。
  2. [最初のテストの実行] をクリックします(プロジェクトですでにテストを実行している場合は [テストを実行] をクリックします)。
  3. テストの種類に [ゲームループ] を選択し、[続行] をクリックします。
  4. [参照] をクリックして、アプリの .apk ファイルを選択します。注: このステップでは、1 つのループではなく、ループのサブセットを実行するように選択できます。複数のループを同時に実行する方法については、オプション機能をご覧ください。
  5. [続行] をクリックします。
  6. アプリのテストに使用する実機を選択します。
  7. [テストを開始] をクリックします。

Firebase コンソールの使い方については、Firebase コンソールでテストを開始するをご覧ください。

gcloud コマンドライン(CLI)を使用する

  1. Google Cloud SDK をまだインストールしていない場合は、ダウンロードしてインストールします。

  2. Google アカウントを使用して gcloud CLI にログインします。

    gcloud auth login

  3. gcloud で Firebase プロジェクトを設定します。PROJECT_ID は Firebase プロジェクトの ID です。

    gcloud config set project PROJECT_ID
    
  4. 最初のテストを実行します。

    gcloud firebase test android run \
     --type=game-loop --app=<var>path-to-apk</var> \
     --device model=herolte,version=23
    

gcloud CLI の使い方については、gcloud コマンドラインからテストを開始するをご覧ください。

オプション機能

Test Lab には、出力データの書き込み、複数のゲームループのサポート、関連するループのラベル付けなど、テストをカスタマイズできるオプション機能がいくつかあります。

出力データを書き込む

ゲームループ テストでは、launchIntent.getData() メソッドに指定したファイルに出力を書き込むことができます。テストを行った後、Firebase コンソールの [Test Lab] セクションでこの出力データを確認できます(ゲームループ テストの出力ファイルの例をご覧ください)。

Test Lab では、アプリ間でファイルを共有する場合のベスト プラクティスを実装しています(詳細はファイルの共有をご覧ください)。インテントを取得しているアクティビティの onCreate() メソッドで次のコードを実行すると、データ出力ファイルを確認できます。

Kotlin+KTX

val launchIntent = intent
val logFile = launchIntent.data
logFile?.let {
    Log.i(TAG, "Log file ${it.encodedPath}")
    // ...
}

Java

Intent launchIntent = getIntent();
Uri logFile = launchIntent.getData();
if (logFile != null) {
    Log.i(TAG, "Log file " + logFile.getEncodedPath());
    // ...
}

ゲームアプリの C++ 側からファイルに書き込む場合には、次の例のように、ファイルパスではなくファイル記述子を渡します。

Kotlin+KTX

val launchIntent = intent
val logFile = launchIntent.data
var fd = -1
logFile?.let {
    Log.i(TAG, "Log file ${it.encodedPath}")
    fd = try {
        contentResolver
            .openAssetFileDescriptor(logFile, "w")!!
            .parcelFileDescriptor
            .fd
    } catch (e: FileNotFoundException) {
        e.printStackTrace()
        -1
    } catch (e: NullPointerException) {
        e.printStackTrace()
        -1
    }
}

// C++ code invoked here.
// native_function(fd);

Java

Intent launchIntent = getIntent();
Uri logFile = launchIntent.getData();
int fd = -1;
if (logFile != null) {
    Log.i(TAG, "Log file " + logFile.getEncodedPath());
    try {
        fd = getContentResolver()
                .openAssetFileDescriptor(logFile, "w")
                .getParcelFileDescriptor()
                .getFd();
    } catch (FileNotFoundException e) {
        e.printStackTrace();
        fd = -1;
    } catch (NullPointerException e) {
        e.printStackTrace();
        fd = -1;
    }
}

// C++ code invoked here.
// native_function(fd);

C++

#include <unistd.h>
JNIEXPORT void JNICALL
Java_my_package_name_MyActivity_native_function(JNIEnv *env, jclass type, jint log_file_descriptor) {
// The file descriptor needs to be duplicated.
int my_file_descriptor = dup(log_file_descriptor);
}

出力ファイルの例

次のような形式の出力データファイルを使用して、Firebase コンソールの [Test Lab] セクションにゲームループ テストの結果を表示できます。/.../ の部分には、このファイル内の他のフィールド名と競合しない限り、任意のカスタム フィールドを含めることができます。

{
  "name": "test name",
  "start_timestamp": 0, // Timestamp of the test start (in us).
                           Can be absolute or relative
  "driver_info": "...",
  "frame_stats": [
    {
      "timestamp": 1200000, // Timestamp at which this section was written
                               It contains value regarding the period
                               start_timestamp(0) -> this timestamp (1200000 us)
      "avg_frame_time": 15320, // Average time to render a frame in ns
      "nb_swap": 52, // Number of frame rendered
      "threads": [
        {
          "name": "physics",
          "Avg_time": 8030 // Average time spent in this thread per frame in us
        },
        {
          "name": "AI",
          "Avg_time": 2030 // Average time spent in this thread per frame in us
        }
      ],
      /.../ // Any custom field you want (vertices display on the screen, nb units …)
    },
    {
      // Next frame data here, same format as above
    }
  ],
  "loading_stats": [
    {
      "name": "assets_level_1",
      "total_time": 7850, // in us
      /.../
    },
    {
      "name": "victory_screen",
      "total_time": 554, // in us
      /.../
    }

  ],
  /.../, // You can add custom fields here
}

複数のゲームループ

アプリ内で複数のゲームループを実行すると便利な場合があります。ループは、ゲームアプリを最初から最後まで通しで実行するものです。ゲームに複数のレベルがある場合、1 つのループですべてのレベルを順に処理するのではなく、各レベルを 1 つのゲームループで起動することが望ましい場合があります。これにより、アプリがレベル 32 でクラッシュした場合、そのゲームループを直接起動してクラッシュを再現し、バグの修正をテストできます。

アプリで複数のループを一度に実行できるようにするには:

  • Test Loop Manager を使用してテストを行う場合:

    1. アプリのマニフェストで <application> 要素内に次の行を追加します。

      <meta-data
        android:name="com.google.test.loops"
        android:value="5" />
      

      この起動インテントには、対象のループが整数パラメータとして含まれます。android:value フィールドには、1~1024 の整数を指定します(1024 は 1 回のテストで許可される最大ループ数です)。ループは 0 ではなく 1 から開始します。

    2. Test Loop Manager アプリで、実行するループを選択できる画面が表示されます。複数のループを選択した場合は、前のループが完了した後に次のループが順番に起動されます。

  • Firebase コンソールでテストを行う場合は、[シナリオ] フィールドにループ番号のリストまたは範囲を入力します。

  • gcloud CLI でテストを行う場合は、--scenario-numbers フラグを使用してループ番号のリストを指定します。たとえば、--scenario-numbers=1,3,5 はループ 1、3、5 を実行します。

  • C++ を記述し、ループの動作を変更する場合は、次のコードを C++ のネイティブ コードに渡します。

    Kotlin+KTX

    val launchIntent = intent
    val scenario = launchIntent.getIntExtra("scenario", 0)

    Java

    Intent launchIntent = getIntent();
    int scenario = launchIntent.getIntExtra("scenario", 0);

    これで、結果の int 値に基づいてループの動作を変更できるようになります。

ゲームループにラベルを適用する

ゲームループに 1 つ以上のシナリオラベルを適用すると、QA チームが一連の関連ゲームループ(たとえば、互換性のあるすべてのゲームループ)を簡単に起動し、1 つのマトリックスでテストを実施できるようになります。独自のラベルを作成することも、Test Lab が提供する事前定義のラベルを使用することもできます。

  • com.google.test.loops.player_experience: ゲームでのユーザーの体験を再現するループに使用します。このループでテストする目的は、実際のユーザーがゲームをプレイしているときに発生する問題を確認することです。
  • com.google.test.loops.gpu_compatibility: GPU 関連の問題をテストするループに使用します。このループで実行するテストの目的は、本番環境で正しく動作しないおそれのある GPU コードを実行し、ハードウェアやドライバの問題を明らかにすることです。
  • com.google.test.loops.compatibility: I/O の問題や OpenSSL の問題など、さまざまな互換性の問題をテストするループに使用します。
  • com.google.test.loops.performance: デバイスのパフォーマンスをテストするループに使用します。たとえば、最も複雑なグラフィック設定でゲームを実行して、新しいデバイスでの動作を確認できます。

アプリで同じラベルのループを実行できるようにするには:

  • Test Loop Manager を使用してテストを行う場合:

    1. アプリのマニフェストで、次のメタデータ行を追加し、LABEL_NAME を任意のラベルで置き換えます。

      <meta-data
       android:name="com.google.test.loops.LABEL_NAME"
       android:value="1,3-5" />
      

      android:value フィールドには、ラベルを適用するループを 1~1,024 の整数の範囲または値のセットで指定します(1,024 は 1 回のテストで許可される最大ループ数です)。ループは 0 ではなく 1 から開始します。たとえば、android:value="1,3-5" はループ 1、3、4、5 に LABEL_NAME を適用します。

    2. Test Loop Manager アプリで、[Labels] フィールドに 1 つ以上のラベルを入力します。

  • Firebase コンソールでテストを行う場合は、[ラベル] フィールドに 1 つ以上のラベルを入力します。

  • gcloud CLI でテストを行う場合は、--scenario-labels フラグを使用して 1 つ以上のシナリオラベルを指定します(例:--scenario-labels=performance,gpu)。

アプリのライセンス サポート

Test Lab は、Google Play が提供するアプリ ライセンス サービスを使用するアプリをサポートしています。Test Lab でアプリをテストする際にライセンスを確認するには、アプリを Play ストアの製品版チャネルに公開する必要があります。Test Lab を使用してアルファまたはベータチャネルでアプリをテストするには、アプリを Test Lab にアップロードする前にライセンス チェックを削除します。

既知の問題

Test Lab のゲームループ テストでは次の問題が確認されています。

  • 一部のクラッシュはバックトレースに対応していません。たとえば、リリースビルドによっては、prctl(PR_SET_DUMPABLE, 0) により debuggerd プロセスの出力が抑止される場合があります。詳しくは、debuggerd をご覧ください。
  • ファイル権限エラーのため、API レベル 19 はサポートされていません。