9
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

【Unity(C#)】UniRxとExtenjectでMV(R)Pやってみる

Last updated at Posted at 2021-02-11

##デモ
早速ですがデモです。

音楽プレーヤーです。シークバーで再生箇所を変更できます。

こちらをExtenjectUniRxをうまいこと使ってMV(R)Pパターンで実装していきます。

下記参考にさせていただきました。
【参考リンク】:UnityにおけるMVPパターンについて

具体的な例を挙げて図解したものが下記です。

MV(R)P_Ex.png

ViewとModelどちらかを変更すればもう片方に変更に応じた処理が反映されます。

途中寄り道しながらいろいろメモしていきます。

View

まずはViewです。

using UnityEngine;
using UnityEngine.UI;

namespace Ono.MVP.View
{
    /// <summary>
    /// View
    /// </summary>
    public class MusicPlayerView : MonoBehaviour
    {
        [SerializeField] private Button _playButton, _stopButton;
        [SerializeField] private Slider _seekBar;
        [SerializeField] private Text _playTimeText;

        /// <summary>
        /// 再生ボタン
        /// </summary>
        public Button PlayButton => _playButton;

        /// <summary>
        /// 停止ボタン
        /// </summary>
        public Button StopButton => _stopButton;

        /// <summary>
        /// シークバー
        /// </summary>
        public Slider SeekBar => _seekBar;

        /// <summary>
        /// 再生時間をセット
        /// </summary>
        /// <param name="timeText">表示される時間</param>
        public void SetPlayTime(string timeText)
        {
            _playTimeText.text = timeText;
        }

        /// <summary>
        /// ボタン切り替え
        /// </summary>
        public void SwitchButton()
        {
            _playButton.gameObject.SetActive(!_playButton.gameObject.activeInHierarchy);
            _stopButton.gameObject.SetActive(!_stopButton.gameObject.activeInHierarchy);
        }
    }
}

UIコンポーネントをSerializeFieldに設定して
public Button PlayButton => _playButton;で公開するというやり方は下記を参考にしました。

【参考リンク】:Web出身のUnityエンジニアによる大規模ゲームの基盤設計

やや冗長に感じましたが、View以外のクラスを肥大化させないため
MonoBehaviorが必要のないクラスにMonoBehaviorの継承をさせないためには有効な手法のようです。
(教えてくださった方ありがとうございます!)

##Model
続いてロジックを担うModelです。

using Ono.MVP.CustomRP;
using UniRx;
using UniRx.Triggers;
using UnityEngine;

namespace Ono.MVP.Model
{
    /// <summary>
    /// Model
    /// </summary>
    public class MusicPlayerModel : MonoBehaviour
    {
        [SerializeField] private AudioSource _bgm;

        /// <summary>
        /// 音楽再生モード
        /// </summary>
        public IReadOnlyReactiveProperty<MusicPlayMode> MusicPlayModeRP => _musicPlayModeRP;

        private readonly MusicPlayModeReactiveProperty _musicPlayModeRP =
            new MusicPlayModeReactiveProperty(MusicPlayMode.Stop);

        /// <summary>
        /// 再生時間
        /// </summary>
        public IReadOnlyReactiveProperty<float> MusicPlayTimeRP => _musicPlayTimeRP;

        private readonly FloatReactiveProperty _musicPlayTimeRP = new FloatReactiveProperty();

        private void Start()
        {
            this.UpdateAsObservable()
                .Where(_ => _musicPlayModeRP.Value == MusicPlayMode.Play)
                .Subscribe(_ => { _musicPlayTimeRP.Value = _bgm.time / _bgm.clip.length; })
                .AddTo(this);
        }

        /// <summary>
        /// 再生時間を時分として取得
        /// </summary>
        /// <returns>再生時間</returns>
        public string GetMusicTime()
        {
            var totalMinute = (int) _bgm.clip.length / 60;
            var totalSecond = (int) _bgm.clip.length % 60;
            var currentMinute = (int) _bgm.time / 60;
            var currentSecond = (int) _bgm.time % 60;
            return $"{currentMinute}:{currentSecond:00} / {totalMinute}:{totalSecond}";
        }

        /// <summary>
        /// 再生し再生モードに切り替え
        /// </summary>
        /// <param name="playTimeNormalizedValue">正規化された再生箇所の値</param>
        public void PlayMusic(float playTimeNormalizedValue)
        {
            _bgm.time = playTimeNormalizedValue * _bgm.clip.length;
            _bgm.Play();
            _musicPlayModeRP.Value = MusicPlayMode.Play;
        }

        /// <summary>
        /// 停止し停止モードに切り替え
        /// </summary>
        public void StopMusic()
        {
            _bgm.Pause();
            _musicPlayModeRP.Value = MusicPlayMode.Stop;
        }
    }
}

###EnumのReactiveProperty
今回は再生中、停止中の切り替えにEnumのReactivePropertyを定義して利用しました。
(後々思いましたが、別にEnumの定義した値が2つならBoolReactivePropertyでも良かったかもです。)

using System;
using UniRx;

namespace Ono.MVP.CustomRP
{
    /// <summary>
    /// 音楽再生に関する状態
    /// </summary>
    public enum MusicPlayMode
    {
        Play,
        Stop
    }
    
    [Serializable]
    public class MusicPlayModeReactiveProperty : ReactiveProperty<MusicPlayMode>
    {
        public MusicPlayModeReactiveProperty (){}
        public MusicPlayModeReactiveProperty (MusicPlayMode initialValue) : base (initialValue) {}
    }
} 

##Presenter
ViewとModelを繋ぐPresenterです。
MonoBehaviorは継承していません。
ですので、ここでExtenjectの機能を利用します。

using System;
using Ono.MVP.Model;
using Ono.MVP.View;
using UniRx;
using UniRx.Triggers;
using Zenject;

namespace Ono.MVP.Presenter
{
    /// <summary>
    /// Presenter
    /// </summary>
    public class MusicPlayerPresenter : IInitializable, IDisposable
    {
        private readonly MusicPlayerView _musicPlayerView;
        private readonly MusicPlayerModel _musicPlayerModel;
        private CompositeDisposable _disposables;

        /// <summary>
        /// コンストラクタインジェクション
        /// </summary>
        /// <param name="musicPlayerView">View</param>
        /// <param name="musicPlayerModel">Model</param>
        public MusicPlayerPresenter(MusicPlayerView musicPlayerView, MusicPlayerModel musicPlayerModel)
        {
            _musicPlayerView = musicPlayerView;
            _musicPlayerModel = musicPlayerModel;
        }

        /// <summary>
        /// "MonoBehaviorで言うところのStart"にあたるライフサイクルイベントとして呼ばれるメソッド
        /// </summary>
        public void Initialize()
        {
            _disposables = new CompositeDisposable();

            //==========================
            // View → Modelへの反映
            //==========================

            //再生ボタン押下
            _musicPlayerView.PlayButton.OnClickAsObservable()
                .Subscribe(_ => { _musicPlayerModel.PlayMusic(_musicPlayerView.SeekBar.value); }).AddTo(_disposables);

            //停止ボタン押下
            _musicPlayerView.StopButton.OnClickAsObservable()
                .Subscribe(_ => { _musicPlayerModel.StopMusic(); }).AddTo(_disposables);

            //シークバーのドラッグ開始
            _musicPlayerView.SeekBar.OnPointerDownAsObservable()
                .Subscribe(_ => { _musicPlayerModel.StopMusic(); }).AddTo(_disposables);

            //シークバーのドラッグ終了
            _musicPlayerView.SeekBar.OnPointerUpAsObservable()
                .Subscribe(_ => { _musicPlayerModel.PlayMusic(_musicPlayerView.SeekBar.value); }).AddTo(_disposables);

            //==========================
            // Model → Viewへの反映
            //==========================

            //再生時間を反映
            _musicPlayerModel.MusicPlayTimeRP
                .Subscribe(time =>
                {
                    //シークバーに反映
                    _musicPlayerView.SeekBar.value = time;
                    //テキストに反映
                    _musicPlayerView.SetPlayTime(_musicPlayerModel.GetMusicTime());
                }).AddTo(_disposables);

            //再生モード変更に応じてボタンの表示を切り替え
            _musicPlayerModel.MusicPlayModeRP
                .SkipLatestValueOnSubscribe()
                .Subscribe(_ => { _musicPlayerView.SwitchButton(); }).AddTo(_disposables);
        }

        /// <summary>
        /// 破棄する処理
        /// </summary>
        public void Dispose()
        {
            _disposables.Dispose();
        }
    }
}

###IInitializable
Extenjectには非MonoBehaviourをDIする際にMonoBehaviourのように振舞わせる仕組みがあるそうです。

InitializableはStart関数と同様なタイミングのようです。
下記が対応表です。

名前 実装するメソッド 対応するMonoBehaviourのメソッド
IInitializable Initialize Start
ITickable Tick Update
ILateTickable LateTick LateUpdate
IFixedTickable FixedTick FixedUpdate

【引用元】:【Unity】【Zenject】非MonoBehaviourをDIする際にMonoBehaviourのように振舞わせる

IInitializableの中で購読を開始することで、
Start関数で購読を開始するというお馴染みの流れを
非MonoBehavior継承クラスにて疑似的に実現できました。

###IDisposable
IDisposableはインスタンスが破棄されるタイミングで呼び出されます。

AddTo(this)とすることで
Monobihaviorを継承したオブジェクトが破棄される際に
購読を自動的に停止することができます。

これはAddToに下記のようなオーバーロードが実装されているからです。

シグネチャ 挙動
AddTo(GameObject gameObject) GameObjectのOnDestroy()のタイミングでレシーバのDispose()が呼ばれる
AddTo(Component gameObjectComponent) component.gameObjectOnDestroy()のタイミングでレシーバのDispose()が呼ばれる
AddTo(ICollection<IDisposable> container) container.Dispose()のタイミングでレシーバのDispose()が呼ばれる(内部的にcontainer.Add(レシーバ)される)
AddTo(ICollection<IDisposable> container, GameObject gameObject) container.Dispose()とGameObjectのOnDestroy()のタイミングでレシーバのDispose()が呼ばれる(内部的にはAddTo(container).AddTo(gameObject)している)

【引用元】:AddToメソッド

しかし、非Monobihaviorの場合、上記表を見てわかる通りAddToの引数にthisは使えません。
なのでIDisposable.Dipose()の中で明示的に購読を停止される必要があるということです。

##Installer

Extenjectでお馴染みのInstallerです。
ViewとModelのインスタンスはヒエラルキー上のオブジェクトから参照したいので
FromComponentOnで指定しています。

using Ono.MVP.Model;
using Ono.MVP.Presenter;
using Ono.MVP.View;
using UnityEngine;
using Zenject;

namespace Ono.MVP.Installer
{
    /// <summary>
    /// Installer
    /// </summary>
    public class MusicPresenterInstaller : MonoInstaller
    {
        [SerializeField] private GameObject _view, _model;

        public override void InstallBindings()
        {
            Container.Bind<MusicPlayerView>().FromComponentOn(_view).AsSingle();
            Container.Bind<MusicPlayerModel>().FromComponentOn(_model).AsSingle();
            Container.Bind(typeof(MusicPlayerPresenter), typeof(IInitializable))
                .To<MusicPlayerPresenter>().AsSingle();
        }
    }
}

ExtenjectのライフサイクルイベントをきっかけにResolveしたいときの書き方を
今回調べて初めて知りました。

【参考リンク】:【Unity】【Zenject】非MonoBehaviourをDIする際にMonoBehaviourのように振舞わせる

##おわりに

個人的な学習用のサンプルなのでツッコミ大歓迎です。
下記に上げときます。
【GitHub】: MVP_Demo

どんどんアップデートされてるVContainerも触っていきたいです。

##参考リンク
Zenjectを使うときに気を付けていること
[UniRx]購読を停止する
UIのシステムをUniRxで構築する
VContainerを組み込んだゲームサンプル
Unityで経過時間、制限時間を表示する機能を作成する
【Unityスクール】カウントダウンタイマーの制作方法

9
10
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
9
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?