スポンサーリンク
Unity C#・スクリプト実装

【Unity C#】Action・Event・delegateの違いと使い方完全ガイド|カスタムイベントで疎結合設計

Unity C#・スクリプト実装

「delegateとevent、なんとなく使い分けているけど、実はよく分かっていない」「Actionって書いておけば動くから、そのまま使っている」——そんな状態で開発を進めていませんか。

Unityでカスタムイベントを作ろうとすると、delegate・Action・event・UnityEventと似たような選択肢がいくつも出てきて、どれを選べばいいのか迷ってしまいますよね。なんとなく動くコードは書けても、それが本当に正しい選択なのか、不安なまま公開している方も多いのではないでしょうか。

この記事では、4つの機能の違いを整理したうえで、自分のケースではどれを使えばいいのかを判断する基準と、それぞれの具体的な実装手順、実装した後に気をつけたい2つの失敗について解説していきます。


delegate・Action・event・UnityEventの選び方

結論からお伝えすると、どれを使うか迷ったときは次の2つによってほぼ答えが決まります。

  • ①Unity Editorの画面上で、ドラッグ&ドロップして処理を繋ぎたいか
  • ②1フレームの間に何度も呼ばれるような、頻度の高い処理か

この2つの答えの組み合わせで、選ぶべき機能はだいたい次のように決まります。

①Inspectorで繋ぎたい②高頻度な処理おすすめ
はいUnityEvent
いいえはいC#のevent
いいえいいえ(独自の引数構成にしたい)delegate
いいえいいえ(シンプルに済ませたい)Action・Func

例えば「ボタンを押したらUIの見た目を変える」くらいの処理なら、Inspector上で完結させられるUnityEventが向いています。デザイナーさんが自分でメソッドを差し替えたい場面でも、コードを触らずに済むので扱いやすいですよね。

一方で「敵の被弾判定のたびに複数の処理を呼びたい」といった、コード内で完結する高頻度な連携には、C#のeventの方が向いています。呼び出しのたびに発生するコストが小さく、外部から意図せず呼び出されてしまう心配もありません。

「なぜそうなるのか」がまだピンとこなくても大丈夫です。この後の見出しで、実際に手を動かしながら理由も含めて確認していきましょう。




実装手順|UnityEventとeventどちらを書く?

前の見出しで自分がどちらのタイプか決まったら、あとは実際に手を動かすだけです。ここでは「Inspectorで繋ぎたい人」向けのUnityEventと、「コードで完結させたい人」向けのeventとActionを、それぞれ手順で紹介していきますね。

UnityEventをInspectorで設定する手順

UnityEventは、スクリプト側で少し準備するだけで、あとはUnity Editorの画面上で処理を繋げられるのが特徴です。手順は次のとおりです。

  • 発行者側のスクリプトに、UnityEvent型の変数をpublicまたは[SerializeField]で定義する
  • Unity Editorでそのコンポーネントを選択し、Inspector上のイベント欄にある「+」ボタンをクリックする
  • 処理を実行させたいオブジェクトをドラッグ&ドロップでアタッチする
  • プルダウンから実行したいメソッドを選択する
  • 特定のタイミングで`Invoke()`を呼び出すと、登録した処理がまとめて実行される

コードを一切触らずに処理を繋げられるので、UIのボタンイベントなど「ちょっとした動作を差し替えたい」場面ではとても重宝します。ただし、public化したUnityEventは外部から直接Invoke()されてしまう可能性がある点は覚えておいてください(この注意点は後の見出しで詳しく触れます)。

Inspector上でイベントを組む機会が多い方は、複雑な参照関係も見やすく整理できるInspector拡張ツールを併用すると、設定作業がぐっと楽になります。

Odin Inspector
✅ Unity Asset Storeでチェックする

eventとActionをコードだけで実装する手順

コード内だけで処理を繋げたい場合は、eventやActionを使います。基本の流れはこちらです。

  • 発行者側のクラスに、Action型やevent Action型の変数を定義する
  • 購読者側のクラスに、イベント発生時に実行したいメソッドを用意する
  • 購読者側で`+=`を使い、発行者のイベントにメソッドを登録する
  • 発行者側で、条件を満たしたタイミングで`Invoke()`(またはそのまま呼び出し)を実行する
  • オブジェクトが破棄されるタイミング(OnDisableなど)で`-=`を使い、登録を解除する

eventキーワードを付けておくと、購読者側からは`+=`と`-=`しかできなくなり、うっかり外部から発行してしまうミスを防げます。単純にメソッドを渡すだけで十分な場面では、eventを付けずにActionのまま使っても問題ありません。

