Unityゲームを開発をする上でEventのPub/Subシステムを作るのは必須で、そのベースをどのように作るかというのは、いつも頭を悩ませたりするのではないでしょうか。
よくあるのは、以下の様なものかと
- ActionなどのCallbackでイベントのデータを受け取る
- UnityEventなどを使用する
と、メソッドベースで、HogeEvent(hogeEventData)
みたいにメソッド+イベントのデータで実装することが多いように思えます。
そこで、一種の方法としてまずは、UniRx.MessageBrokerを紹介します。
注意事項
以降の説明は、UniRxとZenjectについて全く知らないという状態だとわかりづらいかと思います。
ですが、UniRx/ZenjectはUnity開発において勉強しておいて損はありません!
(利益しかないかもしれない)
UniRx.MessageBrokerとは
UniRxの紹介は省きます!(山ほど記事がある)
MessageBrokerとはUniRxに含まれているメッセージのPublish/Receiveが可能なクラスです。
https://github.com/neuecc/UniRx#messagebroker-asyncmessagebroker
http://neue.cc/2016/08/03_536.html
上記で説明したメソッドベースではなく、送受信する型のみ
でグルーピングします。
使い方は以下のような感じです。
例として、何らかのアイテムをゲーム内通貨で買った
というイベントを定義してみます。
(以降もこれを題材に説明します)
買ったアイテムのIDと数のペアで以下のようなクラスを定義してみます。
public class PurchasedItem
{
public int ItemId;
public int Count;
}
イベントを送信する側は以下のようにします。
MessageBroker.Default.Publish(new PurchasedItem(ItemId, ItemCount));
受信する側はこうです。
MessageBroker.Default.Receive<PurchasedItem>()
Subscribe(purchasedItem =>
{
// 購入後の処理。ユーザーデータへの追加など
userData.Inventory.Add(new Item(purchasedItem.ItemID, purchasedItem.ItemCount));
})
.AddTo(this); // MonoBehaviourなら死活管理もこれでOK
メッセージのスコープを少し狭める
MessageBroker.Default
を使用すると、グローバルなメッセージを扱うことになり、もう少し範囲を狭めたくもなります。
好みの問題かもしれませんが。
ということで、MessageBrokerを継承し、特定のメッセージのみを扱うクラス
を作ります。
例に沿うと、アイテムの売買を行うのはショップなので、ShopMessageBroker
としてみます。
さらにイベントデータを扱うクラスも内部に定義してしまいます。
これこそ好き好きでメッセージクラスをどう定義すると効率が良いか他にも方法は色々あるかと思います。
この方法はコードを書くときにちょっと長めになってしまいますし、Publishメソッドに渡すデータに制限をかけられていないので参考程度に留めておいてください。
public class ShopMessageBroker : MessageBroker
{
public class PurchasedItem
{
public int ItemId;
public int Count;
}
}
イベントを投げる際には以下のようになります。(受信は省略)
shopMessageBrokerは、MessageBroker.Default
のようなstaticクラスではないため
使用する側はのインスタンスは何らかの方法で解決するものとします(前フリ)
shopMessageBroker.Publish((new ShopMessageBroker.PurchasedItem(ItemId, ItemCount));
これでメッセージのスコープを狭めることができました。
異論は色々とあると思いますが参考程度にとどめておいてください。
よりよい設計はあると思います!
ようやくDIコンテナZenjectとの相性のお話
Zenjectについての説明は省略します!
GitHubは以下になります。大雑把に説明するとUnityで使用できるDIコンテナです。
https://github.com/modesttree/Zenject
かなり説明を端折りますが、Zenjectで依存性を解決する際はInterfaceを介するのがベターかと思います。
(疎結合にしてテストをかけるようにするなど)
MessageBrokerは、IMessagePublisher/IMessageReceiver
というインターフェースに基づいて実態が作られています。
(正確には、IMessageBrokerというインターフェースもあり、上記ふたつを継承している)
何もしなくても、ZenjectでBindするための準備がすべて整っているんです。
DI(依存性の注入)する!
てことで、ZenjectのInstallerに以下のように書きます。
// IMessageBrokerではなく、IMessagePublisher/IMessageReceiverと明確にできることの権限を分ける
Container.Bind<IMessagePublisher>().To<ShopMessageBroker>().AsSingle();
Container.Bind<IMessageReceiver>().To<ShopMessageBroker>().AsSingle();
使う側は以下のようにして、インスタンスをZenjectに注入してもらいます。
[Inject]
private IMessageReceiver messageReceiver;
[Inject]
private IMessagePublsher messagePublisher;
使うときは今まで書いてきたサンプルとほぼ同じですが、以下のようになります。
messagePublisher.Publish(new ShopMessageBroker.PurchasedItem(ItemId, ItemCount));
...
messageReceiver.Receive<ShopMessageBroker.PurchasedItem>()
.Subscribe(purchasedItem =>
{
// 何らかの処理
})
.AddTo(this);
なぜ、ReceiverとPublisherを分けるのか
IMessageBrokerを直接使えばいいじゃないかとかという意見もあるかと思いますが、できることの権限を明確にわける
というのは意味があります。
ある程度の規模の開発になってくると、様々なメッセージがゲーム中に飛び交い、なんでこのクラスが直接イベント発行してんの?ということに後々気づき、全体を見渡すとカオスになっているということがよくあります。
そこで、使用する際は権限が絞られたインターフェースに限定し、実装時やコードレビューなどで少しでも気づきやすくするという意図があったりもします。
実装中などにIMessagePublisherを使おうとして、「あれ?こいつがイベント発行するのは違うのでは?」と
ちょっとでも気づきやすくなって、どの機能をどこに入れ込むかというのを考えるきっかけにもなりえるのではと。
イベントのPub/Subはよく使う手法であるがゆえに、多人数/大規模開発になってくれば来るほど、
どこでどのイベントを送受信するかというのが制御不能になりがちです。
完全に制御するのは難しいですが、Interfaceで権限を絞る
、依存性が注入されている箇所をわかりやすくする
(Zenjectを使うと[Inject]Attributeを付ける必要があるのですぐわかる)
というのはある程度の手助けになるのではないでしょうか。
なにより、ZenjectとUniRxを使えばイベントのPub/Subシステムをほぼ自前実装しなくて良いというのが楽で作者には感謝しかないです。投げ銭システムあれば確実に寄付してる。