【Unity 6】UnityEvent<Collider>に専用クラスは必要?古いTriggerイベントのコードをシンプルにする方法

広告

UnityでTrigger判定を扱うとき、OnTriggerEnter() や OnTriggerStay() から UnityEvent を呼び出し、Inspectorで処理を設定できるようにすることがあります。

以前のコードでは、次のような専用クラスを定義していました。

[Serializable]
public class TriggerEvent : UnityEvent<Collider>
{
}

Unity 6の環境では、このようなクラスを定義しなくても、UnityEvent<Collider> を直接フィールドに使用できます。

今回は、既存の CollisionDetector を例に、コードの書き換え方と注意点を紹介します。

1. 変更前のCollisionDetector

まずは元のコードを見てみましょう。

using UnityEngine;
using System;
using UnityEngine.Events;

[RequireComponent(typeof(Collider))]
public class CollisionDetector : MonoBehaviour
{
    [SerializeField]
    private TriggerEvent onTriggerEnter = new TriggerEvent();

    [SerializeField]
    private TriggerEvent onTriggerStay = new TriggerEvent();

    private void OnTriggerStay(Collider other)
    {
        onTriggerStay.Invoke(other);
    }

    private void OnTriggerEnter(Collider other)
    {
        onTriggerEnter.Invoke(other);
    }

    [Serializable]
    public class TriggerEvent : UnityEvent<Collider>
    {
    }
}

このスクリプトは、Trigger判定を受け取って、Inspectorで登録したメソッドを実行するためのものです。

処理の流れは次のとおりです。

  1. 別のColliderがTrigger範囲に入る
  2. Unityが OnTriggerEnter() を呼び出す
  3. onTriggerEnter.Invoke(other) が実行される
  4. Inspectorで登録したメソッドが呼び出される

Trigger範囲内にいる間は、物理更新に応じて OnTriggerStay() も繰り返し呼び出されます。

この設計には、衝突検知と実際のゲーム処理を分離できる利点があります。

2. TriggerEventクラスは何をしていたのか?

注目したいのが次の部分です。

[Serializable]
public class TriggerEvent : UnityEvent<Collider>
{
}

これは、UnityEvent<Collider> を継承した専用クラスです。Collider 型の引数を1つ受け取るUnityEventであることを表しています。

ここでの TriggerEvent は、独自のメソッドもフィールドも追加していません。つまり、イベントの型に名前を付けるためだけに存在しています。

以前のUnityの教材やサンプルでは、ジェネリックなUnityEventをInspectorで扱うために、このような具体的な派生クラスを定義する書き方がよく使われていました。

3. Unity 6では直接書ける

Unity 6では、次のように宣言できます。

[SerializeField]
private UnityEvent<Collider> onTriggerEnter = new();

[SerializeField]
private UnityEvent<Collider> onTriggerStay = new();

new() は、フィールドの型(ここでは UnityEvent<Collider>)から、作るオブジェクトの型が自動で決まるC# 9以降の書き方です。詳しくは4章で説明します。

UnityEvent<Collider> をそのまま使用しているため、専用の TriggerEvent クラスは不要です。

呼び出し側は、これまでと同じ書き方です。

private void OnTriggerEnter(Collider other)
{
    onTriggerEnter.Invoke(other);
}

Invoke(other) で渡されたColliderを、イベントの登録先のメソッドが受け取れます。Inspectorでメソッドを登録するときは、引数を受け取るメソッドを「Dynamic Collider」の欄から選びます。

UnityEvent<Collider> を直接宣言しても、Inspectorからイベントを設定する機能はそのまま使えます。Unity公式マニュアルにも、ジェネリック型の UnityEvent を直接宣言する例が紹介されています。

4. 変更後のコード

それでは、元のコードを書き換えてみましょう。

using UnityEngine;
using UnityEngine.Events;

[RequireComponent(typeof(Collider))]
public class CollisionDetector : MonoBehaviour
{
    [SerializeField]
    private UnityEvent<Collider> onTriggerEnter = new();

    [SerializeField]
    private UnityEvent<Collider> onTriggerStay = new();

    private void OnTriggerEnter(Collider other)
    {
        onTriggerEnter.Invoke(other);
    }

    private void OnTriggerStay(Collider other)
    {
        onTriggerStay.Invoke(other);
    }
}

変更した点は、次の4つです。

  • TriggerEvent を UnityEvent<Collider> に変更
  • 不要になった TriggerEvent クラスを削除
  • 不要になった using System; を削除
  • 初期化を new TriggerEvent() から new() に変更

new() は、フィールドの型から生成するオブジェクトの型を判断する、C# 9以降の書き方です。たとえば、

private UnityEvent<Collider> onTriggerEnter
    = new UnityEvent<Collider>();

と書く代わりに、