最後の「購読を解除する」というステップ、つい忘れてしまいがちなのですが、実はここでよくあるトラブルが起きます。次の見出しで詳しく見ていきましょう。




実装後に注意すべき2つの失敗

手順どおりに実装できても、公開してから初めて気づくトラブルがいくつかあります。ここでは特に見落としやすい2つのポイントを紹介します。

購読解除を忘れるとどうなる?

「イベントを購読したメソッドは、放っておいてもいつか自動で消えるだろう」と思っていませんか。実はこれ、そのままにしておくとメモリリークの原因になります。

購読(`+=`)をすると、発行者側は購読者側の参照をずっと持ち続けます。オブジェクトを破棄したつもりでも、発行者側に参照が残ったままだと、ガベージコレクションの対象にならず、メモリ上に残り続けてしまうんです。

さらに厄介なのが、破棄されたはずのオブジェクトのメソッドが呼ばれてしまい、`NullReferenceException`(存在しないはずのものにアクセスしようとしたエラー)が発生するケースです。「シーンを切り替えたらエラーが出た」というときは、たいていこれが原因だったりします。

対策はシンプルで、購読した処理は必ず対になる場所で解除することです。

  • C#のevent・Actionの場合:`OnEnable`で`+=`したら、`OnDisable`で`-=`する
  • UnityEventの場合:`RemoveListener`や`RemoveAllListeners()`をオブジェクトの破棄前に呼ぶ

「登録したら、必ず解除もセットで書く」というクセをつけておくだけで、このトラブルはほぼ防げます。

参照を持ったままイベントを使うと疎結合にならない

もう一つ見落としやすいのが、「イベントを使ってさえいれば、それだけで設計が良くなる」という思い込みです。実はこれ、必ずしも正しくありません。

例えば、発行者側のスクリプトが`GetComponent`で購読者側を直接取得していて、そこに向けてイベントも購読させている場合、結局は発行者が購読者の存在を知ってしまっています。これでは、見た目はイベントを使っていても、中身は密結合のままなんです。

本当の意味で疎結合にするには、発行者側は「誰が購読しているか」を一切知らない状態にする必要があります。発行者はただイベントを発行するだけ、購読者側が自分の意思で登録しにいく、という関係性を保つことが大切です。

この考え方をさらに一歩進めたい場合は、依存性の注入という手法を使うと、参照の持ち方そのものを整理しやすくなります。

また、今回紹介した4つの機能だけでなく、シングルトンやサービスロケーターといった別の設計パターンと組み合わせて考えたい場合は、こちらの記事も参考にしてみてください。




よくある質問(FAQ)

Q
UnityEventとC#のeventはどちらが遅い?
A

一般的には、UnityEventの方が呼び出しにかかるコストは大きいとされています。これはUnityEventがリフレクション(実行時にメソッド情報を調べる仕組み)をベースにしているためです。

ただし、ボタンを押したときの処理のように、頻度が低い場面ではその差はほとんど気にならないレベルです。差が問題になるのは、毎フレーム何度も呼ばれるような処理にUnityEventを使ってしまった場合です。そういった高頻度な処理には、C#のeventやActionを選んでおくと安心です。

Q
publicにしたUnityEventは誰でもInvoke()できてしまう?
A

はい、できてしまいます。UnityEventをpublicや[SerializeField]で公開すると、そのUnityEventを参照できる場所であれば、どこからでも`Invoke()`を呼び出せる状態になります。

意図しないタイミングで外部から発行されてしまうと、想定外の処理が走ってバグの原因になりかねません。外部からの発行をきっちり防ぎたい場面では、UnityEventではなくC#のeventを使うと、`+=`と`-=`しか許可されなくなるため安全です。

Q
delegateとFuncはどう使い分ける?
A

まず戻り値の有無で考えてみてください。戻り値が必要ならFunc、不要ならActionを使うのが基本です。どちらもC#にあらかじめ用意されている型なので、宣言の手間がかからず手軽に使えます。

一方で、引数の数や種類が特殊だったり、コードを読む人にとって分かりやすい独自の名前を付けたかったりする場合は、delegateで自分の関数型を定義した方が扱いやすくなります。迷ったときは、まずAction・Funcで書いてみて、表現しきれないと感じたときにdelegateを検討する、という順番で問題ありません。

※当サイトはアフィリエイト広告を利用しています。リンクを経由して商品を購入された場合、当サイトに報酬が発生することがあります。

※本記事に記載しているAmazon商品情報(価格、在庫状況、割引、配送条件など)は、執筆時点のAmazon.co.jp上の情報に基づいています。
最新の価格・在庫・配送条件などの詳細は、Amazonの商品ページをご確認ください。

スポンサーリンク