[Unity] 実行中動作の自動テスト方法
PlayModeテストは、Unityのテストフレームワークの一部であり、ゲームが実際にプレイされている状態(Play Mode)での挙動を自動的にテストするために使用されます。これは、ゲームのシーンが実行され、ゲームオブジェクトが実際に動いている状況でのテストを可能にし、ゲームの動作や特定のフィーチャーが期待通りに動作するかを確認するために役立ちます。
PlayerMode
PlayModeテストの特徴
- 実行環境: PlayModeテストは、UnityエディターがPlay Modeにあるときに実行されます。これにより、ゲームの実際の実行環境でテストが行われ、ゲームオブジェクトのインスタンス化、シーンの読み込み、アニメーション、物理演算など、実行時の挙動をテストできます。
- 非同期処理のテスト: PlayModeテストは、
IEnumerator
を返すメソッドを利用して、コルーチンを使用することができます。これにより、非同期処理や、特定の時間を待機する処理(例えば、アニメーションの完了や非同期のロード処理など)のテストが可能になります。 - ユーザーインタラクションのシミュレーション: プレイヤーの入力や操作に反応するロジックをテストする場合に有用です。例えば、仮想的なキープレスやマウスクリックをシミュレートして、それに対するゲームの反応をテストすることができます。
PlayModeテストの作成方法
- UnityTest属性を使用する: PlayModeテストメソッドには
[UnityTest]
属性を使用します。これは、テストランナーに対して、該当のメソッドがPlayModeで実行されるテストであることを示します。 - IEnumeratorを返す: PlayModeテストメソッドは
IEnumerator
を返す必要があり、yield return null;
を使ってフレームをスキップしたり、yield return new WaitForSeconds(seconds);
を使って特定の時間だけ処理を待機させたりすることができます。 - Assertクラスを使用する: NUnitの
Assert
クラスを使用して、テストの条件を指定します。例えば、あるアクションが期待した結果を生んだかどうかを検証します。
注意点
- PlayModeテストは実行に時間がかかることがあります。これは、シーンのロードや非同期処理の待機など、実行時の状態を再現するためです。
- EditModeテストとPlayModeテストを適切に分けることが重要です。単純なロジックや関数のテストはEditModeテストで十分な場合が多く、PlayModeテストは実際のゲームプレイに近い環境でのテストに特化させるべきです。
準備
Unity Test Frameworkをインストールする方法は、Unityのドキュメントやリソースに従って行うことができます。一般的には、Unity開発環境にシームレスに統合されるいくつかのステップが含まれます。以下は、その基本的な手順の概要です:
- Unityプロジェクトを開く:テストを含めたいUnityプロジェクトを開きます。
- パッケージマネージャーにアクセスする:
- Unityエディタで、Window(ウィンドウ) > Package Manager(パッケージマネージャー) を選択します。これでパッケージマネージャーウィンドウが開きます。
- Test Frameworkパッケージを探す:
- パッケージマネージャーで、"Unity Registry"を選択してUnityが提供するすべての利用可能なパッケージを表示する必要があるかもしれません。"Test Framework"を検索します。
- Test Frameworkをインストールする:
- Test Frameworkパッケージを見つけたら、それをクリックしてから"Install(インストール)"ボタンをクリックします。Unityがインストールプロセスを処理します。
- テストの設定:
- インストール後、Window(ウィンドウ) > General(一般) > Test Runner(テストランナー) に移動してテストランナーウィンドウを開き、テストの作成、管理、実行を行うことができます。
- EditModeテスト(エディター内で実行されるテスト)またはPlayModeテスト(プレイモードで実行されるテスト)を書くオプションがあります。
- テストの作成:
- テストを書くには、プロジェクト内に新しいC#スクリプトを作成します。通常、Testsなどのフォルダ内に作成すると整理しやすくなります。
NUnit
フレームワークの構文を使用してテストを構造化します。Unity Test FrameworkはNUnitに基づいて構築されているため、テストメソッドの[Test]
、セットアップメソッドの[SetUp]
など、NUnitの属性をサポートしています。
- テストの実行:
- テストランナーウィンドウを使用してテストを実行します。すべてのテストを実行することも、特定のテストを選択して実行することもできます。テストランナーは、どのテストが合格したか、または失敗したかについてのフィードバックを提供します。
- 結果を確認する:
- テストを実行した後、テストランナーウィンドウで結果を確認します。失敗したテストを調査して、何が問題だったのかを理解し、コードまたはテストに必要な調整を行います。
このプロセスにより、Unityプロジェクトにテストを統合し、ゲームやアプリケーションが期待通りに動作することを確認するための単体テストを作成して実行することができます。自動テストは、開発プロセスの早い段階でエラーを捕捉することで、プロジェクトの品質と安定性を大幅に向上させることができます。
テストの環境
敵に見立てた球(Sphere)とプレイヤーに見立てたカプセル(Capsel)をシーンに配置しています
実行結果
Test Runner
自動テスト結果
ゲーム実行
エディターを実行し、上向きカーソルでプレイヤーを移動
コード
テスト評価用コードの概要
1. TriggerEventDetector
クラス
UnityEvent
を公開し、Unityのエディタからイベントリスナーをアタッチ可能にします。OnTriggerStay
メソッドを使用して、オブジェクトがトリガーコライダー内に滞在している間、特定のイベント(この場合はOnEnter
イベント)を発火させます。
2. MockInputProvider
クラス
- テストや特定の入力シミュレーションに使用するための入力プロバイダーのモックを提供します。
GetAxis
メソッドをオーバーライドして、指定された軸名に対する入力値(value
)を返します。
3. Initializer
クラス
- ゲーム開始時に、プレイヤーの入力プロバイダーを設定し、特定のイベントリスナーをトリガーオブジェクトにアタッチします。
OnEnterFromEnemy
メソッドは、プレイヤーが敵(この場合はトリガーオブジェクト)に触れた時に呼び出されるアクションを定義します。
4. PlayerController
クラス
- プレイヤーの移動やアクションを制御するためのクラスです。
SetInputProvider
メソッドで入力プロバイダーを設定し、Update
メソッド内でプレイヤーの移動を処理します。
5. IInputProvider
インターフェイス
- 入力プロバイダーが実装すべき基本的なインターフェイスです。
GetAxis
メソッドを通じて、特定の軸に対する入力値を取得します。
6. ユニットテスト
PreConfiguredSceneTest
クラスを使用して、プレイヤーが移動するとイベントが正しくトリガーされるかをテストします。- シーンを非同期でロードし、指定された条件下でのプレイヤーの挙動やイベントの発火を検証します。
この実装例は、Unityでのイベント駆動型プログラミングの基本を理解し、ゲーム内でのオブジェクト間の相互作用を管理する方法を学ぶのに役立ちます。
TriggerEventDetector
トリガーイベントの検出と処理
このコードは、Unityのイベントシステムを利用して特定のイベントを検出する機能を提供します。クラス内では、UnityEvent
型のOnEnter
という公開フィールドを定義し、これによりUnityのエディタからイベントリスナーをアタッチできるようになっています。OnTriggerStay
メソッドは、他のオブジェクトがこのオブジェクトのトリガーコライダー内に滞在している間中、継続的に呼び出されます。このメソッド内でOnEnter.Invoke()
を呼び出すことで、OnEnter
イベントにアタッチされた全てのリスナーにイベントが発火されます。これにより、ゲームオブジェクトが特定のエリアにいる間に特定のアクションを実行するなど、柔軟なイベント駆動型のプログラミングが可能になります。
using UnityEngine;
using UnityEngine.Events; // UnityEventに必要
public class TriggerEventDetector : MonoBehaviour
{
// UnityEventを公開して、エディタからイベントリスナーをアタッチできるようにする
public UnityEvent OnEnter;
private void OnTriggerStay(Collider other)
{
// UnityEventのInvokeメソッドを呼び出して、イベントを発火させる
OnEnter.Invoke();
}
}
IInputProvider
入力管理のためのインターフェース設計
このコードは、ゲーム内でユーザーの入力を管理するためのインターフェースを定義しています。IInputProvider
インターフェースは、GetAxis
というメソッドを含んでおり、これは特定の軸名を受け取って、その軸の入力値(通常は-1から1の間の浮動小数点数)を返します。例えば、プレイヤーが左右に動かすための入力や、上下に視点を移動するための入力を取得するのに使うことができます。このような設計により、異なる入力デバイスや入力方式でも、ゲームのコントローラー部分を柔軟に対応させることが可能になります。
public interface IInputProvider
{
float GetAxis(string axisName);
}
MockInputProvider
入力値模倣によるテストの簡素化
これはIInputProvider
インターフェースを実装し、入力値を模倣するために使用されます。コンストラクタでは、value
フィールドに初期値を設定し、GetAxis
メソッドを通じてこの値を取得することができます。このクラスは、特定の入力値をテストする際に役立ちます。
public class MockInputProvider : IInputProvider
{
private float value;
public MockInputProvider(float value)
{
this.value = value;
}
public float GetAxis(string axisName)
{
return value;
}
}
Initializer
ゲームオブジェクト初期化とイベントリスニングの実装
このコードは、Unityでゲームの初期設定を行うスクリプトです。PlayerController
オブジェクトにUnityInputProvider
を設定し、前方への移動をシミュレートします。また、"TriggerObject"
タグが付けられたトリガーオブジェクトを検索し、そのオブジェクトにTriggerEventDetector
コンポーネントのOnEnter
イベントにリスナーとしてOnEnterFromEnemy
メソッドを登録します。これにより、敵に接触した際の処理をログに記録します。このスクリプトは、ゲームオブジェクトの初期化とイベントベースのインタラクションの設定を行う例を示しています。
using UnityEngine;
public class Initializer : MonoBehaviour
{
[SerializeField]
PlayerController playerController;
void Start()
{
// MockInputProviderをセット
playerController.SetInputProvider(new UnityInputProvider()); // 前方に移動する入力をシミュレート
// トリガーオブジェクトを取得してイベントリスナーをセット
var triggerObject = GameObject.FindWithTag("TriggerObject");
triggerObject.GetComponent<TriggerEventDetector>().OnEnter.AddListener(OnEnterFromEnemy);
}
public void OnEnterFromEnemy()
{
Debug.Log("エネミーに触れた");
}
}
PlayerController
入力依存型プレイヤー移動制御システム
このコードはプレイヤーの入力を管理し、キャラクターの移動を実現するためのものです。Start
メソッドでCharacterController
コンポーネントを取得し、SetInputProvider
メソッドで入力プロバイダーを設定します。Update
メソッドでは、設定された入力プロバイダーから水平方向と垂直方向の入力値を取得し、それを使ってキャラクターの前方向への移動を計算しています。これにより、さまざまな入力デバイスに対応したプレイヤーの移動制御が可能になります
using UnityEngine;
public class PlayerController : MonoBehaviour
{
private IInputProvider inputProvider;
CharacterController characterController;
void Start()
{
characterController = GetComponent<CharacterController>();
}
public void SetInputProvider(IInputProvider provider)
{
inputProvider = provider;
}
void Update()
{
if (inputProvider == null) return;
float moveHorizontal = inputProvider.GetAxis("Horizontal");
float moveVertical = inputProvider.GetAxis("Vertical");
// 移動ロジック...
characterController.Move(Vector3.forward * moveVertical * Time.deltaTime * 10);
}
}
PreConfiguredSceneTest
非同期シーンロードとイベントトリガーをテストする
このコードは、プレイヤーが動いた時にイベントをトリガーするテストを実装しています。非同期的にシーンをロードし、シーン内のプレイヤーオブジェクトとトリガーオブジェクトを検索します。プレイヤーが前方に移動する入力をシミュレートし、トリガーオブジェクトに接触すると、triggered
フラグを真に設定します。このテストは、プレイヤーが期待通りにイベントをトリガーするかを検証します。
using UnityEngine;
using UnityEngine.TestTools;
using NUnit.Framework;
using System.Collections;
using UnityEngine.SceneManagement;
public class PreConfiguredSceneTest
{
private GameObject player;
private PlayerController playerController;
private bool triggered = false;
[SetUp]
public void SetUp()
{
// ここに初期化処理を記述する
}
[UnityTest]
public IEnumerator PlayerTriggersEventOnMove()
{
// アプリケーションは、現在のシーンが実行されている間、バックグラウンドでシーンをロードします。
// これは、ローディング画面を作成するのに特に適しています。
AsyncOperation asyncLoad = SceneManager.LoadSceneAsync("SampleScene");
// 非同期シーンのロードが完了するまで待ちます
while (!asyncLoad.isDone)
{
yield return null;
}
// シーンからプレイヤーオブジェクトを取得
player = GameObject.FindWithTag("Player");
Assert.That(player, Is.Not.Null, "プレイヤーオブジェクトがシーン内に見つかりませんでした。");
// PlayerControllerコンポーネントを取得
playerController = player.GetComponent<PlayerController>();
Assert.That(playerController, Is.Not.Null, "プレイヤーにPlayerControllerコンポーネントが見つかりませんでした。");
// MockInputProviderをセット
playerController.SetInputProvider(new MockInputProvider(1.0f)); // 前方に移動する入力をシミュレート
// トリガーオブジェクトを取得してイベントリスナーをセット
var triggerObject = GameObject.FindWithTag("TriggerObject");
Assert.That(triggerObject, Is.Not.Null, "トリガーオブジェクトがシーン内に見つかりませんでした。");
triggerObject.GetComponent<TriggerEventDetector>().OnEnter.AddListener(() => triggered = true);
// ゲームのフレームを進める
yield return new WaitForSeconds(0.5f);
// プレイヤーがトリガーオブジェクトに接触してイベントが発生したか検証
Assert.That(triggered, Is.True, "プレイヤーが予期されたイベントをトリガーしませんでした。");
}
}
ディスカッション
コメント一覧
まだ、コメントがありません