そういうのがいいブログ

アプリ個人開発 まるブログ

アプリ開発覚え書き

【Unity】MVPパターン・インターフェース・VContainer・UniRxを用いてスクリプトを書いてみる

環境

Unity2022.3.15f1
VContainer 1.13.2
UniRx 7.1.0

はじめに

ボタンをクリックするとテキストの数字が増えるという内容で
以下の順番でスクリプトを変更していきました。

1.MVPパターンで構成
2.インターフェースを用いてロジックを差し替え可能にする
3.VContainerを用いた書き方
4.UniRxを用いた書き方

本記事は書き方がどのように変化するのか、
疑問点はどこかを自分の中で整理するために書いています。

※拙著を読まれた前提で書いています。
読まれた方は、インターフェースの差し替えまでは理解できると思いますが
VContainerとUniRxは別途勉強が必要です。(参考の記事を参照下さい) marumaro7.hatenablog.com

普通に書く

まずは一般的な書き方でスクリプトを書いてみます。
以下のスクリプトを用意します。

using UnityEngine;
using UnityEngine.UI;

public class CountManager: MonoBehaviour
{
    private int count = 0;
    public Text textX;

    public void PushCountButton()
    {
        count++;
        textX.text = count.ToString();
    }
}

空のオブジェクトにスクリプトを割り付けて
テキストを割り当て、ボタンに関数を設定します。
こちらは特に問題ないと思います。


1.MVPパターン

役割を分ける書き方です。
・Model(計算などのロジック部分)
・View(表示部分の処理)
・Presenter(ViewとModelを繋げる)

3つのスクリプトを用意した後、
初期化処理を書いてスクリプトを動かします。



View

using System;
using UnityEngine;
using UnityEngine.UI;

public class CountView : MonoBehaviour
{
    public Text counterText;
    public Button countButton;

    public event Action OnIncrementButtonClicked;

    void Start()
    {
        countButton.onClick.AddListener(IncrementButtonClicked);
    }

    private void IncrementButtonClicked()
    {
        OnIncrementButtonClicked?.Invoke();
    }

    public void SetText(int value)
    {
        counterText.text = value.ToString();
    }

    private void OnDestroy()
    {
        countButton.onClick.RemoveListener(IncrementButtonClicked);
    }

}


Model

using System;

public class CountModel
{
    private int _count;

    public int Count
    {
        get
        {
            return _count;
        }
        private set
        {
            _count = value;
            OnCountChange?.Invoke(_count);
        }
    }

    public event Action<int> OnCountChange;

    public void IncrementCount()
    {
        Count++;
    }
}


Presenter

public class CountPresenter
{
    public CountPresenter(CountModel countModel, CountView countView)
    {
        //ビューのイベントにモデルの関数を登録
        countView.OnIncrementButtonClicked += countModel.IncrementCount;

        //モデルのイベントにビューの関数を登録
        countModel.OnCountChange += countView.SetText;
    }
}


初期化処理

using UnityEngine;

public class CountInitializer : MonoBehaviour
{
    [SerializeField] private CountView countView;

    void Start()
    {
        CountModel countModel = new CountModel();
        new CountPresenter(countModel, countView);
    }
}


設定方法

2.インターフェースを用いてロジックを差し替え可能にする

次にインターフェースを用いてロジック部分を差し替え可能にします。

(下図はModelとViewの位置が先程の図と逆になってしまっています。)


View

using System;
using UnityEngine;
using UnityEngine.UI;

public class CountView2 : MonoBehaviour
{
    public Text counterText;
    public Button countButton;

    public event Action OnIncrementButtonClicked;

    void Start()
    {
        countButton.onClick.AddListener(IncrementButtonClicked);
    }

    private void IncrementButtonClicked()
    {
        OnIncrementButtonClicked?.Invoke();
    }

    public void SetText(int value)
    {
        counterText.text = value.ToString();
    }

    private void OnDestroy()
    {
        countButton.onClick.RemoveListener(IncrementButtonClicked);
    }

}


Model

using System;

public class CountModel2 : ICountModel
{
    private int _count;

    public int Count
    {
        get
        {
            return _count;
        }
        private set
        {
            _count = value;
            OnCountChange?.Invoke(_count);
        }
    }

