シンプルなサンプルで見るUnityEvent

2022年8月17日

イベントとは、何かきっかけがあったら特定のメソッドが実行される仕組みです

イベントを作るの必要なもの

きっかけ

きっかけは、次のようなものがあります

  • ボタンを押された
  • キーが入力された
  • 時間がきた(タイマー)
  • オブジェクト同士が衝突した

特定のイベント

きっかけから次のようなことをしたくなります

  • 計算する
  • 得点が増える
  • ライフが減る
  • ゲームオーバになる

イベントの仕組みなしで実現する場合

スペースキーの押下を敵にやられたと見立てます
次のコードで問題ないし、十分なようです
わざわざ、何か難しそうなイベントシステムを考える必要もなさそうです
これより複雑になりそうで、メリットが見えません

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        Debug.Log("ライフが減った");
    }
}

「ここで、完成、もう機能追加をしない」であれば、OKです

でも、次のような機能追加を考えている場合どうでしょう
(あなたかまたは、遊んでくれた人からの要望かもしれません)

void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        Debug.Log("ライフが減った");
        Debug.Log("もし、ライフが0ならゲームオーバー");
        Debug.Log("自分を消滅させる");
    }
}
  • ライフが0になったらゲームオーバー
  • 自分のオブジェクトを消す

この程度なら、まだ、ifブロックに追加して対応できますね。まだメリットがわかりません

さらに、次のように要望はどうでしょう?

  • 派手な爆発アニメーションを実行したい
  • タイマーを一時停止したい
  • 背景を点滅させたい
  • 今、シーンに存在しているすべての敵のスピードを一時的に遅くしたい
  • などなど・・・

どうでしょう?やめて!もう色々言わないでってなりますね
ifブロックがどんどん膨れてきます。頑張ってメソッド呼び出しにしても、次のようなことが起こります

  • 都度メソッド呼び出しコードを追加しなければなりません
  • メソッド本体も自分のクラス内にすべて記述するのが見やすいのか?
  • 要望が増えると次第に読みづらくなってくる
  • それぞれのメソッドも膨れ上がる可能性がある
void Update()
{
    if (Input.GetKeyDown(KeyCode.Space))
    {
        DecrementLife();
        Debug.Log("もし、ライフが0ならゲームオーバー");
        DestroyMe();
    }
}
void DecrementLife()
{
    Debug.Log("ライフが減った");
    LifeCount--;
    Debug.Log("ライフの表示を更新(1減っている)");
}
void DestryMe()
{
    Debug.Log("自分を消滅させる");
    Destroy(gameObject);
}

すべてのメソッドをこのクラスに入れるとすべての要望を叶えたとしても膨大な行数になるでしょう。また、他のクラスのメソッドを呼ぶにもアクセスするためにFindやGetComponentをさらに追加する必要があり、非常に煩わしいです

それを嫌って、プレイヤーのお仕事でないメソッドを入れるとオブジェクト指向の設計原則に違反してしまいます。つまり、見づらく、拡張や修正が厄介なコードの完成になります

イベントの仕組みを使ってみます

機能の追加をする予定がある(大抵そうでしょうが)時、素直にコードを追加していくと大変になりそうだというのはわかりました。これより楽になる方法があるのか?ですよね

コード

イベント版を作成しました(クラス名は、Eventクラスとしています)

using UnityEngine;
using UnityEngine.Events;

public class Event : MonoBehaviour
{
    public UnityEvent unityEvent;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            unityEvent.Invoke();
        }
    }
}

宣言の部分を見てみましょう

public UnityEvent unityEvent;

public修飾子と型名がUnityEventのunityEventフィールドが宣言されています
unityEventフィールドには、メソッドを代入できます(複数でも可能)
ただ、代入のコードが見当たりませんね

代入の説明は後にして、次の行で、代入された(登録とも言います)メソッドを実行します

unityEvent.Invoke();

実行するメソッド(イベントハンドラ)を登録する方法

①インスペクタで登録する方法

コードをいずれかのオブジェクトにアタッチするとインスペクタは次のようになります
実行前に登録することになります

Unity UIのボタンイベントを作成したことがある前提

