Hero background image
オブザーバー・パターンでモジュール化された保守性の高いコードを作成する

Unityプロジェクトに一般的なゲームプログラミングデザインパターンを実装することで、クリーンで整理された読みやすいコードベースを効率的に構築し、維持することができます。デザインパターンは、リファクタリングやテストに費やす時間を削減するだけでなく、オンボーディングや開発プロセスをスピードアップし、ゲーム、開発チーム、そしてビジネスを成長させるための強固な基盤に貢献する。

デザインパターンは、コードにコピー&ペーストできる完成されたソリューションではなく、正しく使えばより大規模でスケーラブルなアプリケーションを構築するのに役立つ追加ツールだと考えてほしい。

このページでは、observer パターンについて説明します。observer パターンが、相互に作用するオブジェクト間の疎結合の原則をサポートするのに役立つことを説明します。

ここでの内容は無料電子書籍に基づいています、 ゲームプログラミングパターンでコードをレベルアップよく知られているデザインパターンを説明し、Unityプロジェクトでそれらを使用するための実践的な例を共有しています。

Unityゲームプログラミングデザインパターンシリーズの他の記事は、Unityベストプラクティスハブでご覧いただけます:

被験者オブザーバーのアナロジー
オブザーバー・パターン

ランタイムでは、ゲーム内でさまざまなことが起こりうる。プレイヤーが敵を倒すとどうなりますか?あるいは、パワーアップやレベルアップをしたとき?あるオブジェクトを直接参照することなく、他のオブジェクトに通知できる仕組みが必要になることはよくある。残念なことに、コードベースが増えるにつれて、不必要な依存関係が増え、柔軟性がなくなったり、コードのメンテナンスに余計なオーバーヘッドがかかったりする。

オブザーバー・パターンは、この問題に対する一般的な解決策である。これは、オブジェクトが通信することを可能にするが、「1対多」の依存関係を使用して疎結合を維持する。あるオブジェクトが状態を変更すると、従属するすべてのオブジェクトに自動的に通知される。

これをイメージしやすくするための例えは、さまざまなリスナーに向けて放送するラジオ塔だ。誰がチューニングしているのか知る必要はなく、ただ放送が適切な周波数で適切な時間に生中継されていることを知るだけでいい。

放送される対象は主語と呼ばれる。リッスンしている他のオブジェクトはオブザーバー、あるいはサブスクライバーと呼ばれる(このページでは終始オブザーバーという名称を使っている)。

このパターンの利点は、被写体を観察者から切り離すことができることである。観察者は被観察者に依存しているが、観察者自身は互いのことを知らない。

オブザーバーは、被験者の状態が変化するたびに通知され、それに応じて自分自身をアップデートすることができる。こうすることで、システムの他の部分に影響を与えることなく、コードの修正や拡張が容易になる。

もうひとつの利点は、オブザーバー・パターンが再利用可能なコードの開発を促進することだ。オブザーバーは、修正することなく異なるコンテキストで再利用することができるからだ。最後に、オブジェクト間の依存関係が明確に定義されるため、コードの可読性が向上することが多い。

オブザーバー
イベントを理解する

独自の主体-観察者クラスを設計することもできるが、C#はすでにイベントを使用してこのパターンを実装しているため、通常は不要である。オブザーバー・パターンはC#言語に組み込まれているほど普及しているが、それには理由がある:よりモジュール化された、再利用可能で保守性の高いコードを作成するのに役立つ。

イベントとは何か?これは、何かが起こったことを示す通知で、いくつかの手順を伴う:

パブリッシャー(サブジェクトとも呼ばれる)は、デリゲートが特定の関数シグネチャを確立することに基づいてイベントを作成する。イベントとは、被写体が実行時に行うアクションのことである(例えば、ダメージを受ける、ボタンをクリックする、など)。パブリッシャーは依存者(オブザーバー)のリストを保持し、このイベントで表される 状態が変化した時に、依存者に通知を送ります。