    public event Action<int> OnCountChange;

    public void IncrementCount()
    {
        Count++;
    }
}


インターフェース

using System;

public interface ICountModel
{
    int Count { get; }
    event Action<int> OnCountChange;
    void IncrementCount();
}


Presenter

public class CountPresenter2
{
    public CountPresenter2(ICountModel countModel, CountView2 countView)
    {
        //ビューのイベントにモデルの関数を登録
        countView.OnIncrementButtonClicked += countModel.IncrementCount;

        //モデルのイベントにビューの関数を登録
        countModel.OnCountChange += countView.SetText;
    }
}


初期化処理

using UnityEngine;

public class CountInitializer2 : MonoBehaviour
{
    [SerializeField] private CountView2 countView;

    void Start()
    {
        ICountModel countModel = new CountModel2();
        new CountPresenter2(countModel, countView);
    }
}



新たにCountModelXを作成し、
CountModel2をCountModelXに差し替えたい場合


Model

using System;

public class CountModelX : ICountModel
{
    private int _count;

    public int Count
    {
        get
        {
            return _count;
        }
        private set
        {
            _count = value;
            OnCountChange?.Invoke(_count);
        }
    }

    public event Action<int> OnCountChange;

    public void IncrementCount()
    {
        Count++;
        Count++;
    }
}


下記のように変更するだけでロジックの差し替えが可能となります。
初期化処理

using UnityEngine;

public class CountInitializer2 : MonoBehaviour
{
    [SerializeField] private CountView2 countView;

    void Start()
    {
      //ICountModel countModel = new CountModel2(); 変更前
        ICountModel countModel = new CountModelX();//変更後
        new CountPresenter2(countModel, countView);
    }
}

3.VContainerを用いた書き方

VContainerを使用して書いてみます。

marumaro7.hatenablog.com

これまで初期化処理を書いていましたが。初期化内容の予約を書く形になります。

DIコンテナについていくつもの記事を見て理解しようとしましたが
理解しきれていません。
理解しきれていないながらも書いています。

「参考」にわかりやすかった記事を載せておきます。


View

using System;
using UnityEngine;
using UnityEngine.UI;

public class CountView3 : MonoBehaviour
{
    public Text counterText;
    public Button countButton;

    public event Action OnIncrementButtonClicked;

    void Start()
    {
        countButton.onClick.AddListener(IncrementButtonClicked);
    }

    private void IncrementButtonClicked()
    {
        OnIncrementButtonClicked?.Invoke();
    }

    public void SetText(int value)
    {
        counterText.text = value.ToString();
    }

    private void OnDestroy()
    {
        countButton.onClick.RemoveListener(IncrementButtonClicked);
    }

}


Model

using System;

public class CountModel3 : ICountModel
{
    private int _count;

    public int Count
    {
        get
        {
            return _count;
        }
        private set
        {
            _count = value;
            OnCountChange?.Invoke(_count);
        }
    }

    public event Action<int> OnCountChange;

    public void IncrementCount()
    {
        Count++;
    }
}


インターフェース

using System;

public interface ICountModel
{
    int Count { get; }
    event Action<int> OnCountChange;
    void IncrementCount();
}


Presenter

using UnityEngine;
using VContainer;
using VContainer.Unity;

public class CountPresenter3 :IInitializable
{
    private ICountModel _countModel;
    private CountView3 _countView;

    [Inject]
    public CountPresenter3(ICountModel countModel, CountView3 countView)
    {
        _countModel = countModel;
        _countView = countView;
    }

    public void Initialize()
    {
        Debug.Log("Presenter.Initialize");

        //ビューのイベントにモデルの関数を登録
        _countView.OnIncrementButtonClicked += _countModel.IncrementCount;

        //モデルのイベントにビューの関数を登録
        _countModel.OnCountChange += _countView.SetText;
    }


}


初期化内容の予約

using VContainer;
using VContainer.Unity;