ボタンが押下された時、どのゲームオブジェクトのどのメソッド(イベントハンドラ)が実行されるかを登録することができましたね。これで同じことが可能になります

②コードで登録する方法

この方法では、実行中にでも登録、解除が可能です

シーン

ダミーとしてUIのテキストオブジェクトを追加、UIControllerスクリプトをアタッチしています

コード

衝突した時に、このメソッドを実行したいとします
①インスペクタで登録する方法は、皆さんできる前提としていますので、今回は、コードで同じことを実行することにします

using UnityEngine;

public class UIController : MonoBehaviour
{
    public void ShowLife()
    {
        Debug.Log("ライフの表示を更新");
    }
}

コードにShowLifeメソッドの登録を追加します

using UnityEngine;

public class UIController : MonoBehaviour
{
    public Event eventScript;

    void Awake()
    {
        eventScript.unityEvent.AddListener(ShowLife);
    }
    public void ShowLife()
    {
        Debug.Log("ライフの表示を更新");
    }
}

ドラッグ&ドロップ後

実行すると、コンソール画面に次の結果が表示されます

ライフの表示を更新

応用

引数を使いたい場合

引数は、4つまでです

例えば、string型とint型の引数を取りたい場合

UnityEvent<string, int>

と宣言します
呼び出す時は、

Invoke("スペース", 5);

メソッド呼び出しの時と同じですね
メソッド本体は、

ShowLife(string msg, int data)

でシグネチャーを記述します。これもメソッドの書き方そのままですね

コード

using UnityEngine;
using UnityEngine.Events;

public class Event : MonoBehaviour
{
    public UnityEvent<string, int> unityEvent;

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            unityEvent.Invoke("スペース", 5);
        }
    }
}

旧バージョンのUnityでは、UnityEventクラスが抽象クラス(Abstractクラス)になっているので、直接インスタンスを作成できません。同じようなコードにするためには、一旦、継承する必要がありました。

using System;
using UnityEngine;
using UnityEngine.Events;

public class Event : MonoBehaviour
{

    public MyUnityEvent unityEvent = new MyUnityEvent();

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space))
        {
            unityEvent.Invoke("スペース", 5);
        }
    }
}

[Serializable]
public class MyUnityEvent : UnityEvent<string, int>
{
}
using UnityEngine;

public class UIController : MonoBehaviour
{
    public Event eventScript;

    void Awake()
    {
        eventScript.unityEvent.AddListener(ShowLife);
    }
    public void ShowLife(string msg, int data)
    {
        Debug.Log($"{msg} と {data}を受け取りました");
    }
}

実行結果

スペース と 5を受け取りました

インスペクターで登録する場合

引数のないメソッドと選択場所が違うので注意してください

選択のウィンドウ

prefabから不定期に生成されるエネミーにアタッチされているスクリプトのメソッドをイベントハンドラに追加したい場合

引数なしでのサンプルです

Instantiateした時に、イベントに追加しておけば、このコードのように敵との当たり判定で、全敵のスピードを低下させることもできます

onDisableで無効になった時(敵が消滅した時)にイベントから外すことも忘れずに

using UnityEngine;

public class Enemy : MonoBehaviour
{
    Event eventScript;

    int speed;

    // このスクリプトが有効(ゲームオブジェクトが有効になった)時
    private void OnEnable()
    {
        eventScript = GameObject.Find("GameObject").GetComponent<Event>();
        eventScript.unityEvent.AddListener(DecreaseSpeed);
    }

    // このスクリプトが無効(ゲームオブジェクトが無効になった)時
    private void OnDisable()
    {
        eventScript.unityEvent.RemoveListener(DecreaseSpeed);
    }

    // スピードを減少させる
    public void DecreaseSpeed()
    {
        speed--;
    }
}

さらに細かな処理をしたい場合

例えば、次のような処理です

  • 最初の一回だけ敵の戦車が弾を発射したときに頭上にビックリマークを出す
  • いずれかの戦車が弾を発射したらカメラを揺らす
  • 同じフレームで複数の戦車が発射した場合はSEは1回のみ再生する

イベントでもできますが、LINQのように記述できるUniRXを使う方法もあります
ただし、学習障壁は少し高いです。まずは、覗いてみてください

Unity

Posted by hidepon