LoginSignup
14
0
お題は不問!Qiita Engineer Festa 2023で記事投稿!

【Unity】Unityで学ぶデザインパターン19: Observer パターン【デザパタ】

Last updated at Posted at 2023-07-04

はじめに

様々な言語で「デザインパターン」の本が世の中にありますが、筆者個人の経験では
いまいちピンとこない例 いまいちピンとこないコード で説明されてることが多く、
結局これっていつ使うの? という疑問に答えるには仕事仲間等との議論をしないと
辿り着けないことが多々ありました。

そこで特に「ゲーム開発ではどう使うか?」にフォーカスを当てて、実践的な例を交えて
デザインパターンの説明の需要があると思い記事を作りました。

デザインパターンを学ぶ理由

デザインパターンを学ぶ理由としては

  1. 車輪の再発明の防止
  2. 長文で読みにくいコード(可読性の低いコード)を減らす
  3. コードを疎結合にして変更に強くなる(変更時のコスト・変更箇所を減らす)
  4. モジュールとして使いまわせるように、コードの再利用性を高める
    といった効果を期待できます。

対象読者

Unity 全くの初心者(インストールしただけで触ったことがないような方)はお断りです。
最低限以下のことは理解・経験を積んでおくことが必須になります。

  • MonoBehaviour 継承クラスでコードを書いたことがある
  • C# のピュアクラスを用いた自作クラスを作ったことがある
  • クラスの継承という概念は知っている

そのため、脱・初心者 中級者へのステップアップ として デザインパターンを学ぶ のが良いと思います。

デザパタ記事リンク

生成系

  1. AbstractFactory パターン
  2. Builder パターン
  3. FactoryMethod パターン
  4. Prototype パターン
  5. Singleton パターン

構造系

  1. Adapter パターン
  2. Bridge パターン
  3. Composite パターン
  4. Decorator パターン
  5. Facade パターン
  6. Flyweight パターン
  7. Proxy パターン

様態・ふるまい系

  1. Chain of Responsibility パターン
  2. Command パターン
  3. Interpreter パターン
  4. Iterator パターン
  5. Mediator パターン
  6. Memento パターン
  7. Observer パターン(本記事)
  8. State パターン
  9. Strategy パターン
  10. TemplateMethod パターン
  11. Visitor パターン

Observer パターンについて

観測者 という意味のObserverパターン。手続的な処理だけなら特に不要なデザインパターンですが、 イベントドリブン な作りに関しては絶対に避けては通れないほど非常に重要なデザインパターンです。
特にイベントドリブンを多用する リアクティブプログラミング ではObserverパターンを理解していないと、処理内容を追えないし、まともに動作するプログラムすら難しいでしょう。

UniRx-UniTask完全理解-より高度なUnity-C-プログラミング でもUniRxの説明の前に Observerパターンイベントドリブン(イベント駆動プログラミング) について書かれているとおり、概念として非常に重要だし、最初は正直わかりづらい仕組みだと思います。

ただし、Observerパターンを理解することによって、MVVM(Model-View-ViewModel) による設計がしやすくなったり、クラス間の疎結合化を進めることができたりと、より設計力が問われることを行うことにつながるため、勉強しておいて損はないでしょう。

ちなみにObserverパターンは、正直なところ 観測 というよりは 通知 がメインになってくるため、 用途からしたら Notifier パターンといってもいいくらい 観測成分が少ない 印象です。

Unityにおける Observer パターン

まずObserverパターンの要件としては 予めイベント時に実行する処理を登録 し、イベントが来たタイミングで登録済みの処理を一斉に実行する ことの二点が満たされていれば問題ありません。

イベントが起きる→事前登録処理に通知(関数呼び出し)→イベント時の登録済み処理を実行 という流れになり、この登録したイベント視点でいうと イベントが起きるまで見守っている・監視・観測している ことからObserver と呼ばれます。

NonUniRx なObserverパターン

UnityEvent では、AddLister() あらかじめ登録した関数を UnityEvent.Invoke() のタイミングで一斉に呼び出すようなことが出来ます。

これもフロー的には UnityEvent が内部でActionのListを保持しており、 Invoke() タイミングで内部のActionListを全て実行という作りです。
もし自前で作るなら以下のような形です。

MockSubject.cs

public class MockSubject
{
    private List<Action> actionList = new List<Action>();

