🍣

Zenject.Signalsを使う

2020/10/15に公開

Zenject.Signalsとは

  • Zenjectの一機能
  • UniRxのMessageBrokerのようなPub/Subメッセージングモデル
  • UniRxとも親和性がある
  • 尚、本稿で扱うのはVersion 9.2.0とする

基本の使い方

PubからSubにメッセージを送る

  1. Installerで SignalBus のインストール・メッセージの宣言を行う
  2. Sub側はメッセージを受け取るメソッドをバインドする
  3. Pub側では SignalBus を持ち、任意のタイミングでSignalBus.Fire を発火
public class Publish : MonoBehaviour
{
    [Inject]
    private SignalBus signalBus = default;

    private void Start()
    {
        signalBus.Fire(new MessageSignal { Message = "Fire" });
    }
}

public class MessageSignal
{
    public string Message;
}

public class MessageSubscriber
{
    public void RecieveMessage(MessageSignal messageSignal)
    {
        Debug.Log(messageSignal.Message);
    }
}

public class Installer : MonoInstaller
{
    public override void InstallBindings()
    {
        SignalBusInstaller.Install(Container);
        Container.DeclareSignal<MessageSignal>();
        Container.Bind<MessageSubscriber>().AsSingle();
        Container.BindSignal<MessageSignal>()
            .ToMethod<MessageSubscriber>(x => x.RecieveMessage).FromResolve();
    }
}

メッセージをSignalBus経由で受け取る

  • メッセージを受け取るようにできたが、受け取るメソッドをインストーラに記述したくない場合もある
  • SignalBus.Subscribe / SignalBus.Unsubscribe を使用し、 メッセージを受け取るメソッドを登録/解除する
public class Publish : MonoBehaviour
{
    [Inject]
    private SignalBus signalBus = default;

    private void Start()
    {
        signalBus.Fire(new MessageSignal { Message = "Fire" });
    }
}

public class MessageSignal
{
    public string Message;
}

public class MessageSubscriber: MonoBehaviour
{
    [Inject]
    private SignalBus signalBus = default;

    private void Start()
    {
        signalBus.Subscribe<MessageSignal>(RecieveMessage);
    }

    private void OnDestroy()
    {
       signalBus.Unsubscribe<MessageSignal>(RecieveMessage);
    }

    public void RecieveMessage(MessageSignal messageSignal)
    {
        Debug.Log(messageSignal.Message);
    }
}

public class Installer : MonoInstaller
{
    public override void InstallBindings()
    {
        SignalBusInstaller.Install(Container);
        Container.DeclareSignal<MessageSignal>();
    }
}

メッセージの受け取りをUniRx経由にする

  • Zenject.SignalsでUnirxを使用できるように設定する必要がある
  1. ZenjectのAssembly DefinitionのAssembly Definition References にUniRxを追加し、 Apply
  2. Edit > Player Settings > Player > Scripting Define Symbolsに ZEN_SIGNALS_ADD_UNIRX を追加
  • SignalBus.GetStream 経由でSubscribeする
public class Publish : MonoBehaviour
{
    [Inject]
    private SignalBus signalBus = default;

    private void Start()
    {
        signalBus.Fire(new MessageSignal { Message = "Fire" });
    }
}

public class MessageSignal
{
    public string Message;
}

public class MessageSubscriber: MonoBehaviour
{
    [Inject]
    private SignalBus signalBus = default;

    private void Start()
    {
        signalBus.GetStream<MessageSignal>()
            .Subscribe(messageSignal => Debug.Log(messageSignal.Message))
            .AddTo(this);
    }
}

public class Installer : MonoInstaller
{
    public override void InstallBindings()
    {
        SignalBusInstaller.Install(Container);
        Container.DeclareSignal<MessageSignal>();
    }
}

メッセージの宣言について

