やらなイカ?

たぶん、iOS/Androidアプリの開発・テスト関係。

統合テストの合否判定を Gemini API の画像解析で行なう Visual Verification Agent 実装サンプル

Googleの提供するマルチモーダルLLM Gemini を利用して、ゲームプレイの自動テストの合否判定を画像(スクリーンショット)ベースで行なう方法を紹介します。

これまでも、Anjin などのオートパイロットフレームワークを利用することで規定のシナリオに沿った自動テストは実現できていました。 しかし、テストの合否判定は「シナリオの最後まで画面遷移できたこと」をもって合格とすることが多く、操作完了時点のSceneの状態まで検証することは高コスト*1になるため避けてきました。

本記事では、操作完了など合否判定を行ないたいタイミングでスクリーンショットを撮影し、Gemini APIを使用して「画像になにが描かれていれば合格か」をプロンプトとして与えるだけで検証ができるAnjinのカスタムAgentを実装します*2

利用イメージ

まず、どのように動作するかを紹介します。 動作するプロジェクトは、Unity社の提供する2Dシューターサンプル GalacticKittens をフォークしたリポジトリjudge-by-gemini-agent ブランチにあります。

事例1: マルチプレイが成立していることを検証する

GalacticKittensは4人までのマルチプレイが可能なゲームです。マルチプレイのテストシナリオでは、別プロセスで立ち上げたプレイヤーがJoinできていればインゲームで2機の戦闘機が表示されます。

これを、次のAgentで検証します。

Prompt には「スクリーンショットに何が写っていれば成功と判断できるか」を自然言語で書きます。ここでは「右向きの赤い戦闘機と青い戦闘機が表示されていること」としています。

Success Threshold には、Geminiが返すスコア(0.0〜1.0)がいくつ以上であればテスト成功とみなすかのしきい値を設定します。LLMの性質上、プロンプトの工夫だけでは偽陰性偽陽性はゼロにはできません。そのテストシナリオの目的によって偽陰性偽陽性どちらを許容するかは異なるため、このプロパティで調整します。

Anjinを起動し、インゲームが開始されて数秒経つと、Geminiから次のようなレスポンスが返されます。 スコアが0.8以上ですので、テストは成功と判断されます。

Response: {
  "comment": "画像には右向きの赤い戦闘機と青い戦闘機が表示されています。",
  "score": 1.0
}

もし別プレイヤーのJoinに失敗した場合、次のようなレスポンスが返ります。 スコアが0.8未満なので、テストは失敗と判断されます。

Response: {
  "comment": "画像には右向きの赤い戦闘機が表示されていますが、青い戦闘機は表示されていません。",
  "score": 0.5
}

事例2: リザルト画面の文字を判読する

GalacticKittensでは、ボスを倒したとき、もしくは全滅したときにリザルト画面が表示されます*3

これを、次のAgentで検証します。

Prompt は「VICTORY!と表示されていること」としています。 これはボスを倒したときに画面に表示される文字で、テキストでなくスプライトです。

Geminiからは次のようなレスポンスが返ります。ちゃんと文字が読めています。

Response: {
  "comment": "画像には「DEFEAT」と表示されており、「VICTORY!」とは表示されていません。",
  "score": 0.0
}

実装方法

続いて実装方法を紹介します。 環境は Unity 6000.0.23f1、Anjin v1.9.0 です。

GemiNetのインストール

Gemini には公式 C# SDK は存在しないため、REST APIもしくはサードパーティSDKを利用します。

今回は、GemiNet v1.0.3を使用しました。 APIデザインが公式SDKに準じているため、公式ドキュメントのサンプルコードを見ながら実装するのがとても楽でした。

github.com

NuGetパッケージですので、NuGetForUnityを使用してインストールします。 NuGetForUnityについては次の記事を参照してください。

www.nowsprinting.com

インストールしたら、Agentを置くasmdefのAssembly Referencesに次のDLLを追加します。

  • GemiNet.dll
  • System.Text.Json.dll

続いて、Agentのコードを実装していきます。

Geminiクライアントの初期化

Geminiクライアントの初期化にはGeminiのAPIキーが必要です。APIキーはハードコードするのではなく、コマンドライン引数か環境変数から受け取るようにします。 Anjinに含まれるユーティリティクラス Argument<T> が便利なので使っています。

var apikey = new Argument<string>("GEMINI_API_KEY");
if (!apikey.IsCaptured())
{
    return;
}

using var ai = new GoogleGenAI();
ai.ApiKey = apikey.Value();

Unityエディターで実行するときは、Unity Hubでプロジェクト右端のドロップダウン > Add command line arguments クリックで表示されるダイアログでコマンドライン引数を設定できます。

スクリーンショットの撮影とアップロード

スクリーンショットを撮影し、GoogleGenAI.Files.UploadAsync メソッドでGeminiにアップロードします。

画像は、後で紹介する GoogleGenAI.Models.GenerateContentAsync メソッドに直接渡すこともできるのですが、20MB制限があるため、事前にアップロードします。

var texture = ScreenCapture.CaptureScreenshotAsTexture();
var bytes = texture.EncodeToPNG();
var base64 = Convert.ToBase64String(bytes);

var file = await ai.Files.UploadAsync(
    new UploadFileRequest()
    {
        File = new UploadFileContent(new Blob() { Data = base64, MimeType = "image/png" }),
        MimeType = "image/png",
    },
    cancellationToken: cancellationToken);