private UnityEvent<Collider> onTriggerEnter = new();

と記述できます。型を2回書く必要がなくなり、コードが読みやすくなります。

5. 既存のInspector設定を確認しよう

すでにシーンやPrefabで TriggerEvent にメソッドを登録している場合は、書き換えのあとにInspectorの設定を確認しましょう。フィールド名(onTriggerEnter など)を変えなければ、登録内容をそのまま使える形で進められます。次の手順がおすすめです。

手順1:シーンとPrefabをバックアップする

Gitで管理している場合は、変更前にコミットしておくと安全です。

手順2:コードを変更する

フィールドの型を変更し、専用の TriggerEvent クラスを削除します。フィールド名は変更しないでください。

手順3:Unityでコンパイル結果を確認する

Consoleにエラーが表示されていないか確認します。

手順4:Inspectorでイベント登録を確認する

On Trigger Enter と On Trigger Stay に設定していたオブジェクトやメソッドが維持されているか確認します。

実際に、On Trigger Stay に EnemyMove.OnDetectObject を登録していた CollisionDetector を、Unity 6(6000.3)のシーンで書き換えてみました。コンパイルエラーは出ず、登録もそのまま残っていました。

手順5:Playモードで動作を確認する

実際にTrigger範囲へオブジェクトを出入りさせ、登録したメソッドが正常に呼び出されることを確認します。

もし登録が外れていた場合は、バックアップから戻すか、Inspectorから登録し直してください。

既存のシーンやPrefabがない新規プロジェクトなら、最初から新しい書き方で始めて構いません。

6. なぜUnityEventを使うのか?

そもそも、Triggerイベントを受け取るだけなら、UnityEvent を使わなくても書けます。たとえば、プレイヤーが範囲に入ったことを直接検知するなら、次のようになります。

private void OnTriggerEnter(Collider other)
{
    if (other.CompareTag("Player"))
    {
        Debug.Log("プレイヤーが入った");
    }
}

一方、今回の CollisionDetector は、衝突した相手を別の処理へ通知する役割に徹しています。

private void OnTriggerEnter(Collider other)
{
    onTriggerEnter.Invoke(other);
}

この形なら、Triggerを検知した後の処理をInspectorから変更できます。敵の索敵範囲への侵入通知、アイテム取得処理の呼び出し、ギミックの起動などに応用できます。

ただし、CollisionDetector 自身は、相手がプレイヤーなのか敵なのかを判断していません。相手を限定したい場合は、登録先のメソッドの中で判定を行います。

また、UnityEvent が便利だからといって、すべての処理をInspectorに登録する必要はありません。処理が複雑になる場合や、コード上で依存関係を明確に管理したい場合は、通常のC#メソッドやイベントを使う方が適していることもあります。

7. Triggerを動作させるための注意点

今回のスクリプトには、次の属性が付いています。

[RequireComponent(typeof(Collider))]

これは、同じGameObjectにColliderコンポーネントが必要であることをUnityへ伝えるものです。スクリプトを追加するときに、Colliderがなければ自動で追加されます。

ただし、これだけではTriggerイベントは発生しません。3D物理でTriggerイベントを受け取るには、次の条件が必要です。

  • Triggerとして使うColliderで Is Trigger をオンにする(通常は、このスクリプトを付けたColliderです)
  • 衝突する2つのオブジェクトのうち、少なくとも一方にRigidbodyを付ける

うまく動かないときは、まずこの2点を確認しましょう。

また、OnTriggerStay() は、Update() のような描画フレームごとの呼び出しではなく、物理シミュレーションの更新に合わせて繰り返し呼び出されます。

たとえば OnTriggerStay() にダメージ処理を登録すると、意図せず短時間に何度もダメージを与えてしまうことがあります。継続ダメージなどを実装する場合は、経過時間やダメージ間隔を管理する仕組みを別途用意しましょう。

8. まとめ

Unity 6では、UnityEvent<Collider> を直接フィールドに宣言できるため、型を定義するだけの専用クラスは不要です。

変更前

[SerializeField]
private TriggerEvent onTriggerEnter = new TriggerEvent();

[Serializable]
public class TriggerEvent : UnityEvent<Collider>
{
}

変更後

[SerializeField]
private UnityEvent<Collider> onTriggerEnter = new();

この変更により、コードの行数が減り、イベントの引数の型も宣言部分から直接わかるようになります。

専用の型名を付けたい場合や、独自の機能を加えたい場合には、これまでどおり継承クラスを定義する意味があります。今回のように、独自の処理を追加していないクラスであれば、まずは直接宣言する書き方を試してみてください。

大切なのは、コードを短くすること自体ではなく、役割が同じなら余分な定義を増やさず、読みやすいコードにすることです。

参考資料

訪問数 12 回, 今日の訪問数 13回

広告