Action
Action とは、関数を参照型として扱い、沢山保持して一斉に実行できる参照型です。
Action を使って依存関係を逆転すれば、再利用しやすいクラスが作れます。
例として、OnCollisionEnter が呼ばれたら、他のクラスから受け取った関数を実行するクラスを紹介します。
using UnityEngine;
using System;
public class OnCollisionEnterEvent : MonoBehaviour
{
[SerializeField] public event Action<Collision> Action;
private void OnCollisionEnter(Collision collision)
{
// action が持っている各関数の引数に collision を代入して、各関数を実行
Action.Invoke(collision.gameObject.name);
}
}
OnCollisionEnterEvent の Action に関数を渡すときは、下記のように記述します。
using UnityEngine;
public class DebugLog: MonoBehaviour
{
[SerializeField] private OnCollisionEnterEvent onCollisionEnter;
private void Start()
{
// action に View 関数を渡す(引数が一致している必要がある)
onCollisionEnter.Action += View;
}
private void View(Collision collision)
{
Debug.Log(collision.gameObject.name);
}
}
上2つのクラスをオブジェクトに追加して再生すると、衝突したコライダーの名前がコンソールに表示されます。
衝突したコライダーに対してどんな処理を実装するとしても、OnCollisionEnterEvent.Action に関数を追加するだけで済みます。
補足
- action から View 関数を除外するときは「onCollisionEnter.action -= View;」のように書きます
- action に関数が渡されていない状態で Invoke を呼ぶとエラーになります。基本的にNullチェックしましょう。Null条件演算子を使えば簡潔にNullチェックできます。例:action?.Invoke();
- action の宣言の前についている event を取ると、他のクラスからも Invoke を呼べるようになります。誰が呼んだか分からなくなるので注意して下さい
補足に書いた内容を例に反映すると下記のようになります
using UnityEngine;
using System;
public class OnCollisionEnterEvent : MonoBehaviour
{
[SerializeField] public event Action<Collision> Action;
private void OnCollisionEnter(Collision collision)
{
Action?.Invoke(collision.gameObject.name);
}
}
using UnityEngine;
public class DebugLog: MonoBehaviour
{
[SerializeField] private OnCollisionEnterEvent onCollisionEnter;
private void Start()
{
onCollisionEnter.Action += View;
}
private void OnDestroy()
{
onCollisionEnter.Action -= View;
}
private void View(Collision collision)
{
Debug.Log(collision.gameObject.name);
}
}
UnityEvent
UnityEventとは Action と同様、関数の参照を沢山保持して一斉に実行できる参照型です。
Action より僅かに処理が遅いですが、インスペクタ上から関数を渡せます。ゆえに処理を追いづらくなり、予期せぬ不具合や可読性の低下を招く恐れがあります。UnityEvent が最適な状況のみ利用しましょう(例:非エンジニアにロジックを組んでもらう)。
OnCollisionEnterEvent を UnityEvent で実装すると、下記のようになります。
using UnityEngine;
using UnityEngine.Events;
public class OnCollisionEnterEvent : MonoBehaviour
{
[SerializeField] private UnityEvent<Collision> Event;
private void OnCollisionEnter(Collision collision)
{
// Event が受け取った各関数の引数に collisionを代入して、各関数を実行
Event.Invoke(collision);
}
}
上記のコンポーネントをゲームオブジェクトに追加すると、Event(Collision) と書かれた枠が出現します。
コンポーネント上で UnityEvent に関数を渡すには、
- 枠の右下の + ボタンを押す
- 枠の左下の None(Object) にゲームオブジェクトを割り当てる
- 枠の右上の No Function と書かれたプルダウンメニューから関数を選択
と操作します。これで、OnCollisionEnter が呼ばれたタイミングで選択した関数が実行されます。
補足
- UnityEvent に追加する関数の引数が1つだけの場合に限り、インスペクタ上で自由な値を代入できます(関数の引数が一致している必要もありません)
- UnityEvent に追加する関数の引数が2つ以上の場合は、スクリプトから関数を追加するか、引数が全て一致する関数を指定する必要があります
- プルダウンメニューに関数が出てこない場合は、関数が public になっていないか、戻り値が void になっていないかを確認して下さい
using UnityEngine;
public class DebugLog : MonoBehaviour
{
[SerializeField] private OnCollisionEnterEvent onCollisionEnter;
private void Awake()
{
// Event に View 関数を渡す(引数が一致している必要がある)
onCollisionEnter.Event.AddListener(View);
}
private void View(Collision collision)
{
Debug.Log(collision.gameObject.name);
}
}
ラムダ式
ラムダ式とは、使い捨ての関数を簡潔に作れる便利な機能です。
Action 用に関数を沢山作るとコードがかさばります。代わりに、ラムダ式で作った使い捨ての関数を使えばコードが簡潔になります。
ラムダ式を使うと、1つ目のコードを2つ目のコードのように、簡潔に書き換えられます。
private void Awake()
{
onCollisionEnter.Event.AddListener(View);
}
private void View(Collision collision)
{
Debug.Log(collision.gameObject.name);
}
private void Awake()
{
onCollisionEnter.Event.AddListener((collision) =>
{
Debug.Log(collision.gameObject.name);
});
}
コードが一行なら、中括弧とセミコロンを省略できます。
onCollisionEnter.Event.AddListener((collision) => Debug.Log(collision.gameObject.name));
引数が一つなら、括弧も省略できます。
onCollisionEnter.Event.AddListener(collision => Debug.Log(collision.gameObject.name));
補足
- RemoveListener 等はラムダ式に対応していません
- ラムダ式で参照型を使う際、Action が呼ばれた時点で参照型に存在する値が使われるので注意して下さい
List<int> numbers = new List<int>();
numbers.Add(1);
Action action = () => Debug.Log(numbers[0]);
numbers.Insert(0, 100);
// コンソールに「100」と表示される
action.Invoke();
- 関数名には処理の内容を説明する役割があります。ラムダ式の乱用で説明を省略し過ぎると、逆にコードが読み辛くなるので注意しましょう