画像解析

次に、GoogleGenAI.Models.GenerateContentAsync メソッドに画像やプロンプトを渡してレスポンスを受け取ります。

var response = await ai.Models.GenerateContentAsync(new GenerateContentRequest
    {
        Model = Models.Gemini2_0Flash,
        Contents = Content.CreateUserContent(
            Part.FromUri(file.Uri, file.MimeType),
            Part.FromText(prompt)),
        SystemInstruction = "Analyze the image and determine whether it meets the user prompt's requirements. The response consists of a score (maximum = 1.0) and a corresponding comment in Japanese.",
        GenerationConfig = new GenerationConfig
        {
            ResponseMimeType = "application/json",
            ResponseSchema = new Schema
            {
                Properties = new Dictionary<string, Schema>
                {
                    { "score", new Schema { Type = DataType.Number } },
                    { "comment", new Schema { Type = DataType.String } }
                },
                Required = new[] { "score", "comment" },
                Type = DataType.Object
            },
        }
    },
    cancellationToken: cancellationToken);

引数の GenerateContentRequest に設定している内容は次のとおりです。

  • Model: 推論に使用するモデルを指定します。ここではGemini 2.0 Flash
  • Contents: アップロードした画像の情報と、SerializeFieldに設定されたユーザープロンプトをそのまま渡します
  • SystemInstruction: システム命令は固定にしています。内容は「画像を解析し、ユーザープロンプトの要件を満たしているかどうかを判断します。応答は、スコア(最大1.0)と、それについての所見を日本語で返すこと」としています
  • GenerationConfig: レスポンス本文のフォーマットを指示します。JSONで "score" と "comment" を含むフォーマットと定義しています

レスポンスの処理

レスポンスは指定したJSON形式なので、System.Text.Jsonパッケージを使ってスコアとコメントを取り出し、合否を判定します。

var json = JsonDocument.Parse(response.GetText()).RootElement;
var score = json.GetProperty("score").GetDouble();
var comment = json.GetProperty("comment").GetString();

if (score < successThreshold)
{
    var message = $"Visual verification is a failure! score:{score} comment:{comment}";
    AutopilotInstance.TerminateAsync(ExitCode.AutopilotFailed, message).Forget();
}

テストシナリオ(AutopilotSettings)への組み込み

作成した VisualVerificationAgent は、他のAgentと同じようにコンテキストメニューからインスタンスを生成し、プロンプトを設定します。 そして、テストシナリオの終端(および途中の要点)に配置すれば完了です。

※ 実際は2つのテストシナリオにそれぞれ使用するべきところ、1つのAutopilotSettingsにまとめて設定した例

注意事項

画像解像度

画像解析の精度は、画像の解像度に影響を受けるようです*4。当初、320x180サイズで試していたところ、インゲームの画面で次のレスポンスが返ってきました。

Response: {
  "comment": "画像には赤い戦闘機が表示されていません。宇宙空間のような背景の中に、赤い惑星が見られます。",
  "score": 0.0
}

背景は認識しているようなので、プロンプトを「宇宙にいる」に変えてみたところ、次のレスポンスが返りました。

Response.text: {
  "comment": "画像は宇宙空間を示しているようです。",
  "score": 0.95
}

ゲームタイトルによって異なるはずですが、今回の GalacticKittens では、800x480以上で安定したレスポンスが返るようになりました。

消費トーク

画像解像度によって消費トークンが変わってきます。 320x180の場合、300くらい。 800x480の場合、1900くらいでした。

なお、800x480より大きくしても結果の精度も消費トークンも上がりませんでした*5

まとめ

マルチモーダルLLMの登場で、これまでルールベースでの自動化が困難だった分野も低コストで自動化できる可能性が出てきました。 ゲーム領域において統合/E2Eレベルのテストは難しく、しかしコストはかけられず、ただ動かして進行不能にならないかを見るだけだったり、自動でスクリーンショットだけ撮影して後で人間がチェックする「半自動化」が最適解だったわけですが、状況は変わってきそうです。

なお、LLMとルールベースとは異なる特性を持っています。 LLMは人間に近く、柔軟ですがミスもします。 従って、たとえば自動で操作する部分は従来通りルールベースやキャプチャ/プレイバックのほうが向いており、LLMの推論ベースに置き換える必要はないと考えています。 適材適所です。

関連

本記事のように小さな部品を簡単に組み合わせて使えるAnjinは良いフレームワークなので(自画自賛)、ぜひ使ってみてください。使いかたから拡張方法まで、こちらの同人誌に詳しく載っています。

ikagoya.booth.pm

CEDEC 2025で登壇します。Anjinの話もしますが、ユニットテストを含めた開発者テスト全般についてのセッションです。

cedec.cesa.or.jp

*1:ゲームは画面に表示される内容が決定的でないことが多く、またそうでなくてもヒエラルキーをたどる実装コスト、仕様変更に対応するメンテナンスコストがかかります

*2:将来的にAnjinパッケージのSamplesに追加するかもしれません

*3:実はボスを倒したときと全滅したときで異なるSceneに遷移するのでこのような仕組みは不要なのですが、文字を読む事例として…

*4:サイバーエージェントのAI Labの方々に伺ったところ、前処理とかコントラストとかを考えるよりも、まず解像度だそうです

*5:サイバーエージェントのAI Labの方曰く、Gemini内でサイズに上限があり圧縮されているのではないかとのこと