やらなイカ?

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

#C105 ありがとうございました

コミックマーケット105、無事終わりました。 ご購入いただいた方々、また、スペースにお立ち寄りいただいた方々、準備会やインフラの方々、本当にありがとうございました。

新刊は電子・物理ともBOOTHで販売しております。 新刊は折を見て(遅くとも次のイベント出展までに)値上げを検討しています。お早めにお買い求めください。

ikagoya.booth.pm

頒布した書籍の詳細・フォローアップは次の記事を参照してください。

www.nowsprinting.com

www.nowsprinting.com

www.nowsprinting.com

今回も西2ホールで人が少なかったのと新刊がニッチさを極めているのとで、ずいぶん平和な一日でした。 既刊の『Unity Test Framework完全攻略ガイド 統合テスト編』が完売すると、ぱっと見Unity成分がスペースからなくなるという事態に。 テスト自動化ピラミッドの図も小さかったし、ディスプレイもっと頑張るべきだったと反省。

ちなみに今回、ブースクロスをグラフィックさんで新調。疾走するめの一夜干しバージョンがいい感じに。 ちゃんとアイロンかけよう…

Anjin非公式ファンブック

株式会社ディー・エヌ・エー(以下DeNA)が公開しているオープンソースのUnity向けオートパイロットフレームワークAnjin(あんじん)を紹介する同人誌を、コミックマーケット105の2日目(月曜)西う47b「いか小屋」で頒布します。

企画当初は「必要なことはREADMEに書いてあるし日本語版あるし」という前提で、ゆるふわな紹介本のつもりでした*1。 しかし書き進めるうちにずいぶん踏み込んだ内容になりました。

Anjinの主な用途は自動テストですが、「どのような観点のテストを自動化するのか」からスタートしてもらえるよう、効果の高いユースケースや、インゲームの自動プレイを実装する事例、テストのメンテナンスコストを抑える工夫なども盛り込みました。 Anjinに限った話ではなく、ゲームプレイの自動テストを実装するのに役立つはずです。

とはいえ、Anjin自体はアウトゲームのある程度の範囲はビルトイン機能だけで自動プレイできる、気軽に導入できるフレームワークです。 開発中/運用中のゲームタイトルに自動テストを導入する第一歩としても参考にしていただけます。

想定読者

第1章〜第6章は、ソフトウェアエンジニア(プログラマー)だけでなく、プランナー(ゲームデザイナー)やQA/テストエンジニアが独力でAnjinを利用できるよう、すべてUnityエディター内で(ノーコードで)完結できる内容となっています。

第7章・第8章では、ゲームタイトルに応じたAnjinの拡張方法などを扱っており、UnityでC#スクリプトを書けるレベルのソフトウェアエンジニアを対象としています。

目次

第1章 Anjinの概要

Anjinのコンセプト、Anjinを導入する「統合テスト」の概要と「階層化テスト戦略」における位置づけ、Anjinを使った各種テストのユースケースを紹介しています。

第2章 Anjinの使いかた

Anjinのインストールからテストシナリオの設定、および基本的な実行方法を紹介しています。

マルチプレイヤー対応ゲームをMultiplayer Play ModeパッケージやParrelSyncパッケージで動作確認するときにAnjinで自動プレイさせる方法も紹介しています。

第3章 ビルトインAgentカタログ

Anjinの本体はAgentを起動する役割しか持っておらず、実際にゲームを操作したり、エラーハンドリングを行なうのがAgentです。 本章では、Anjinにビルトインで提供されているAgentの使いかたを紹介しています。

第4章 ビルトインLoggerカタログ

Anjinにビルトインで提供されているLoggerの使いかたを紹介しています。

第5章 ビルトインReporterカタログ

Anjinにビルトインで提供されているReporterの使いかたを紹介しています。 またSlack通知に必要なSlack Botの生成・設定方法、購入者特典としてBotアイコンの配布もあります。

第6章 逆引き設定レシピ&Tips

ユースケースに沿ったテストシナリオの設定、またAgentの組み合わせパターンを紹介しています。

第7章 Anjinの拡張

ゲームタイトル固有の事情やAnjinのビルトイン機能だけではテストできない部分(たとえばインゲーム)のためにAnjinを拡張する方法を紹介しています。 カスタムAgentの具体的な実装例を5例、インゲーム向けカスタムAgentの実装ヒント。

またカスタムLogger、カスタムReporterの実装方法も紹介しています。

第8章 高度な実行方法

継続的インテグレーション(CI)、プレイヤービルドへの組み込み、Play Modeテストからの実行方法を紹介しています。

詳しい目次は次のポストを参照してください。

表紙イラスト・題字・挿絵について

江戸むらさき特急』『東京お侍ランド』*2などで知られる、漫画家のほりのぶゆき先生(@nobhori)にお願いしました。

Anjinの名前の由来である「按針」は当時の航海士(パイロット)を指す言葉であり特定の人物を指すものではありませんが、着想を得たのはドラマ『SHOGUN(1980)』*3の三浦按針(演 リチャード・チェンバレン)です。このイメージをイラストにしていただけるのは、ほり先生しかいない!ということで、無理を承知でお願いしました。

C105

2日目(月曜)西う47b「いか小屋」でお待ちしております。 西2ホール入口からまっすぐ!

