はじめに
様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例
いまいちピンとこないコード
で説明されてることが多く、
結局これっていつ使うの?
という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。
そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。
デザインパターンを学ぶ理由
デザインパターンを学ぶ理由としては
- 車輪の再発明の防止
- 長文で読みにくいコード(可読性の低いコード)を減らす
- コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
- モジュールとして使いまわせるように、コードの再利用性を高める
といった効果を期待できます。
対象読者
Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。
- MonoBehaviour 継承クラスでコードを書いたことがある
- C# のピュアクラスを用いた自作クラスを作ったことがある
- クラスの継承という概念は知っている
そのため、脱・初心者
中級者へのステップアップ
として デザインパターンを学ぶ
のが良いと思います。
デザパタ記事リンク
生成系
構造系
様態・ふるまい系
- Chain of Responsibility パターン
- Command パターン
- Interpreter パターン
- Iterator パターン
- Mediator パターン(本記事)
- Memento パターン
- Observer パターン
- State パターン
- Strategy パターン
- TemplateMethod パターン
- Visitor パターン
Mediator パターンについて
Mediator とは 調停者
という意味で、役割を抽象的に説明すると 多入力の信号を適切な出力先に伝える
というものです。
ポイントは Mediator は1人ですが、それとつながっているObject は多数ある状況です。
もう少し実例を交えて説明するとオンラインアクション(スポーツ)ゲームなどがわかりやすいでしょう。
ロケットリーグやswitch スポーツのサッカーのような1つのボールを2チームに分かれたユーザー達が応用なゲームの場合、最後に誰がボールに触れたか?
が非常に重要になります。
この 誰が最後にボールに触れた?
を調停者がいる場合といない場合を考えてみましょう。
もし調停者、この場合は サッカーの審判
に相当する役目がいる場合は、適切に状況を判断して誰が最後にボールに触れたかをジャッジます(実際のサッカーはボールがラインを超えた際は主審やラインジャッジが判定しますが、今回はまとめて 審判
と理解してください)。
このように第3者がいる場合は基本的には 審判の言うこと
が絶対であって、それぞれのチームメンバーは主張こそしてよいですが、審判の決めたことには逆らってはいけません。
次に、もし審判がいない場合を考えてみましょう。
もしゴールが決まりそうなタイミングで 最後に触ったゴールが味方の場合はオウンゴールにならないようにボールが弾かれる
ような仕様が入ってしまうと、シュートを打った側は 自分たちが最後にシュートした
と主張し、防衛側は点数が入らないように 最後にちょっとカスったけど触れたのでシュートは弾かれるべきだ
と主張するでしょう。このままではお互いが 自分のチームに有利になる主張
をずっと行うだけで議論が平行線になって、ゲームになりません。
このような 審判役
を担当するのが Mediator
です。
Observer パターンとの違い
Mediator は多数からの入力を受けて、その結果を本人やその他の入力側に返すような動きをすることが多いです。
これだけ聞くと Observer パターン(執筆中) を知っている人からすると、 入力からの通知に反応して処理を行なう
のはObserverパターンそのものではないのか?という疑問が浮かぶと思います。(Qiitaを見ていたら同じ感想を持っている方がいらっしゃいました)
Mediator とObserver パターンの大きな違いは 通知を行う・検知する仕組み
は Observer パターン
で、受け取った入力を元に 交通整理を行う役目
が Mediator
パターンです。
つまり先ほどの図で言うと、黒矢印、橙矢印を実現するのが Observerパターン
で吹き出しにあるボールの向かう方向を決める判断を行ったのが Mediator
パターンです。
さらに言えば先ほどの例で言えば端末の通信環境のラグにより、サーバー(Mediator) が受け取った入力は時系列と逆順で受け取っていますが、受け取った入力を整理して 正しい時系列に整えた結果
を各ユーザーに伝えています。
もしMediator がObserver パターンだけで構築されていたら 入力を受け取ったタイミングで判断
するため、今回でいえば 時系列が前
だけど サーバーに届いたのがちょっと遅れた
ために、受け取った入力が遅い方で結論が上書きされるため、今回でいえば ゴールシュート
の結果が優先されます。
今回はMediator が存在したので、遅れてやってきた②の入力は 時系列的に過去の動き
で、 すでに受取済みの入力により上書きされるため棄却
という判定が行われます。ここが一番の違いになります。
ゲームにおける Mediator
MVC/MVVM、UI との関連性
ゲーム開発において、Mediator はUI部分の制御で非常に大事な役目を負っています。
まず設計レベルの話として、UI(View) を管理するクラスはUIの制御(パラメータのSet・Get)に注力すべきで、その先の部分の処理に関してはUI(View) は手出しせず、別のクラスが責任を負うべきという考え方があります。
MVC(Model-View-Controller) モデルでいうところの Modelが先ほどの 別のクラス=Mediator
に該当し、UI管理クラスがView, Controller に相当します(Unityの場合UIイベントを受け取るクラスとUIを制御するクラスは同じMonoBehaviour 派生クラスで行うため)。
また、MVVM(Model-View-ViewModel) では UI管理クラス
が View
, 別のクラス=Mediator
が Model
, そして両者をつなぐためのクラス(パラメータ) が ViewModel
に相当します。
このMediator ですが、もし何も処理をしないとボタンを連打されるたびに処理が実行されます。
単純な処理ならそれでもよいのですが、ボタンを押すことによって UnityのSceneを遷移する
, Addressables リソースのDownload を実行する
, 自社サーバー等外部サーバーと通信する
など重い処理を実行したい場合、 連打されることによって多重実行される
と 瞬間的な負荷が上がったり
、 適切に処理が実行されなかったり
とさまざまな悪影響を及ぼします。
そこでMediator が 一度処理を開始したら終わるまでは次の処理依頼をブロックする
または 処理実行中は、入力元になったボタンを無効化する
ような処理を加えないとボタンの連打対応による悪影響が解消できません。
そして、処理が終わったらブロックや無効化の解除を行うようにシステムを組むことで、安全なUI操作を提供することができます。
Photon等のゲームサーバーにおけるMediator
先ほどの解説事例がまさに ゲームサーバーによるMediatorパターン
の一つです。
そしてUnityのマルチプレイ用通信エンジンであるPhoton Fusion ではこの ゲームサーバーによるMediatorパターン
がて開発者側に提供されるようになりました。
(Photon ブログ「Photon Fusion – 数億人規模の未来のプレイヤーのパフォーマンス基準へ」より引用 )
(いぜんもWindowsによるPhotonServerはありましたが、HostモードではPUNに比べてよりMediator であることを意識したコードを書くことになります)
※PhotonFusionを用いた開発Tips は弊社Graffityの @AzuQiita さんが解説しているのでぜひ!
まとめ
UI の安全な制御やマルチプレイの調整役など、様々な場面でMediator は活躍します。
用語自体はなかなか聞きなれない用語ですが、特にマルチプレイのゲームサーバー構築・制御に携わるような場合、必須の知識になります。
また、Mediator とそれ以外の繋ぐための方法としてObserver パターンが頻繁に利用されるので、そちらも合わせて勉強することをおすすめします!