PlayModeテストとは?
PlayModeテストは、Unity のゲーム実行環境でテストを実行する機能です。
EditModeテストとは異なり、実際にゲームを起動した状態でテストを行うため、MonoBehaviourのライフサイクルメソッド(Start, Updateなど)やGameObjectの動作、物理演算などを検証できます。
ゲームの実際の挙動をテストする必要がある場合に、PlayModeテストが活躍します。
PlayModeテストが必要な場面
PlayModeテストは、以下のような場面で使用します。
MonoBehaviourのライフサイクルをテストする
Awake(), Start(), Update()などのライフサイクルメソッドは、EditModeでは自動的に呼ばれません。これらのメソッドの動作を確認するには、PlayModeテストが必要です。
GameObjectやコンポーネントの動作確認
実際にGameObjectを生成し、コンポーネントを追加して、その動作をテストできます。
時間経過を伴う処理のテスト
複数フレームにわたる処理や、時間経過による状態変化をテストできます。
コルーチンのテスト
StartCoroutine()で実行するコルーチンの動作を検証できます。
物理演算のテスト
RigidbodyやColliderを使った物理演算の挙動をテストできます。
基本的なPlayModeテストの書き方
PlayModeテストでは、[UnityTest]属性とIEnumeratorを使用します。
シンプルなPlayModeテスト
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class SimplePlayModeTest
{
[UnityTest]
public IEnumerator GameObjectCreation_CreatesGameObject()
{
// Arrange & Act
var gameObject = new GameObject("TestObject");
// 1フレーム待機
yield return null;
// Assert
Assert.IsNotNull(gameObject);
Assert.AreEqual("TestObject", gameObject.name);
// クリーンアップ
Object.Destroy(gameObject);
}
}
重要なポイント:
[UnityTest]属性を使用([Test]ではない)- 戻り値の型は
IEnumerator yield return nullで1フレーム待機- テスト後は
Object.Destroy()でオブジェクトを削除
実装例
MonoBehaviourのテスト
MonoBehaviourのライフサイクルメソッドをテストします。
テスト対象のクラス:
using UnityEngine;
public class Counter : MonoBehaviour
{
public int count = 0;
public bool isStarted = false;
private void Start()
{
isStarted = true;
count = 10;
}
private void Update()
{
count++;
}
public void Reset()
{
count = 0;
}
}
テストコード:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class CounterTest
{
[UnityTest]
public IEnumerator Start_InitializesCountToTen()
{
// Arrange
var gameObject = new GameObject();
var counter = gameObject.AddComponent<Counter>();
// Act
// 1フレーム待機してStart()が呼ばれるのを待つ
yield return null;
// Assert
Assert.IsTrue(counter.isStarted);
Assert.AreEqual(10, counter.count);
// Cleanup
Object.Destroy(gameObject);
}
[UnityTest]
public IEnumerator Update_IncrementsCountEveryFrame()
{
// Arrange
var gameObject = new GameObject();
var counter = gameObject.AddComponent<Counter>();
// Start()が呼ばれるまで待機
yield return null;
int initialCount = counter.count;
// Act
// 3フレーム待機
yield return null;
yield return null;
yield return null;
// Assert
Assert.AreEqual(initialCount + 3, counter.count);
// Cleanup
Object.Destroy(gameObject);
}
[UnityTest]
public IEnumerator Reset_SetsCountToZero()
{
// Arrange
var gameObject = new GameObject();
var counter = gameObject.AddComponent<Counter>();
yield return null;
// Act
counter.Reset();
// Assert
Assert.AreEqual(0, counter.count);
// Cleanup
Object.Destroy(gameObject);
}
}
時間経過を伴うテスト
タイマー機能をテストします。
テスト対象のクラス:
using UnityEngine;
public class Timer : MonoBehaviour
{
public float duration = 5f;
public bool isFinished = false;
private float _elapsedTime = 0f;
private void Update()
{
if (isFinished) return;
_elapsedTime += Time.deltaTime;
if (_elapsedTime >= duration)
{
isFinished = true;
}
}
public float GetElapsedTime()
{
return _elapsedTime;
}
}
テストコード:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class TimerTest
{
[UnityTest]
public IEnumerator Timer_CompletesAfterDuration()
{
// Arrange
var gameObject = new GameObject();
var timer = gameObject.AddComponent<Timer>();
timer.duration = 1f;
yield return null; // Start()を待つ
// Act
// 1秒待機
yield return new WaitForSeconds(1f);
// Assert
Assert.IsTrue(timer.isFinished);
Assert.GreaterOrEqual(timer.GetElapsedTime(), 1f);
// Cleanup
Object.Destroy(gameObject);
}
[UnityTest]
public IEnumerator Timer_NotFinishedBeforeDuration()
{
// Arrange
var gameObject = new GameObject();
var timer = gameObject.AddComponent<Timer>();
timer.duration = 2f;
yield return null;
// Act
yield return new WaitForSeconds(0.5f);
// Assert
Assert.IsFalse(timer.isFinished);
// Cleanup
Object.Destroy(gameObject);
}
}
コルーチンのテスト
コルーチンの動作をテストします。
テスト対象のクラス:
using System.Collections;
using UnityEngine;
public class DelayedAction : MonoBehaviour
{
public bool actionExecuted = false;
public void StartDelayedAction(float delay)
{
StartCoroutine(ExecuteAfterDelay(delay));
}
private IEnumerator ExecuteAfterDelay(float delay)
{
yield return new WaitForSeconds(delay);
actionExecuted = true;
}
}
テストコード:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class DelayedActionTest
{
[UnityTest]
public IEnumerator DelayedAction_ExecutesAfterDelay()
{
// Arrange
var gameObject = new GameObject();
var delayedAction = gameObject.AddComponent<DelayedAction>();
yield return null;
// Act
delayedAction.StartDelayedAction(0.5f);
// 実行前の確認
Assert.IsFalse(delayedAction.actionExecuted);
// 0.5秒待機
yield return new WaitForSeconds(0.5f);
// Assert
Assert.IsTrue(delayedAction.actionExecuted);
// Cleanup
Object.Destroy(gameObject);
}
}
Physicsのテスト
物理演算を使ったテストの例です。
テスト対象のクラス:
using UnityEngine;
public class Projectile : MonoBehaviour
{
private Rigidbody _rigidbody;
private void Awake()
{
_rigidbody = GetComponent<Rigidbody>();
}
public void Launch(Vector3 force)
{
_rigidbody.AddForce(force, ForceMode.Impulse);
}
}
テストコード:
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class ProjectileTest
{
[UnityTest]
public IEnumerator Launch_MovesProjectileUpward()
{
// Arrange
var gameObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
var rigidbody = gameObject.AddComponent<Rigidbody>();
var projectile = gameObject.AddComponent<Projectile>();
yield return null; // Awake()を待つ
Vector3 initialPosition = gameObject.transform.position;
// Act
projectile.Launch(Vector3.up * 10f);
// 物理演算を進める
yield return new WaitForFixedUpdate();
yield return new WaitForFixedUpdate();
// Assert
Assert.Greater(gameObject.transform.position.y, initialPosition.y);
// Cleanup
Object.Destroy(gameObject);
}
[UnityTest]
public IEnumerator Launch_AppliesForceToRigidbody()
{
// Arrange
var gameObject = GameObject.CreatePrimitive(PrimitiveType.Sphere);
var rigidbody = gameObject.AddComponent<Rigidbody>();
var projectile = gameObject.AddComponent<Projectile>();
yield return null;
// Act
projectile.Launch(Vector3.forward * 5f);
yield return new WaitForFixedUpdate();
// Assert
Assert.Greater(rigidbody.velocity.magnitude, 0f);
// Cleanup
Object.Destroy(gameObject);
}
}
テストシーンの活用
複雑なテストでは、専用のテストシーンを作成すると便利です。
テストシーンの作成
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.TestTools;
public class SceneBasedTest
{
[UnitySetUp]
public IEnumerator SetUp()
{
// テストシーンを読み込む
yield return SceneManager.LoadSceneAsync("TestScene", LoadSceneMode.Single);
}
[UnityTest]
public IEnumerator TestScene_ContainsPlayer()
{
// シーン内のオブジェクトを検索
var player = GameObject.Find("Player");
// Assert
Assert.IsNotNull(player);
yield return null;
}
[UnityTearDown]
public IEnumerator TearDown()
{
// テスト後のクリーンアップ
yield return SceneManager.LoadSceneAsync("EmptyScene", LoadSceneMode.Single);
}
}
プレハブを使ったテスト
using System.Collections;
using NUnit.Framework;
using UnityEngine;
using UnityEngine.TestTools;
public class PrefabTest
{
[UnityTest]
public IEnumerator LoadPrefab_InstantiatesCorrectly()
{
// Arrange
var prefab = Resources.Load<GameObject>("Prefabs/Enemy");
Assert.IsNotNull(prefab, "プレハブが見つかりません");
// Act
var instance = Object.Instantiate(prefab);
yield return null;
// Assert
Assert.IsNotNull(instance);
Assert.IsNotNull(instance.GetComponent<Enemy>());
// Cleanup
Object.Destroy(instance);
}
}
よく使うテストパターン
フレーム待機のパターン
// 1フレーム待機
yield return null;
// 複数フレーム待機
for (int i = 0; i < 10; i++)
{
yield return null;
}
物理演算の待機
// FixedUpdate 1回分待機
yield return new WaitForFixedUpdate();
// 物理演算を複数回進める
for (int i = 0; i < 5; i++)
{
yield return new WaitForFixedUpdate();
}
時間待機
// 実時間で待機
yield return new WaitForSeconds(1f);
// スケールされた時間で待機
yield return new WaitForSecondsRealtime(1f);
条件が満たされるまで待機
// 条件が真になるまで待機
yield return new WaitUntil(() => enemy.isDead);
// 条件が偽になるまで待機
yield return new WaitWhile(() => player.isMoving);
タイムアウト付き待機
[UnityTest]
public IEnumerator WaitForCondition_WithTimeout()
{
var gameObject = new GameObject();
var component = gameObject.AddComponent<MyComponent>();
yield return null;
// 最大5秒待機
float timeout = 5f;
float elapsed = 0f;
while (!component.isReady && elapsed < timeout)
{
yield return null;
elapsed += Time.deltaTime;
}
Assert.IsTrue(component.isReady, "タイムアウト: 条件が満たされませんでした");
Object.Destroy(gameObject);
}
注意点
PlayModeテストは実行時間が長い
PlayModeテストは、実際にゲームを起動するため、EditModeテストよりも実行時間が長くなります。
頻繁に実行するテストはEditModeに、GameObjectや物理演算が必要なテストのみPlayModeにすることで、テスト全体の実行時間を短縮できます。
テストの独立性を保つ
各テストは独立して実行できる必要があります。テスト間で状態が共有されないよう、適切にクリーンアップを行いましょう。
[UnityTest]
public IEnumerator MyTest()
{
// Arrange
var gameObject = new GameObject();
yield return null;
// Act & Assert
// テストのコード
// Cleanup(必ず実行)
Object.Destroy(gameObject);
}
TearDownの活用
複数のテストで共通のクリーンアップ処理がある場合は、[UnityTearDown]を使用します。
public class MyPlayModeTest
{
private GameObject _testObject;
[UnitySetUp]
public IEnumerator SetUp()
{
_testObject = new GameObject("TestObject");
yield return null;
}
[UnityTearDown]
public IEnumerator TearDown()
{
if (_testObject != null)
{
Object.Destroy(_testObject);
}
yield return null;
}
[UnityTest]
public IEnumerator Test1()
{
Assert.IsNotNull(_testObject);
yield return null;
}
[UnityTest]
public IEnumerator Test2()
{
Assert.AreEqual("TestObject", _testObject.name);
yield return null;
}
}
Time.timeScaleに注意
Time.timeScaleを変更すると、WaitForSecondsなどの待機時間に影響します。テスト後は必ず元に戻しましょう。
[UnityTest]
public IEnumerator TimeScale_Test()
{
// 元の値を保存
float originalTimeScale = Time.timeScale;
try
{
// 時間を加速
Time.timeScale = 2f;
// テストのコード
yield return new WaitForSeconds(1f); // 実際は0.5秒
Assert.Pass();
}
finally
{
// 必ず元に戻す
Time.timeScale = originalTimeScale;
}
}
まとめ
PlayModeテストは、GameObjectやMonoBehaviourの実際の動作をテストするための強力なツールです。
EditModeテストでは検証できない、ライフサイクルメソッドや物理演算、コルーチンなどをテストできます。実行時間は長くなりますが、ゲームの挙動を確実に検証するためには欠かせません。
テストの使い分け:
- EditMode: ロジック、計算、データクラスなどの高速テスト
- PlayMode:
GameObject、MonoBehaviour、物理演算などの実行環境が必要なテスト
両方を組み合わせて使用することで、包括的なテストを実現できます。
ぜひプロジェクトにPlayModeテストを導入して、品質の高いゲーム開発を実現してください!