既刊在庫も持っていきます。

www.nowsprinting.com

www.nowsprinting.com

信販売・電子版について

BOOTHで電子・物理とも販売開始しました! [12/31]

ikagoya.booth.pm

折を見て(遅くとも次のイベント出展までに)値上げを検討しています。 お早めにお買い求めください。

*1:タイトルと表紙イラストはその名残りです

*2:若い世代にも「この利休にまっ茶ラテを作れと!」で通じるらしいですね

*3:SHOGUN(2024)でないのがポイント

Unity Version Control (Plastic SCM) を JetBrains Rider から操作する

Unity Advent Calendar 2024 シリーズ2、9日目の記事です。

いつの間にかUnityに買収され、名前が Unity Version Control となった Plastic SCM。 Unityエディターからでも、フルクライアントからでもリポジトリを操作できます。

しかし、やっぱりRiderから使いたい! ということでやってみたログです。

前提バージョン

  • Unity Version Controlクライアント 11.0.16.9069
  • JetBrains Rider 2024.3

Unity Version Controlクライアントのインストール

一般的なRiderプラグインはJetBrains Marketplaceで公開されていますが、Unity Version Controlプラグインはフルクライアントに同梱されているものを使います。

そのため、まずは下記ページからフルクライアントをダウンロード・インストールします。 なおこれ以降は以前の名前である "Plastic SCM" 表記のままですので、本記事でも Plastic SCM と表記します。

www.plasticscm.com

Windowsの場合、インストールの過程でSelect Componentダイアログが出ますので、"IDE integrations" 下の "IntelliJ IDEA plugin" を選択します。 その後、IntelliJ IDEAのインストールパスを入力するダイアログが出ます。ここで指定するとプラグインをインストールしてくれるようなのですが、そのせいでRiderが起動しなくなってしまったので、ここでは指定しないほうがよさそうです。

Riderにプラグインをインストール

Riderを起動してメニューから JetBrains Rider > Settings を選択してSettingsウィンドウを開き、Plugins タブを選択します。

右上の歯車アイコン > Install Plugin from Disk... を選択。 ファイル選択ダイアログが出ますので、次のファイルを選択します。

  • macOSの場合: /Applications/PlasticSCM.app/Contents/Plugins/plastic4idea/lib/plastic4idea.jar
  • Windowsの場合: インストールディレクトリ¥intellij12¥plastic4idea.jar

すると、Installed タブの中に "Plastic SCM Integration" が出るはずです。 Restart IDE ボタンをクリックしてRiderを再起動すれば完了です。

Plastic SCM プラグインの使いかた

command + 9 でエディタ下部にVCSウィンドウが開きます。

Local Changesタブに差分のあるファイル、選択すると右のペインにdiffが表示されます。 またファイル単位にチェックイン(コミット)履歴を見ることもできます。

リアルタイムに更新されないようなので、左にあるリロードアイコンをクリックしてください。

command + K でコミットダイアログが表示されます。

左上のペインでチェックインするファイルを選択、下にコミットメッセージを入力し、Checkin ボタンでリポジトリにチェックインされます。

機能や使い勝手はGitプラグインに比べて足りない感は否めませんが、Rider内で操作が完結するメリットは大きいのではないでしょうか。 なお、Gitプラグインはとても便利で、私もずっと愛用していたGitクライアント*1から乗り換えたほどです。

Gitリポジトリとの複合プロジェクト

バイナリファイルの扱いは Plastic SCM が有利、でもコードはGitで管理したいというチームもあるのではないでしょうか。 たとえば、プロジェクト全体はGit管理、Assets/Plastic/ 下に Plastic SCM リポジトリという構成をRiderで使う場合の設定方法を紹介します。

メニューから JetBrains Rider > Settings を選択してSettingsウィンドウを開き、Version Control > Directory Mappings を選択します。

初期状態ではプロジェクトルートのみ表示されているはずです。+ アイコンをクリックして追加ダイアログを開き、

と設定して OK ボタンをクリックします(上画像は追加した状態です)。

これで、GitとPlastic SCM両方のリポジトリをRiderから操作できます。

参考

blog.plasticscm.com

unity.com

unity3d.jp

www.jetbrains.com

*1:gitコマンド

Anjin v1.8.0 リリース

Anjin v1.8.0がリリースされました。大きめのマイナーチェンジ()なので、変更内容を紹介します。 本バージョンでは主に、可用性を広げる機能が追加されています。

github.com

Anjinパッケージ(com.dena.anjin)は、株式会社ディー・エヌ・エーが開発・OSSとして公開しているUnity向けオートパイロットフレームワークです*1

github.com

AutopilotSettingsの変更

オートパイロットシナリオの根幹であるAutopilotSettingsの設定項目が色々変更されました。項目が移行されたものは、Runボタンでオートパイロットを起動すると自動変換されます*2

下図は、左がv1.6時点のもの、右がv1.8で自動変換されたものです。

変更箇所は次のとおりです。

Nameフィールドを追加

AutopilotSettingsの名前を指定できるようになりました。 Slack通知のメッセージに挿入することができます*3

Observer Agent を Scene Crossing Agents に変更

従来のObserver Agentには、すべてのSceneで(Scene Agent Mappingに設定されたオペレーションを担当するAgentとは別に)起動されるAgentを設定できました。

