適当おじさんの適当ブログ

技術のことやゲーム開発のことやゲームのことなど自由に雑多に書き連ねます

ZenjectのSignalを使ってみたのでメモ

Zenjectが提供する機能の1つである Signal を使ってみたのでそのときのメモです。

Zenject/Signals.md at master · modesttree/Zenject · GitHub

前半でざっくりとしたSignalの説明、後半でSignalを使った簡単な一時停止機能のサンプルの話をします。

Signal とは?

あるクラスが情報をやりとりする際に、お互いの存在を知らずに情報をやり取りさせるための仲介者です。クラスはそれぞれSignalの存在だけを知っています。Signalを用いることで、やりとりする相手の実装を気にせずに情報をやりとりできるようになります。

Signalを使う大まかな流れは、以下のような感じです。

  1. Signal を定義して、Container にバインド
  2. Signal.Fire() でシグナルを送る
  3. Signal をハンドリングする

1. Signalの定義

Zenjectの Signal クラスを継承したクラスを定義します。公式ドキュメントに Classes that derive from Signal should always be left empty - their only purpose is to represent a single action. とある通り、自身がどんなSignalかだけを表すクラスとします。

public class TestSignal : Signal<TestSignal> {}

自身の型以降にSignal発火時の引数の型を定義できます。最大4つまで指定できました。それ以上はエラーになってしまいます。

public class TestSignal : Signal<TestSignal, int, string, bool, double> {}

Signalと同時に多くのデータを送りたいのであれば、専用のDTOクラスを作成するのも手かと思います。渡すデータがあまりに肥大化するようであれば、一度立ち止まって別の方法を検討しても良いかもしれません。

public class TestSignal : Signal<TestSignal, TestSignalDto> {}

class TestSignalDto() {
    public bool IsBool;
    ...
}

Zenjectではおなじみ InstallBindings() のオーバーライドで、Container.DeclareSignal<T>() します。これでContainerに定義したSignalの存在を知らせることできました。引数がある場合は同様にSignal自身の型以降に、引数の型を指定します。

public class TestInstaller : MonoInstaller<TestInstaller>
{
    public override void InstallBindings()
    {
        Container.DeclareSignal<TestSignal>();
        // 引数がある場合
        // Container.DeclareSignal<TestSignal, int, string, bool, double>()
    }
}

DeclareSignal<T>() したことで、他のクラスにインジェクションできるようになります。

2. Signal.Fire() でシグナルを送る

Fire() メソッドでシグナルを送ることができます。Signal定義時に引数に対応する型を指定していた場合は、引数にそれぞれ値を指定する必要があります。

public class TestFire {
    private TestSignal _testSignal;

    public TestFire(TestSignal testSignal) {
        _testSignal = testSignal;
        _testSignal.Fire();
        // 引数がある場合
        // _testSignal.Fire(1, "文字列", false, 1.2)
    }
}

3. Signal をハンドリングする

以下のいずれかの方法があります。今回は 「2. UniRxでSubscribeする」 で進めます。他の部分でも多くUniRxを使っており、揃えたかったからです。

  1. C#のeventの仕組みを使う
  2. UniRxでSubscribeする
  3. InstallerBindingする

3の方法はUniRx不要で、より疎結合にすることができます。1と2の方法は Signal を発火する側・ハンドリングする側の双方がSignalの存在を知っていなければなりませんが、3の方法であればSignalを発火する側だけがSignalを知っていれば良いためです。

3の具体的なサンプルは 公式のサンプル を参照してください。状況に応じて適した方法を選択すれば良いかと思います。

UniRxを使うための設定

「2. UniRxでSubscribeする」の方法を取る場合、Edit -> Project Settings -> Playerの以下の項目に ZEN_SIGNALS_ADD_UNIRX と設定しなければなりません。

f:id:subarunari:20180227213547p:plain

プラットフォームごとに下記項目は存在するためそれぞれ設定する必要があるかと思います。この点については確認していないので、ご存知の方がいれば教えていただきたいです。

実装サンプル

以上を踏まえて、一時停止を実現するための各クラスを定義してみました。Signalを役割別に定義していますが、引数に停止状態か否かを表すbool などでも良いと思います。

一時停止機能はTime.timeScale=0を使わない場合を前提としています。その部分の実装については説明を省いているので以下を参考にしてください。

// 一時停止を知らせるSignal
public class PauseSignal : Signal<PauseSignal> {}
// 一時停止の解除を知らせるSignal
public class RestartSignal : Signal<PauseSignal> {}
public class PauseInstaller : MonoInstaller<PauseInstaller>
{
    public override void InstallBindings()
    {
        Container.DeclareSignal<PauseSignal>();
        Container.DeclareSignal<RestartSignal>();
    }
}
// スペースキーが押されたらSignalを送るマネージャークラス
public class PauseManager : MonoBehaviour {
    [Inject]
    private PauseSignal _pauseSignal;
    [Inject]
    private ReleaseSignal _releaseSignal;
    private bool _isPaused;

    void Awake()
    {
        _isPaused = false;
    }

    void Start()
    {
        this.UpdateAsObservable().Where(_ => Input.GetKey(KeyCode.Space)).Subscribe(_ =>
        {
            if (_isPaused)
            {
                _releaseSignal.Fire()
            }
            else
            {
                 _pauseSignal.Fire();
            }
            _isPaused = !_isPaused;
        });
    }
}
// 一時停止対象のGameObjectにアタッチするコンポーネント
public class PauseTarget : MonoBehaviour
{
    [Inject]
    PauseSignal _pauseSignal;
    [Inject]
    RestartSignal _restartSignal;

    void Start()
    {
        _pauseSignal.AsObservable.Subscribe(_ => Pause(_)).AddTo(this);
        _restartSignal.AsObservable.Subscribe(_ => Restart(_)).AddTo(this);
    }

    void Pause() { /* ポーズ時の処理 */ }
    void Restart() { /* 再開時の処理 */ }
}