public class CountLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // CountPresenter3をエントリーポイントとして登録。
        // これにより、VContainerはアプリケーションの起動時にCountPresenter3を初期化します。
        builder.RegisterEntryPoint<CountPresenter3>();

        // ICountModelのインターフェースにCountModel3の実装をシングルトンとして関連付ける。
        // これはアプリケーション全体で一つのCountModel3インスタンスが共有されることを意味します。
        builder.Register<ICountModel,CountModel3>(Lifetime.Singleton);

        // CountView3コンポーネントをUnityの階層から検索し、VContainerに登録します。
        // これにより、VContainerはUnityの階層にあるCountView3インスタンスを見つけ出し、依存関係を注入できます。
        builder.RegisterComponentInHierarchy<CountView3>();
    }
}

設定方法
空のオブジェクトを作成しCountLifetimeScopeを割り当てます。

Viewのスクリプト設定内容はこれまでと変化ありません。

ここまでの内容だけですとスクリプトが複雑になっただけで
VContainerを使うメリットは感じられません。

恩恵を受けるのは下記のように複数のPresenterで
ICountModelを使う場合と考えています。

CountPresenterXとCountPresenterYを追加した場合を考えます。
(どのPresenterもCountView3を使っており
 そんなシチュエーションある??という感じですが例としてご容赦を・・・)


追加のPresenter:CountPresenterX (内容はCountPresenter3と同じ)

using UnityEngine;
using VContainer;
using VContainer.Unity;

public class CountPresenterX : IInitializable
{
    private ICountModel _countModel;
    private CountView3 _countView;

    [Inject]
    public CountPresenterX(ICountModel countModel, CountView3 countView)
    {
        _countModel = countModel;
        _countView = countView;
    }

    public void Initialize()
    {
        Debug.Log("Presenter.Initialize");

        //ビューのイベントにモデルの関数を登録
        _countView.OnIncrementButtonClicked += _countModel.IncrementCount;

        //モデルのイベントにビューの関数を登録
        _countModel.OnCountChange += _countView.SetText;
    }

}


追加のPresenter:CountPresenterY (内容はCountPresenter3と同じ)

using UnityEngine;
using VContainer;
using VContainer.Unity;

public class CountPresenterY : IInitializable
{
    private ICountModel _countModel;
    private CountView3 _countView;

    [Inject]
    public CountPresenterY(ICountModel countModel, CountView3 countView)
    {
        _countModel = countModel;
        _countView = countView;
    }

    public void Initialize()
    {
        Debug.Log("Presenter.Initialize");

        //ビューのイベントにモデルの関数を登録
        _countView.OnIncrementButtonClicked += _countModel.IncrementCount;

        //モデルのイベントにビューの関数を登録
        _countModel.OnCountChange += _countView.SetText;
    }

}


初期化内容の予約:コード追加

using VContainer;
using VContainer.Unity;

public class CountLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // CountPresenter3, CountPresenterX, CountPresenterYをエントリーポイントとして登録。
        // VContainerはこれらのクラスをアプリケーション起動時に初期化します。
        builder.RegisterEntryPoint<CountPresenter3>();
        builder.RegisterEntryPoint<CountPresenterX>();//追加
        builder.RegisterEntryPoint<CountPresenterY>();//追加

        // ICountModelインターフェースに対してCountModel3の実装をシングルトンとして関連付けます。
        // アプリケーション全体で1つのCountModel3インスタンスが共有されます。
        builder.Register<ICountModel, CountModel3>(Lifetime.Singleton);

        // Unityの階層からCountView3コンポーネントを検索し、VContainerに登録します。
        // VContainerはUnity階層内のCountView3インスタンスを見つけ出し、依存関係を注入します。
        builder.RegisterComponentInHierarchy<CountView3>();
    }
}


CountPresenter3, CountPresenterX, CountPresenterにて
ICountModelはCountModel3を参照するよう指定している状況です。

ここからCountModelXを参照するよう指定したい時に
CountModel3からCountModelXへ一行のコードを変更することで
各Presenterへ設定する内容を一挙に変更することができます。

using VContainer;
using VContainer.Unity;