v1.8で置き換えられたScene Crossing Agentsでは、次の点が異なります。

  • Agentの起動・停止はSceneごとでなく、オートパイロットの起動・停止と同期*4
  • 複数のAgentを指定できる*5

タイムアウト時の振る舞いを設定可能に

従来、Lifespanで指定したタイムアウト時間に達すると、オートパイロットは正常終了(CLIでは終了コード=0)していました*6

v1.8からは、終了コード(Exit Code)およびSlack通知に含めるメッセージ(Message)を設定できるようになりました。

JUnit Report Path設定を JUnitXmlReporter に移行

終了時にJUnit形式のXMLファイルを出力したいとき、従来はAutopilotSettingsのJUnit Report Pathにパスを設定していましたが、廃止されました。

v1.8からは、ReporterとしてJUnitXmlReporterを追加することで実現できます。

Loggerを複数指定可能に

設定項目としては "Logger" は廃止、場所を移動して複数形の "Loggers" になりました。 従来もCompositeLogger*7を使用することで複数のLoggerを使用できましたが、より簡単に設定できます。

Reporterを複数指定可能に

設定項目としては "Reporter" は廃止、場所を移動して複数形の "Reporters" になりました。 従来もCompositeReporter*8を使用することで複数のReporterを使用できましたが、より簡単に設定できます。

Slack設定の廃止

v1.1からSlack通知設定はSlackReporterで行なうように推奨されていましたが、v1.8でSlack Token/ Channelsを廃止しました。 もしまだ使用されていた場合、SlackReporterに変換されます。

Output Root Pathを追加

FileLoggerなどファイル出力を行なうAgent、Logger、Reporterの出力先に相対パスを指定したときの起点ディレクトリを設定できるようになりました。 コマンドライン引数でも指定できます。

Screenshots Pathを追加

Agentがスクリーンショットを保存するディレクトリを直接指定できるようになりました。相対パスで指定したとき、起点はOutput Root Pathになります。 コマンドライン引数でも指定できます。

また "Clean Screenshots" をonにすると、オートパイロット起動時にディレクトリ内のファイルが全削除されます。

エラーハンドリング設定を ErrorHandlerAgent に移行

エラーハンドリング設定が廃止され、ErrorHandlerAgentに置き換えられました。 ErrorHandlerAgentは、Scene Crossing Agentsに追加して常に監視状態にする使いかたを想定しています。

ファイルを分けることで複数シナリオで設定を共有できるようになるほか、ParallelCompositeAgentを使うことで特定のSceneのみ別設定でエラーハンドリングさせることもできるようになります。

SlackReporterの機能追加

オートパイロット終了時にSlack通知を行なうSlackReporterにもいくつか機能が追加されました。

v1.8からは、Slack通知のリード文、本文、アタッチメントの色を設定できるようになりました。 リード文および本文には、次のプレースホルダを使用できます。

  • "{message}": 終了要因メッセージ(エラーログのメッセージなど)
  • "{settings}": 実行中の AutopilotSettings 名
  • "{env.KEY}": 環境変数
  • "{mppm-tags}": Multiplayer Play Mode パッケージのタグ

また、従来は異常終了時にのみ送信していましたが、"Post if completes" をonにすることで正常終了時にも送信できるようになりました。

UGUIEmergencyExitAgentの変更

以前はEmergencyExitAgentでしたが、v1.8から名称変更と機能追加を行ないました。

EmergencyExitアノテーションの出現をチェックする間隔の指定と、ボタンクリック時にスクリーンショットを撮影できるようになりました。

TerminateAgent

オートパイロットを停止させるAgentを追加しました。 終了コード(Exit Code)およびSlack通知に含めるメッセージ(Message)を設定できます。

テストシナリオの目的を達成したところに配置し、TerminateAgentでは正常終了、AutopilotSettingsのタイムアウトを異常終了にするといった組み合わせを想定しています。

GameView解像度の指定

コマンドラインから起動するとき、コマンドライン引数でGameViewの解像度を指定できるようになりました。

  • GAME_VIEW_WIDTH: GameView の幅を設定します。この引数は、コマンド ラインからエディターを起動する場合にのみ使用されます。デフォルト値は 640 です
  • GAME_VIEW_HEIGHT: GameView の高さを設定します。この引数は、コマンド ラインからエディターを起動する場合にのみ使用されます。デフォルト値は 480 です

Multiplayer Play Mode パッケージ対応

Multiplayer Play Mode (MPPM) パッケージと使用するとき、Virtual Playersでオートパイロットが実行されない場合がありましたが、確実に同期されるよう修正しました。

また、SlackReporterの送信メッセージにタグを挿入できるようになりました。

MPPMパッケージとの使用方法については次の記事を参照してください。

www.nowsprinting.com

InitializeOnLaunchAutopilotの呼び出し順を指定可能に

オートパイロット起動時にゲームタイトル固有の初期化を行なうことができる InitializeOnLaunchAutopilot 属性に引数が追加され、呼び出し順を指定できるようになりました。

蛇足

今回が大きめのマイナーチェンジ()であることをコミット数から見てみましょう。 v1.0以降*9のマイナーバージョンリリース日をプロットしてみました。 v1.7との境界がわかりづらいですが、ピーク値はv1.7のリリース後です。その後4週目で失速しているのは体力的な問題…。

