##デモ
早速ですがデモです。
それっぽく実装できたので記事にまとめます😎 pic.twitter.com/yISTofPako
— KENTO⚽️XRエンジニア😎Zenn100記事マラソン挑戦中24/100 (@okprogramming) February 11, 2021
音楽プレーヤーです。シークバーで再生箇所を変更できます。
こちらをExtenjectとUniRxをうまいこと使ってMV(R)Pパターンで実装していきます。
下記参考にさせていただきました。
【参考リンク】:UnityにおけるMVPパターンについて
具体的な例を挙げて図解したものが下記です。
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.gameObject のOnDestroy() のタイミングでレシーバの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スクール】カウントダウンタイマーの制作方法