そして、オブザーバーはそれぞれイベント・ハンドラーと呼ばれるメソッドを作りデリゲートのシグネチャーと一致させなければならない。オブザーバーはパブリッシャーからの通知を受け取り、それに応じて自身を更新するオブジェクトである。

各オブザーバーのイベントハンドラはパブリッシャーのイベントを購読します。オブザーバーは何人でも加入できる。そのすべてが、イベントがトリガーされるのを待つ。

パブリッシャーが実行時にイベントの発生を通知することをイベントの発生と呼びます。これにより、オブザーバーのイベントハンドラが呼び出され、それに応じて内部ロジックが実行される。

こうすることで、被写体からのひとつのイベントに対して、多くのコンポーネントを反応させることができる。被験者がボタンをクリックしたことを示すと、観察者はアニメーションやサウンドを再生したり、カットシーンをトリガーしたり、ファイルを保存したりすることができる。オブジェクト間のメッセージ送信にオブザーバー・パターンがよく使われるのはそのためだ。

代表対イベント

デリゲートは、メソッドのシグネチャを定義する型である。これにより、メソッドを他のメソッドの引数として渡すことができる。値の代わりにメソッドへの参照を保持する変数のようなものだと考えてほしい。

一方、イベントは基本的に特別なタイプのデリゲートであり、クラス同士が疎結合で通信できるようにするものだ。デリゲートとイベントの違いに関する一般的な情報は、C#におけるデリゲートとイベントの違いを参照してください。

コードで書かれた単純な主題

以下のコードで、基本的な件名/発行者をどのように定義するか見てみましょう。

下のコード例のSubjectクラスでは、GameObjectに簡単にアタッチできるようにMonoBehaviourを継承していますが、これは必須ではありません。

独自のデリゲートを定義するのは自由ですが、System.Actionを使うこともできます。このコード例では、イベントと一緒にパラメータを送る必要はないが、もし必要であれば、Action<T>デリゲートを使い、角括弧の中にList<T>としてパラメータを渡すだけでよい(最大16個のパラメータ)。

コード・スニペットでは、ThingHappenedが実際のイベントで、サブジェクトがDoThingメソッドで呼び出す。

."演算子はヌル条件演算子であり、ヌルでない場合にのみイベントが起動されることを意味する。Invokeメソッドは、イベントを発生させるために使われます。これは、そのイベントにサブスクライブされているイベントハンドラを実行することを意味します。この場合、DoThingメソッドはNULLでなければThingHappenedイベントを発生させ、そのイベントにサブスクライブしているイベントハンドラを実行します。

オブザーバーやその他のデザイン・パターンを実際に動作させたサンプル・プロジェクトをダウンロードできます。このコード例はこちらでご覧いただけます。

コードによるシンプルなオブザーバー

イベントをリッスンするには、以下のコード例(Githubプロジェクトでも入手可能)のように、Observerクラスのサンプルをビルドすることができる。

このスクリプトをコンポーネントとしてGameObjectに取り付け、InspectorでsubjectToObserverを参照し、ThingHappenedイベントをリッスンする。

OnThingHappenedメソッドには、オブザーバーがイベントに応答して実行する任意のロジックを含めることができます。多くの場合、開発者はイベントハンドラを示すために接頭辞 "On "を追加します(スタイルガイドの命名規則を使用してください)。

AwakeまたはStartでは、+=演算子でイベントをサブスクライブできる。これは、オブザーバーのOnThingHappenedメソッドとサブジェクトのThingHappenedメソッドを組み合わせたものだ。

もし、そのオブジェクトのDoThingメソッドが実行されれば、イベントが発生する。そして、オブザーバーのOnThingHappenedイベント・ハンドラが自動的に起動し、デバッグ・ステートメントを表示する。