宣伝

冬コミ(C105)で、Anjinのあれこれを詰め込んだ『Anjin非公式ファンブック』を頒布予定です。夏から出す出す詐欺していましたが今度こそ間に合うはず。

表紙・挿絵は『江戸むらさき特急』の著者、ほりのぶゆき先生にお願いしました!

2日目 西う47b「いか小屋」でお待ちしております。 また、後日BOOTHでも頒布予定です。

*1:筆者はもう社員ではないですがコントリビューターです

*2:従ってCIなどではそのまま使えますが、早めに変換後のファイルをVCSにコミットしておくことをお勧めします

*3:AutopilotSettingsはScriptableObjectなのでnameはファイル名になります。シナリオごとにディレクトリに入れてファイル名はAutopilotSettingsのままにすることが多いので、明示的に名前を付けられるようにしました。省略時はファイル名が使われます

*4:つまりDontDestroyOnLoadされます

*5:従来のObserver AgentでもCompositeAgentを使うことで実現できましたが、より簡単に設定できるようになりました

*6:元々コンセプトとして時間いっぱいモンキーテストを行なう想定のため

*7:この変更に伴い、CompositeLoggerはコンテキストメニューから除外されました

*8:この変更に伴い、CompositeReporterはコンテキストメニューから除外されました

*9:ベースはDeNA社内用だったので、v1.0より前のコミットはInitial commitに集約されています

Test Helper パッケージ v1.0.0 リリース

Unity Test Framework でテストを書くときに便利なライブラリ Test Helper パッケージのバージョン 1.0.0 をリリースしました*1

github.com

Test Helperパッケージは、Unity Test FrameworkおよびNUnit 3の属性(attribute)、Comparer、制約(constraint)などの汎用的な拡張を詰め合わせたものです。 拡張のサンプルとしてもご利用いただけるはずです。

インストールは、GitHubリポジトリ指定や、openupm.comから可能です。 openupm-cli であれば次のコマンドでインストールできます。

openupm add com.nowsprinting.test-helper

以下、v1.0.0で実装している機能を簡単に紹介します*2

属性(attribute)

FocusGameView

この属性をテストに付与すると、テスト実行時にGameViewにフォーカスが移ります。 フォーカスが無いと正しく動作しないテストケースに有効なほか、バッチモードでもGameViewを表示することで、バッチモードの様々な制限事項(WaitForEndOfFrameを使用できない等)を回避できます。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    [FocusGameView]
    public void MyTestMethod()
    {
        // e.g., Test using InputEventTrace of Input System package.
    }
}

バッチモードの制限事項と回避方法について詳しくは過去記事を参照してください。

www.nowsprinting.com

GameViewResolution

この属性では、テスト実行時のGameView解像度を指定できます。 また FocusGameView属性と同様、バッチモードでもGameViewを表示できます。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [UnityTest]
    [GameViewResolution(640, 480, "VGA")]
    public IEnumerator MyTestMethod()
    {
        yield return null; // Wait for one frame to apply resolution.

        // e.g., Test using GraphicRaycaster.
    }
}

GizmosShowOnGameView

この属性を付与したテストを実行する間、GameViewにGizmoを表示できます。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    [GizmosShowOnGameView(true)]
    public void MyTestMethod()
    {
        // Show Gizmos on GameView during the test running.
    }
}

IgnoreBatchMode

この属性を付与したテストは、バッチモードでスキップされます。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [UnityTest]
    [IgnoreBatchMode("Using WaitForEndOfFrame.")]
    public IEnumerator MyTestMethod()
    {
        // e.g., Test needs to take a screenshot.

        yield return new WaitForEndOfFrame();
        ImageAssert.AreEqual(expectedTexture, Camera.main, settings);
    }
}

IgnoreWindowMode

IgnoreBatchMode属性と逆に、ウィンドウモード*3でスキップされます。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    [IgnoreWindowMode("Requires command line arguments")]
    public void MyTestMethod()
    {
        var args = Environment.GetCommandLineArgs();
        Assert.That(args, Does.Contain("-arg1"));
    }
}

UnityVersion

テストを実行するUnityエディターのバージョンによってテストをスキップできます。 #ifディレクティブでも実現できますが、この属性を使うことによって明示的にスキップしたテストをテスト実行結果に残すことができます。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    [UnityVersion(newerThanOrEqual: "2022")]
    public void MyTestMethod()
    {
        // Test run only for Unity 2022.1.0f1 or later.
    }

    [Test]
    [UnityVersion(olderThan: "2019.4.0f1")]
    public void MyTestMethod()
    {
        // Test run only for Unity older than 2019.4.0f1.
    }
}

CreateScene

テスト実行時に空のSceneを生成して使用します。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    [CreateScene(camera: true, light: true)]
    public void MyTestMethod()
    {
        var camera = GameObject.Find("Main Camera");
        Assert.That(camera, Is.Not.Null);

        var light = GameObject.Find("Directional Light");
        Assert.That(light, Is.Not.Null);
    }
}

LoadScene