Container.DeclareSignal<SignalType>()
    .WithId(Identifier)
    .(RequireSubscriber|OptionalSubscriber|OptionalSubscriberWithWarning)()
    .(RunAsync|RunSync)()
    .WithTickPriority(TickPriority)
    .(Copy|Move)Into(All|Direct)SubContainers();
  • SignalType - メッセージを表す型
  • Identifier - 識別子。Bindなどと同様に一意の値を指定して同様の型が宣言されていても場合分けすることが可能になる。e.g. SignalTypeがstring型など
  • RequireSubscriber/OptionalSubscriber/OptionalSubscriberWithWarning - メッセージの受取先が全く無い場合の振る舞いの設定
  • RequireSubscriber - 例外をスロー
  • OptionalSubscriber - とくになし
  • OptionalSubscriberWithWarning - 警告をコンソールに出力
  • デフォルトは Container.Settings.Signals.MissingHandlerDefaultResponse で設定できるのでProjectInstallerなどで設定すると良い。 尚、MissingHandlerDefaultResponse のデフォルトは公式ドキュメントを見る限りだと OptionalSubscriber となっているが現バージョンだと OptionalSubscriberWithWarning となっている模様
  • RunAsync/RunSync - メッセージの受け取りを同期的に行うか、非同期的に行うか。デフォルトはRunSync
  • RunSync - SignalBus.Fire されたときに直ちにすべてのSubscribe先のメソッドが呼び出される
  • RunAsync - 後述の TickPriority で指定されたタイミングで呼び出す
  • TickPriority - RunAsync 設定時のみ有効。メソッドが呼び出される優先度を指定
  • (Copy|Move)Into(All|Direct)SubContainers - 通常のBind同様に殆どの場合無視できる。SubContainerに対して一部Bindしたくないときなどに使用

BindSignalについて

Container.BindSignal<SignalType>()
    .WithId(Identifier)
    .ToMethod(Handler)
    .From(ConstructionMethod)
    .(Copy|Move)Into(All|Direct)SubContainers();
  • SignalType- 受け取るメッセージを表す型
  • Identifier - メッセージの宣言についてと同上
  • ConstructionMethod - インスタンスの取得元の定義。FromResolve From FromNew など
  • **(Copy|Move)Into(All|Direct)**SubContainers - メッセージの宣言についてと同上
  • Handler メッセージを受け取ったときに呼び出されるメソッドの指定

Abstract Signals

  • Version9.2.0 で追加された機能
  • メッセージにInterfaceを複数定義しておき、受ける側はInterfaceで受け取るようにすることで一度のFireで様々なメッセージを同時に発火できる
  • メッセージの宣言についての項でも記載したが OptionalSubscriberWithWarning がデフォルトになっているようなので 意図しないところでSubするべき型がなくて警告が出ることがあるので ontainer.Settings.Signals.MissingHandlerDefaultResponseOptionalSubscriber にして運用するなどが良いように思える
public class Example
{
   SignalBus signalBus;
   public Example(Signalbus signalBus) => this.signalBus = signalBus;
   
   public void CheckpointReached() => signalBus.AbstractFire<SignalCheckpointReached>();
   
   public void DestroyWorld() => signalBus.AbstractFire<SignalWorldDestroyed>();
}

public class SoundSystem
{
   public SoundSystem(SignalBus signalBus)
   {
      signalBus.Subscribe<ISignalSoundPlayer>(x => PlaySound(x.soundId));
   }
   
   void PlaySound(int soundId) { PlaySE(soundId);}
}

public class AchievementSystem
{
   public AchievementSystem(SignalBus signalBus)
   {
      signalBus.Subscribe<ISignalAchievementUnlocker>(x => UnlockAchievement(x.achievementKey));
   }
   
   void UnlockAchievement(string key) { Debug.Log("Unlocks the achievement with the given key" + key); }
}

public class Installer : MonoInstaller  
{  
    public override void InstallBindings()  
    {  
        Container.DeclareSignalWithInterfaces<SignalCheckpointReached>();
        Container.DeclareSignalWithInterfaces<SignalWorldDestroyed>();
    }
} 

// signal types
public struct SignalCheckpointReached : ISignalGameSaver, ISignalSoundPlayer
{ 
   public int SoundId { get => 2} 
}
public struct SignalWorldDestroyed : ISignalAchievementUnlocker, ISignalSoundPlayer
{
   public int SoundId { get => 4}
   public string AchievementKey { get => "WORLD_DESTROYED"}
}

// signal interfaces
public interface ISignalGameSaver{}
public interface ISignalSoundPlayer{ int SoundId {get;}}
public interface ISignalAchievementUnlocker{ string AchievementKey {get;}}

参考

Discussion