注:ThingHappenedにサブスクライブしたまま、実行時にオブザーバーを削除したり取り除くと、そのイベントを呼び出すとエラーになる可能性があります。したがって、オブジェクトのライフサイクルの適切なタイミングで、MonoBehaviourのOnDestroyメソッドで-=演算子を使ってイベントの登録を解除することが重要です。

オブザーバー・サンプル・プロジェクト
オブザーバー・パターンの使用例

サンプルプロジェクトをダウンロードして、11 Observerというフォルダに行くと、シンプルなボタン(ExampleSubject)とスピーカー(AudioObserver)、アニメーション(AnimObserver)、パーティクル効果(ParticleSystemObserver)を示すサンプルがあります。

ボタンをクリックすると、ExampleSubjectはThingHappenedイベントを呼び出します。AudioObserver、AnimObserver、ParticleSystemObserverは、それに応じてイベント処理メソッドを呼び出します。

オブザーバーは同じGameObject上に存在することも、異なるGameObject上に存在することもできる。AnimObserverはExampleSubjectのボタンアニメーションを生成し、AudioObserverとParticleSystemObserverは別のGameObjectを占有することに注意してください。

ButtonSubjectは、ユーザーがマウスボタンでClickedイベントを呼び出すことを可能にします。AudioObserverコンポーネントとParticleSystemObserverコンポーネントを持つ他のGameObjectは、それぞれイベントに反応することができます。

どの対象が主語で、どの対象が観察者であるかは、用法によって異なるだけである。出来事を提起するものは主体として働き、出来事に反応するものは観察者である。同じGameObject上の異なるコンポーネントは、サブジェクトにもオブザーバーにもなり得る。同じ部品であっても、ある文脈では主体であり、別の文脈では観察者になりうる。

例えば、この例のAnimObserverは、クリックされるとボタンにちょっとした動きを追加します。これは、ButtonSubject GameObjectの一部であるにもかかわらず、オブザーバーとして動作する。

ユニティのイベントとアクション
UnityEvents と UnityActions

また、Unityには Unityイベントを使用する UnityActionデリゲートを使用します。Inspector(オブザーバー・パターンのグラフィカル・インターフェースを提供する)で設定することができ、開発者はイベントが発生したときに呼び出されるメソッドを指定することができます。

UnityのUIシステム(UI ButtonのOnClickイベントの作成など)を使ったことがある人なら、すでに経験があると思います。

上の画像では、ボタンのOnClickイベントが、2つのAudioObserversのOnThingHappenedメソッドを呼び出し、レスポンスをトリガーしています。このように、コードなしで被写体のイベントを設定することができる。

UnityEventsは、デザイナーやノンプログラマーがゲームプレイイベントを作成できるようにしたい場合に便利です。しかし、System名前空間からの同等のイベントやアクションよりも遅くなる可能性があることに注意してください。また、UnityEventが引数を持たないメソッドに限定されるのに対し、UnityActionsは引数を取るメソッドの呼び出しに使用できるという利点もあります。

UnityEventsとUnityActionsを検討する際には、パフォーマンスと使用量を比較検討しましょう。UnityEventsの方がシンプルで使いやすいですが、呼び出せるメソッドの種類が限られています。また、インスペクタのすべてのイベントを公開することで、よりエラーが起こりやすくなるという意見もあるかもしれない。

例については、Unity LearnのCreate a Simple Messaging System with Eventsモジュールを参照してください。

長所と短所

イベントを実装することで余分な作業が増えるが、利点もある:

オブザーバー・パターンは、オブジェクトを切り離すのに役立つ: イベントパブリッシャーはイベント購読者自身について何も知る必要はない。あるクラスと別のクラスの間に直接的な依存関係を作る代わりに、主語と観察者はある程度の分離を保ちながらコミュニケーションをとる(ルース・カップリング)。

作る必要はない: C#には確立されたイベントシステムがあり、独自のデリゲートを定義する代わりにSystem.Actionデリゲートを使用することができます。あるいは、UnityにはUnityEventsと UnityActionsもあります。