テスト実行時に指定したSceneファイルをロードします。 通常、Edit Modeテスト、Play ModeテストのUnityエディター内実行、プレイヤー実行でSceneの指定方法は異なり、またScenes in Buildに含まれないSceneファイルはプレイヤー実行で使用できない制約がありますが、この属性を使用すればこれらはすべて考えなくてよくなります。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    [LoadScene("Assets/MyTests/Scenes/TestScene.unity")]
    public void MyTestMethod()
    {
        var cube = GameObject.Find("Cube in TestScene");
        Assert.That(cube, Is.Not.Null);
    }

    [Test]
    [LoadScene("Packages/YourPackageName/**/SampleScene.unity")]
    public void UsingGlobPattern()
    {
        // snip
    }

    [Test]
    [LoadScene("../../Scenes/SampleScene.unity")]
    public void UsingRelativePath()
    {
        // snip
    }
}

BuildScene

プレイヤーでも実行するテストにおいて(かつLoadScene属性が使えないケースで)Scenes in Buildに含まれないSceneをビルドに含めるためのマーカー属性です。

次のように使用します。

Usage:

[TestFixture]
public class MyTestClass
{
    [Test]
    [BuildScene("../../Scenes/SampleScene.unity")]
    public void MyTestMethod()
    {
        // Setup before load scene

        // Load scene
        await SceneManagerHelper.LoadSceneAsync("../../Scenes/SampleScene.unity");

        // Excercise the test
    }
}

TakeScreenshot

この属性を付与したテストの実行終了時、スクリーンショットを撮影します。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    [TakeScreenshot]
    public void MyTestMethod()
    {
        // Take screenshot after running the test.
    }
}

TimeScale

この属性を付与したテストを実行する間、Time.timeScale を変更します。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    [TimeScale(2.0f)]
    public void MyTestMethod()
    {
        // Running at 2x speed.
    }
}

カスタム属性の実装方法については過去記事を参照してください。

www.nowsprinting.com

Comparer

Comparerは、NUnit 3の制約モデル(Assert.That)で検証するときに比較方法をUsing修飾子で指定するものです。

GameObjectNameComparer

GameObject同士をnameで比較するComparerです。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        var actual = GameObject.FindObjectsOfType<GameObject>();
        Assert.That(actual, Does.Contain(new GameObject("test")).Using(new GameObjectNameComparer()));
    }
}

XDocumentComparer

XDocument同士をゆるく比較するComparerです。要素の順序やコメントを無視して比較できます。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        var x = XDocument.Parse(@"<root><child>value1</child><child attribute=""attr"">value2</child></root>");
        var y = XDocument.Parse(@"<?xml version=""1.0"" encoding=""utf-8""?>
<root><!-- comment --><child attribute=""attr"">value2</child><!-- comment --><child>value1</child></root>");
        // with XML declaration, comments, and different order

        Assert.That(x, Is.EqualTo(y).Using(new XDocumentComparer()));
    }
}

XmlComparer

string同士を、XMLとしてゆるく比較するComparerです。改行やスペース、要素の順序やコメントを無視して比較できます。

次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        var x = @"<root><child>value1</child><child attribute=""attr"">value2</child></root>";
        var y = @"<?xml version=""1.0"" encoding=""utf-8""?>
<root>
  <!-- comment -->
  <child attribute=""attr"">
    value2
  </child>
  <!-- comment -->
  <child>
    value1
  </child>
</root>";

        Assert.That(x, Is.EqualTo(y).Using(new XmlComparer()));
    }
}

制約(constraint)

NUnit 3の制約モデル(Assert.That)で使用できる制約です。

Destroyed

GameObjectが(nullでなく)破棄されているかを検証します。

次のように使用します。

using Is = TestHelper.Constraints.Is;

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        var actual = GameObject.Find("Cube");
        GameObject.DestroyImmediate(actual);

        Assert.That(actual, Is.Destroyed);
    }
}

ランライムAPI

上記カスタム属性の機能のうちいくつかは、APIで呼び出せるようになっています。 使用するには、TestHelper.RuntimeInternalsアセンブリをAssembly Definition Referencesに追加する必要があります。

ScreenshotHelper

テストコード内の任意の場所でスクリーンショットを撮影するAPIです。 次のように使用します。

[TestFixture]
public class MyTestClass
{
    [UnityTest]
    public IEnumerator MyTestMethod()
    {
        yield return ScreenshotHelper.TakeScreenshot();
    }

    [Test]
    public async Task MyTestMethodAsync()
    {
        var coroutineRunner = new GameObject().AddComponent<CoroutineRunner>();
        await ScreenshotHelper.TakeScreenshot().ToUniTask(coroutineRunner);
    }

    private class CoroutineRunner : MonoBehaviour { }
}

SceneManagerHelper

テストコード内の任意の場所でSceneをロードするAPIです。 Edit Modeテスト、Play ModeテストのUnityエディター内実行、プレイヤー実行の区別なく実行できます。 次のように使用します。

[TestFixture]
public class MyTestClass
{
    [Test]
    public void MyTestMethod()
    {
        // Setup before load scene

        // Load scene
        await SceneManagerHelper.LoadSceneAsync("../../Scenes/SampleScene.unity");
        
        // Excercise the test
    }
}

エディター拡張

Window > Test Helper > Open Persistent Data Directory

Application.persistentDataPath ディレクトリを Finder/ File Explorer で開きます。

TakeScreenshot属性のデフォルト保存先はApplication.persistentDataPath下になっています。