public class CountLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        // CountPresenter3, CountPresenterX, CountPresenterYをエントリーポイントとして登録。
        // VContainerはこれらのクラスをアプリケーション起動時に初期化します。
        builder.RegisterEntryPoint<CountPresenter3>();
        builder.RegisterEntryPoint<CountPresenterX>();
        builder.RegisterEntryPoint<CountPresenterY>();

        // ICountModelインターフェースに対してCountModelXの実装をシングルトンとして関連付けます。
        // アプリケーション全体で1つのCountModelXインスタンスが共有されます。
      //builder.Register<ICountModel, CountModel3>(Lifetime.Singleton);//変更前
        builder.Register<ICountModel, CountModelX>(Lifetime.Singleton);//変更後

        // Unityの階層からCountView3コンポーネントを検索し、VContainerに登録します。
        // VContainerはUnity階層内のCountView3インスタンスを見つけ出し、依存関係を注入します。
        builder.RegisterComponentInHierarchy<CountView3>();
    }
}


私の開発規模で恩恵を受けるシチュエーションがイメージできないため
無理して使わなくてもよいかなと考えています。


4.UniRxを用いた書き方

UniRxを使用し数値の変更を検出、関数を実行するように書き換えます。
(導入手順は省略)

ViewとPresenterの内容が先程のコードと変更になります。

スクリプト間の構成としては変化ありません。


View

using UnityEngine;
using UnityEngine.UI;
using UniRx;

public class CountView4 : MonoBehaviour
{
    public Text counterText;
    public Button countButton;

    // UniRxのReactiveCommandを使用
    public ReactiveCommand OnIncrementButtonClicked = new ReactiveCommand();


    void Start()
    {
        // ButtonのonClickイベントをUniRxのObservableに変換
        countButton.onClick.AsObservable()
            .Subscribe(_ => OnIncrementButtonClicked.Execute())
            .AddTo(this); // このオブジェクトが破棄された時に購読を自動的に解除
    }

    public void SetText(int value)
    {
        counterText.text = value.ToString();
    }
}


Model

using System;

public class CountModel4 : ICountModel
{
    private int _count;

    public int Count
    {
        get
        {
            return _count;
        }
        private set
        {
            _count = value;
            OnCountChange?.Invoke(_count);
        }
    }

    public event Action<int> OnCountChange;

    public void IncrementCount()
    {
        Count++;
    }
}


インターフェース

using System;

public interface ICountModel
{
    int Count { get; }
    event Action<int> OnCountChange;
    void IncrementCount();
}


Presenter

using UnityEngine;
using VContainer;
using VContainer.Unity;
using UniRx;

public class CountPresenter4 :IInitializable
{
    private ICountModel _countModel;
    private CountView4 _countView;

    [Inject]
    public CountPresenter4(ICountModel countModel, CountView4 countView)
    {
        _countModel = countModel;
        _countView = countView;
    }

    public void Initialize()
    {
        Debug.Log("Presenter.Initialize");

        // UniRxを使用してイベントを購読
        _countView.OnIncrementButtonClicked.Subscribe(_ =>
        {
            _countModel.IncrementCount();
        }).AddTo(_countView);

        // モデルのイベントにビューの関数を登録
        _countModel.OnCountChange += _countView.SetText;
    }


}


初期化内容の予約

using VContainer;
using VContainer.Unity;

public class CountUniRxLifetimeScope : LifetimeScope
{
    protected override void Configure(IContainerBuilder builder)
    {
        builder.RegisterEntryPoint<CountPresenter4>();
        builder.Register<ICountModel,CountModel4>(Lifetime.Singleton);
        builder.RegisterComponentInHierarchy<CountView4>();
    }
}


設定方法
VContainerを用いた書き方と変化ありません。

おわりに

今回、VContainerとUniRxについては調べながら書いてみました。
これらはプロジェクトの規模が大きいほど効果を発揮すると感じます。

私の場合ですと勉強コストと導入効果が釣り合わない感覚があったため、
基本は「MVPパターン+時々インターフェース」の内容で作り、
どうしても必要になったらVContainerやUniRxの導入を検討する
というスタンスにすることにしました。

時間をかけて調べた割に結局使わない決断をしたわけですが、
自分の中で設計スタンスが決定したので良しとします。

なにか得るものがありましたら幸いです。

参考

UniRxの情報はネットに豊富にあるため
VContainerやDI、DIコンテナについての記事が中心です。

note.com

shuhelohelo.hatenablog.com

xrdnk.hateblo.jp

backpaper0.github.io

qiita.com