各オブザーバは、独自のイベント処理ロジックを実装しています: このようにして、各観測オブジェクトは、応答するために必要なロジックを維持する。これにより、デバッグやユニットテストが容易になる。

ユーザーインターフェースに適している: コアとなるゲームプレイのコードは、UIのロジックとは別に生きることができる。UIエレメントは、特定のゲームイベントや条件をリッスンし、適切に応答します。MVPパターンやMVCパターンは、この目的のためにオブザーバー・パターンを使用する。

しかし、このような注意点も知っておく必要がある:

さらに複雑さが増す: 他のパターンと同様、イベントドリブンアーキテクチャを作るには、最初に多くの設定が必要になる。また、被験者やオブザーバーの削除にも注意が必要だ。OnDestroyでオブザーバーの登録を解除し、オブザーバーが不要になったときにメモリ参照が適切に解放されるようにしてください。

オブザーバーは、イベントを定義するクラスへの参照を必要とする: オブザーバーは、イベントをパブリッシュしているクラスへの依存関係を持ちます。すべてのイベントを処理する静的なEventManager(次のセクションを参照)を使用すると、オブジェクトとオブジェクトを分離することができます。

パフォーマンスが問題になることもある: イベント・ドリブン・アーキテクチャは、余分なオーバーヘッドを追加する。大きなシーンや多くのGameObjectは、パフォーマンスの妨げになります。

パターンの改善

ここではオブザーバー・パターンの基本的なものだけを紹介したが、これを拡張してゲーム・アプリケーションのあらゆるニーズに対応できるようにすることもできる。

オブザーバー・パターンを設定する際には、以下の提案を考慮すること:

ObservableCollectionクラスを使う: C#は動的な オブザーバブルコレクションを提供しています。アイテムが追加されたり、削除されたり、リストが更新されたりすると、オブザーバーに通知することができます。

一意のインスタンスIDを引数として渡す: 階層内の各GameObjectは一意のインスタンスIDを持っている。複数のオブザーバーに適用できるイベントをトリガーする場合は、イベントに一意のIDを渡します(Action<int>型を使用。そして、GameObjectがユニークIDに一致した場合のみ、イベントハンドラのロジックを実行する。

静的な EventManager を作成します: イベントはゲームプレイの大部分を駆動するため、多くのUnityアプリケーションは静的またはシングルトンのEventManagerを使用します。こうすることで、オブザーバーはゲームイベントの中心的な情報源を参照することができ、セットアップが容易になる。

FPS Microgameは、カスタムGameEventを実装し、リスナーを追加または削除するための静的ヘルパーメソッドを含む静的EventManagerの優れた実装を持っています。

Unity Open ProjectではScriptableObjectがUnityEventをリレーするゲームアーキテクチャも紹介されている。イベントを使ってオーディオを再生したり、新しいシーンをロードしたりします。

イベント・キューを作成する: シーンにたくさんのオブジェクトがある場合、イベントを一度に上げたくないかもしれません。Observerパターンとコマンド・パターンを組み合わせることで、イベントをイベント・キューにカプセル化することができる。その後、コマンドバッファを使ってイベントを1つずつ再生したり、必要に応じて選択的に無視したりすることができる(一度に音を出せるオブジェクトの最大数が決まっている場合など)。

オブザーバー・パターンは、電子書籍で取り上げられているModel View Presenter(MVP)アーキテクチャ・パターンに大きく関わっている。 ゲーム・プログラミング・パターンでコードをレベルアップする.

電子ブックカバー
Unityでプログラミングするための高度なリソース

Unityアプリケーションでデザインパターンを使用する方法とSOLIDの原則については、無料の電子書籍に多くのヒントがあります。 ゲームプログラミングパターンでコードをレベルアップ.

Unityの上級テクニカル電子書籍や記事はすべて、ベストプラクティスハブでご覧いただけます。電子書籍は、ドキュメンテーションの上級ベストプラクティスのページでもご覧いただけます。

このコンテンツにご満足いただけましたか?