Window > Test Helper > Temporary Cache Directory

Application.temporaryCachePath ディレクトリを Finder/ File Explorer で開きます。

テストコードから使用する一時ファイルの保存先に使うことが多いため追加しました。

JUnit XML format report

コマンドライン引数に -testHelperJUnitResults を指定することで、テスト終了時に(NUnit 3でなく)JUnit XML形式のテストレポートファイルを出力できます。

宣伝

www.nowsprinting.com

www.nowsprinting.com

*1:v0.1.0 の公開が May 20, 2023 なので、実に1年半

*2:詳しくはリポジトリのREADMEを参照してください

*3:公式な用語ではありません。非バッチモードの意味です

Unity Multiplayer Play Mode (MPPM) のVirtual PlayerをAnjinで自動プレイさせてみた

Unity 6以降で利用できる Multiplayer Play Mode (以下MPPM)パッケージを使うことで、Unityエディターに加え最大3つのVirtual Playerを起動してマルチプレイ対応ゲームの動作確認を簡単に行えるようになりました。

とはいえ、複数のVirtual Playerウィンドウを切り替え切り替え操作するのは煩わしいので、Unityエディター上で動作するオートパイロットフレームワークを使って操作を自動化してみました。 使用したのは、株式会社ディー・エヌ・エーが開発・OSSとして公開している Anjin(com.dena.anjin)パッケージです。

github.com

題材には、Unity社が公開しているNetcodeサンプルの2Dシューター GalacticKittens を使用しました。MPPMとAnjinを組み込んだforkを公開しています。

github.com

前提バージョン

  • Unity 6000.0.23f1
  • MPPMパッケージ v1.3.1
  • Anjinパッケージ v1.7.0 v1.8.0
  • Input Test Helperパッケージ v1.0.1

GalacticKittensの修正

本題に入る前に、GalacticKittensを若干修正しました。詳細は割愛します。

  1. Unity 6へのアップグレード
  2. TextMesh Proアップデートに伴ない、TMP Essensialsの更新(再インポート)
  3. Physics 2DのGravity.Yを0に変更

MPPMの導入

MPPMはパッケージマネージャからインストールできます。 "Unity Registry" で "multiplay" で検索すれば見つかります。

インストールしたら、Window > Multiplayer > Multiplayer Play Mode でウィンドウを開き、Virtual Playerを有効化できます。 下図では、Editorだけに host タグ、Virtual Playerに join タグを付けています。 タグについては後述します。

Virtual Playerを有効化(チェックボックスをonにした後、表示が "Active" に変わる)した時点で、対応するVirtual Playerウィンドウが開きます。この状態でエディター側で再生モードに入ると、各Virtual Playerでもゲームが再生されます。

ここまでで、手動でマルチプレイの動作確認ができるようになりました。

Anjinの導入

Anjinパッケージのインストール

Anjinパッケージのインストールは、openupm-cli を使用して次のコマンドで行なうのが簡単です。

openupm add com.dena.anjin

Package Managerウィンドウによるインストール方法は、リポジトリのREADMEを参照してください。

GalacticKittensの追加修正(InputをDI可能にする)

続いて、GalacticKittens側のいくつかのクラスに修正を加えます。 GalacticKittensはキー入力判定にInput Manager (legacy) を使用しており、このままではAnjinから入力を操作できません。 そこで、Input Test Helperパッケージを利用してスタブを注入できるようにします。

Input Test Helperパッケージのインストールは、同様に次のコマンドで行ないます。

openupm add com.nowsprinting.test-helper

今回、Inputを置き換えたのは次の3ファイルです。

  • Assets/Scripts/Managers/MenuManager.cs
  • Assets/Scripts/Player/PlayerShipMovement.cs
  • Assets/Scripts/Player/PlayerShipShootBullet.cs

いずれも、クラスに次のプロパティを追加するだけです。

public IInput Input { private get; set; } = new InputWrapper();

Input Test Helperパッケージについて詳しくは拙著『Unity Test Framework完全攻略ガイド 統合テスト編』第4章を参照してください。

www.nowsprinting.com

オートパイロットの設定

Anjinの設定は、Sceneごとに操作を担当するAgentを割り当てることで行ないます*1。 設定のルートは Assets/Autopilot/Settings/AutopilotSettings ファイルで、これをInspectorウィンドウで開いて編集します。

今回は次のように設定しました(図はScene Agent Mappingのみ抜粋)。

カスタムAgent MenuAgent を実装しました。 Menuには2つの状態があり、状態を見て振る舞いを変えています。

  1. "PRESS ANY KEY TO START"と表示されているとき:MenuManager の参照している Input にスタブを注入し、"ANY KEY" が押されている状態を返します。これで次の状態に遷移します。
  2. "HOST" か "JOIN" を選択:Virtual Playerに設定されているタグに応じて、"HOST" か "JOIN" ボタンをクリックします。タグは、CurrentPlayer.ReadOnlyTags() で取得できます。

CharacterSelection

ビルトインの UGUIPlaybackAgent を使用して、"READY" ボタンをクリックします。

キャラクターは4種類いますが、マルチプレイヤーがJoinしたときに異なるキャラクターが選択された状態になっているため、そのまま変更せず次に進みます。

Gameplay