    public void AddListener(Action act){actionList.Add(act);
    public bool RemoveListener(Action act){ return actionList.Remove(act);}

    public void Invoke()
    {
        foreach( var act in actionList){ act?.Invoke(); }
    }
}

このようにして、あとはこのUnityEvent(MockSubject) のInvoke()を適切なタイミングで実行してあげるということです。
例えば以下のようなコードであれば、自分のキャラクターが死亡時にイベントを発火して事前登録されていた処理を実行という仕組みが出来ます。

DummyDeadEvent.cs
public MockSubject deadEventNotifier = new MockSubject();
void Update()
{
    if( myCharacter.Hp < 1)
    {
        deadEventNotifier.Invoke();
    }
}

あとはUIのButtonなどは典型的なイベントドリブンで処理が実行される例です。
Button.onClick に関数を登録することで、ボタン押下時にイベントが発火しますね。おそらくチュートリアル的にほとんどの人が使っていると思いますが、これもObserverパターンの実装例です。

UniRx におけるObserverバターン

UniRx を導入すると、イベントの発火ではなくイベントの登録(UniRxでいうところの購読(=subscribe) 部分がだいぶ柔軟に扱えるようになります。

具体的にはIObserver を拡張してイベント時に実行される処理をラムダ式でかけるようになっているところと、値変更イベントを通知してくれる Reactive~ 系が充実しているところです。

UniRxはUniRxを使わない方式に比べ、だいぶコーディングの仕方変わってきます。
UniRxを用いない場合は Observer(イベント時に実行してほしい処理があるクラス)Observed(処理を登録する場所。イベント実行するところ) に対して関数を登録するという方式のため、 ObserverがObservedに依存する 作りになっていました。(もちろんInterface を使えば依存性の逆転を出来ますが、費用対効果が低いのが実情です)

DummyDeadEvent.cs
public class DummyDeadEvent
{
    public MockSubject deadEventNotifier = new MockSubject();
    void Update()
    {
        if( myCharacter.Hp < 1)
        {
            deadEventNotifier.Invoke();
        }
    }
}

DummyDeadEventObserver.cs

public class DummyDeadEventObserver
{
    public DummyDeadEventObserver(DummyDeadEvent deadEvent)
    {
        deadEvent.deadEventNotifier.AddListener(this.OnDead);
    }

    private void OnDead()
    {
        // 何かの処理
    }
}

特に、1回しか発火しないイベントでも、わざわざ関数定義して処理を書かないといけないなど色々と手間がかかりました。

一方UniRxで書くと以下のようになります。

DummyDeadEventUniRx.cs
using System;
using UniRx;

public class DummyDeadEventUniRx : IDisposable
{
    public IObservable<Unit> OnDead => deadSubject;
    private Subject<Unit> deadSubject = new Subject<Unit>();
    void Update()
    {
        if( myCharacter.Hp < 1)
        {
            deadSubject.OnNext(Unit.Default);
        }
    }
    public void Dispose(){
        deadSubject.Dispose();
    }
}

DummyDeadEventObserverUniRx.cs
using System;
using UniRx;

public class DummyDeadEventObserver : IDisposable
{
    private readonly CompositeDisposable _disposable = new CompositeDisposable();
    public DummyDeadEventObserver(DummyDeadEventUniRx deadEvent)
    {
        deadEvent.OnDead.Subscribe(_ =>{
            // 死亡時の何かの処理
            // DummyDeadEventObserver.OnDeadの代わり
        }).AddTo(_disposable);
    }
    public void Dispose(){
        deadSubject.Dispose();
    }
}

ラムダ式が使えるため、イベントの購読タイミングで実行時の処理をかけるようになりました。
これのおかげで、イベントと実行時の処理をひとまとめで管理できます。
また、イベント発火側は 購読先にイベントの通知 だけに専念でき、 どの関数を実行するか 等は考えなくてよくなりました。

今回は小さいサンプルのため恩恵が薄いように見えますが、UniRxはメソッドチェーンでイベントに対して加工ができたりします。
.Take(N) で最初のN回しか実行しない や、 .Where(x => someBoolMethod(x)) でWhere内部で指定した処理がtrue の時だけイベントが実行されるようになるとかさまざまな便利機能があるために人気になっています。

また、MVVMという設計においては ViewModel が以下のように作られます。

DummyViewModel.cs

public class DummyViewModel : IDisposable{

    public readonly IntReactiveProperty SomeNumber = new IntReactiveProperty(0);
    public readonly BoolReactiveProperty SomeFlag = new BoolReactiveProperty(false);

    private readonly CompositeDisposable _disposable = new CompositeDisposable();
    public DummyViewModel(){
        SomeNumber.AddTo(_disposable);
        SomeFlag.AddTo(_disposable);
    }

    public void Dispose(){
        if(_disposable.IsDisposed)return;
        _disposable.Dispose();
    }
}

DummyView.cs

public class DummyView:MonoBehaviour
{
    [SerializeField]private Text _numLabel;
    [SerializeField]private Text _label;
    public void Bind(DummyViewModel vm){
        vm.SomeNumber.Subscribe(num=>{
            // 数値が更新されたらテキストを変更
            _numLabel.text = num.ToString();
        }).AddTo(this);

        vm.SomeFlag.Subscribe(isActive=>{
            // Flag が変わったらテキストを切り替える
            _label.text = isActive : string.Intern("On") : string.Intern("Off");
        }).AddTo(this);

    }
}

DummyModel.cs

public class DummyModel
{
    public DummyModel(DummyViewModel vm)
    {
        vm.SomeNumber.Value = 100;//初期値設定&通知
        vm.SomeFlag.Value = false;//初期値設定&通知
    }

    
}

このようにするとView(UI) はViewModel にしか依存せず、Model側のロジック変更があっても特に修正をする必要はなく、またUi側の操作が仮に変更してもModel側のソースを変更する必要がありません。(従来なら void SetNumberLabel(int num) のような関数を呼び出しているはずです)
また、両者はあくまでもViewModelを介してしかやりとりしないため、ViewはUI制御に専念でき、Modelはロジックに専念できるため、疎結合化が進み、余計な介入も少ないため変更に強い作りになります。

このあたりは著者の別記事で詳しく解説してあります。

まとめ

UniRx やイベントドリブンな作りにするには理解が必須のObserverパターン。
ただしやってることはほぼNotifier なので、名前に惑わされずに処理内容を追っていくと良いと思います。

14
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
0