カスタムAgent GameplayAgent を実装しました。 移動を行なう PlayerShipMovement と、弾を発射する PlayerShipShootBullet それぞれの参照している Input にスタブを注入し、ランダムな入力を与えています。

自動プレイの目的が攻略であるなら、ランダム入力でなく賢いAIを実装したり、ML-Agents機械学習エージェントを使います。

Defeat および Victory

ビルトインの UGUIPlaybackAgent を使用して、"MENU" ボタンをクリックします。

Bootstrap および Controls

いずれも勝手に次のsceneに遷移するため、Agentを割り当てていません。

オートパイロットの実行

MPPMウィンドウでVirtual Playersの設定が済んだ状態で(ひとつは host タグが必要です)、次の操作で実行できます。

  1. Assets/Autopilot/Settings/AutopilotSettings をInspectorウィンドウで開く
  2. 下のほうにある "Run" ボタンをクリック

これで再生モードに切り替わり、オートパイロット設定に従って自動プレイされます。 停止するには "Stop" ボタンをクリックします。

参考:Virtual PlayersでもAnjinが動作する仕組み

Anjinは起動状態を AutopilotState というScriptableObjectで永続化しています。"Run" ボタンをクリックされると、まず AutopilotState の状態が変わり、再生モードに入ります。 MPPMでは、再生モードに入るときにプロジェクトのファイルが共有されるため*2、Virtual Player側でも AutopilotState を見てオートパイロットが動き出します。

この仕組みのため、現状では次の制約があります*3

  • Editorは手動、Virtual Playerは自動、といった使い分けはできません
  • Playerによって別々のシナリオ(AutopilotSettings)を割り当てることはできません
  • 再生モード中にAutopilotSettingsの "Run" ボタンをクリックしたときは、Editorのみオートパイロットが起動します

[11/18追記] AutopilotStateのファイル更新がされたりされなかったりするようで、Anjin v1.8から明示的に AssetDatabase.SaveAssetIfDirty する修正を入れます。 またv1.8ではSlackReporterのメッセージを入力できるようになりますが、ここにタグを出力するプレースホルダも追加します。

参考:出力ファイル

各Virtual PlayerはMacOSの場合、プロジェクトルート/Library/VP/の下にユニークな名前のディレクトリが作られます。 Anjinの FileLogger などの出力ファイルパスを相対パスで指定した場合、このVirtual Playerのパスが起点となります。 つまりVirtual Playerごと個別のパスに出力されます。

ただし、UGUIPlaybackAgent が内部で使用しているAutomated QAパッケージは、スクリーンショットApplication.PersistentDataPath に出力します。これは変更できません。 従って、MPPM環境下で UGUIPlaybackAgent を使用すると、出力ファイル重複のエラーが出ます。

将来的にAnjinはAutomated QAパッケージ非依存になり、この制限事項は解消される予定です。

Play Mode Scenarios [11/18追記]

MPPMには、Virtual Playerでなくプレイヤービルド*4をローカルインスタンスとして起動するPlay Mode Scenarios機能もあります。 Play Mode Scenariosではタグでなく起動時引数を渡すことができ、インスタンスごとに別々のAutopilotSettingsを割り当てることも可能です。

ただし、Play Mode ScenariosはAutopilotSettingsの "Run" ボタンでは起動しないため、通常の再生モードを開始してから "Run" ボタンで(Editorのみ)オートパイロットを起動する必要があります。 また、タグはEditor側は1つしか選択できず、ローカルインスタンスには指定できません。 Virtual Playersとは使い勝手が異なるので注意してください。

Play Mode Scenariosについて詳しくは Test live instances locally and remotely | Unity Multiplayer を参照してください。

宣伝

冬コミ(C105)で、Anjinのあれこれを詰め込んだ『Anjin非公式ファンブック』を頒布予定です。夏から出す出す詐欺していましたが今度こそ間に合うはず。

表紙・挿絵は『江戸むらさき特急』の著者、ほりのぶゆき先生にお願いしました!

2日目 西う47b「いか小屋」でお待ちしております。 また、後日BOOTHでも頒布予定です。

*1:テストシナリオとしてゴールを設けることもできますが、基本は設定したタイムアウト時間まで動き続けます

*2:MacOSではシンボリックリンク

*3:AutopilotState を使わないか、Assets下に置かないようにできれば可用性は上がりそうなので、検討してみます

*4:Anjin v1.7で実験的機能としてプレイヤービルドに対応しています

メンテナンス不要でチュートリアルを突破するAnjinのオートパイロット設定

Anjin v1.3で追加された機能を使って、スマートフォンゲームによくあるアウトゲームのチュートリアル(次に押せるボタンが限定される系)を自動で突破するオートパイロット設定の方法を紹介します。

Anjinパッケージのインストール

Anjinパッケージ(com.dena.anjin)は、株式会社ディー・エヌ・エーが開発・OSSとして公開しているUnity向けオートパイロットフレームワークです*1。 Unityエディタ上で動作させることを前提とし、また基本的な設定をすべてUnityエディタ上で完結できます。 本記事の事例もノーコードです。

github.com

パッケージのインストールは、openupm-cli を使用して次のコマンドで行なうのが簡単です。

openupm add com.dena.anjin

Package Managerウィンドウによるインストール方法は、リポジトリのREADMEを参照してください。

インストールしたら、.gitignoreなどで次のパスをトラッキングしないよう設定しておきましょう。

/Assets/AutopilotState.asset*

チュートリアルを突破するオートパイロット設定

最小構成として、Agentの設定を2つと、それを統括するオートパイロット設定を作成します。

uGUI Monkey Agentの設定

uGUI Monkey Agentは、uGUIで構築されたUI画面をでたらめに操作する、いわゆるモンキーテストを行なうためのAgentです。 本記事ではこれをチュートリアルの操作に利用します。チュートリアル中、ユーザが実際に操作可能なUI要素以外はブロックされているのであれば、「操作できるものを操作する」だけでチュートリアルは進行するはずです*2

Projectウィンドウで右クリック > Create > Anjin > uGUI Monkey Agent でAgent設定ファイルが作られます。 これをInspectorウィンドウで開き、動作設定を行ないます。

細かくは適用するプロジェクトによりますが、Lifespacn Sec は0以外*3チュートリアルを突破できるまでの時間を余裕を持って指定します。 また Secs Searching Components には、操作できるUI要素が1つもない時間を何秒検出したらエラー(進行不能)と判断するかを指定します。スキップできないムービー再生や演出がある場合は考慮が必要です。

なお、MonkeyAgentに操作されたくないUI要素*4がある場合は、IgnoreUGUIMonkey コンポーネントを追加することで操作を避けることができます。

Time Bomb Agentの設定

Time Bomb(時限爆弾)Agentは、Anjin v1.3で追加されたAgentです。 別のAgentを内包して動作させ、そのAgentが動作を終えるまでに指定した「解除メッセージ」がログ出力されなければエラー終了するAgentです。

Projectウィンドウで右クリック > Create > Anjin > Time Bomb Agent でAgent設定ファイルが作られます。 これをInspectorウィンドウで開き、動作設定を行ないます。

Agent には、先に作ったMonkey Agentの設定ファイルを指定します。 Defuse Message には、チュートリアルを完遂したときにログ(UnityEngine.Debug.Log)出力している文字列*5を指定します。正規表現が使用できます。

Tutorial Monkey Agentは40秒間動作する設定です。40秒以内に解除メッセージ「チュートリアル突破!」がログに出力されればTime Bomb Agentは正常終了、40秒経過してTutorial Monkey Agentが終了してしまうとエラーとなります。

オートパイロット設定

最後にオートパイロット本体の設定です。

Projectウィンドウで右クリック > Create > Anjin > Autopilot Settings でオートパイロット設定ファイルが作られます。 これをInspectorウィンドウで開き、動作設定を行ないます。

ここでは、Sceneに対して、そのSceneを操作するAgentを割り当てます。 まず、最初に表示されるTitle sceneにはTitle To Tutorial Agentという名前のuGUI Playback Agentを指定し*6、TitleからHomeに遷移するよう設定しています(詳細は割愛します)。

続いて Home scene(ここでチュートリアルが進行する想定)に先のTime Bomb Agentを指定して、チュートリアルを突破させます。

設定を終えたら、Inspectorウィンドウを下までスクロールして Run ボタンをクリックするとその場で再生モードに切り替わり、オートパイロットが実行されます。

なお、図の設定ではチュートリアル突破後、Lifespan secに設定された60秒までなにもしないで終了します。 実際のプロジェクトでは、Time Bomb Agentの後にさらにuGUI Playback Agentで各画面を巡回するシナリオを組んだり、uGUI Monkey Agentでモンキーテストを実行するなりするとよいでしょう。

その他の新機能

Anjin v1.2およびv1.3で追加された機能を簡単に紹介します。

Play Modeテストからのオートパイロット実行

Inspectorウィンドウやコマンドラインからでなく、Play Modeテストコード内でオートパイロットを起動し、終了を待つことができるようになりました。 次のように使います。

[Test]
public async Task LaunchAutopilotFromTest()
{
  await LauncherFromTest.AutopilotAsync("Assets/Path/To/AutopilotSettings.asset");
}

uGUI Monkey Agentのスクリーンショット撮影オプション

uGUI Monkey Agentに、操作ごとにスクリーンショットを撮影するオプションが追加されました。 Inspectorウィンドウで有効化できます。

デフォルトでは、Application.persistentDataPath 下の "/TestHelper/Screenshots/" に Agentの名前+連番のファイルが保存されます。

batchmode起動サポート

CIなどでコマンドラインから起動するとき、-batchmodeを指定しても動作するようになりました。 Unityのbatchmode(ヘッドレスモード)には様々な制限がありますが、それを回避するハック(下記)によるものです。

www.nowsprinting.com

参考

swet.dena.com

swet.dena.com

*1:筆者がDeNA在籍時代に作ったものですが、すでにメンテナではありません。本記事で紹介している新機能は、現在お手伝いしているプロジェクトで使いそうなものをPull Requestし採用されたものです

*2:進行しない場合、専用のAgentを実装する必要があります

*3:0は無制限に動作します

*4:課金要素、広告表示、外部アプリ起動、デバッグメニューなど

*5:なにも出力していなければ、プロダクト側に手を加える必要があります

*6:uGUI Playback Agentは、マウス操作を記録してその通りに操作を再現するAgent