はじめに
Unityでの開発において、MV*パターンを適用した設計を用いている現場は多いかと思いますが、毎回リストビューの実装で色々悩んだりするので、自分の考えを整理するために色々と書き出してみる記事です。
面倒なので図とか使わずテキストでダラダラ考えを綴っているので読み辛いです。また筆者の考えを整理するためのメモ書き的な性質が強く、特に何か知見が得られるような記事でも無いためご了承下さい。
MV*パターンについて
MV*パターンはMVC、MVVM、MVPとか色々ありますが、とりあえず最近のUnity界隈ではMVPパターンがイケてるっぽいので、今回はMVPで行くことにします。ぶっちゃけただの呼び方の問題でどうでもいいのではと思ったりするけど、深く突っ込むと戦争になりそう
UnityにおけるMVPパターンの解説については以下の記事が大変分かりやすくまとまっているので、丸投げします。
Web出身のUnityエンジニアによる大規模ゲームの基盤設計
Unityにおける(スクロール)リストビューについて
まず、今回議論の対象である(スクロール)リストビューについての要件を整理することにします。
要はよくありなカード・ユニット・キャラクター一覧画面のことで、大体実装要件は以下のような感じになると思います。
- スクロールビュー
- リストの子要素のビュー
- 子の要素を一定間隔で配置する実装(グリッドレイアウト)
- 大量のリストでも高速に表示できる実装(描画範囲内のみオブジェクトを生成し使い回す)
- これのために、子要素のビューは再利用可能である必要がある
- 選択、複数選択
- ソート、絞り込み
MVPパターンにおける(スクロール)リストビューについて
上記の要件をMVPパターンに落とし込むことを考えます。ここでは仮にカード一覧画面ということにしておきましょう。
とりあえず、最低限以下のようなクラスが登場してきそうな気がします。
-
Model
- Card
-
View
- CardView
- CardListView
-
Presenter
- CardPresenter
- CardListPresenter
ここで、UIの状態保持について考えてみます。上記の要件だと、「カードの選択状態」と「ソート条件」「絞り込み条件」をどこかで持っておく必要がありそうです。持つのはCardListPresenterだと考えられます。
-
Model
- Card
-
View
- CardView
- CardListView
-
Presenter
- CardPresenter
- CardListPresenter
- SortCondition
- FilterCondition
- カード選択状態(Dictionary<Card, bool>的な?)
ここで、「カードの選択状態」の表示(選択されたら枠で囲まれる的なもの)を担うのはCardView、CardListViewのどちらか?という問題があります。カードを選択するというのはリストビュー側の都合ですが、選択されたら枠を出すというのはリストビューの子要素であるCardViewが担当するのが良さそうです。
ただ、そうなるとそのViewはCardを単に表示するViewではなく、「自身がCardListViewの子要素であることを知っている」Viewになるので、適切なクラス名に変えたほうが良さそうです。ここが悩みポイント1で、毎回命名に困ります。「CardListItemView」「CardListChildView」などが考えられそうですが…例えばこれがアイテム一覧画面だったら「ItemListItemView」なんてことになったりしてすごい微妙な気持ちになります。
とりあえず今回はCardListChildViewということにしておきます。
-
Model
- Card
-
View
- CardListChildView
- CardListView
-
Presenter
- CardListChildPresenter
- CardListPresenter
- SortCondition
- FilterCondition
- カード選択状態(Dictionary<Card, bool>的な?)
ここで、ユーザーの入力を受け取ってから選択状態の表示の反映まではどのようにして行うか?という問題があります。
恐らく、以下のようなフローになりそうです。(UniRxを用いることにします)
- CardListChildViewがButtonClickEvent(IObservable<UniRx.Unit>)を発行
- CardListChildPresenterが1.を受け取りCardSelectedEvent(IObservable<Card>)を発行
- CardListPresenterが2.を受け取り、カード選択状態のDictionary的なものに反映、かつ該当するCardに対応するCardListChildPresenterに変更を通知
- CardListChildPresenterが3.を受け取り、CardListChildViewに対して選択状態の表示の反映をさせる
この3.の部分で状態管理とViewへの反映を別でやっているのが冗長で面倒な感じなので、ReactivePropertyなんかを使って状態管理と通知を一括でやれるようにしたくなってきます。
で、悩みポイント2、そのためのクラスの命名に悩みます。CardListChildViewに対応するModelだから、CardListChildViewModel…????(?)MVPなのにViewModel?いいのか?まあ別にいいのかもしれない
追記:UIの状態保持のためのオブジェクトなので、Presentation Modelのほうが良いかも
public class CardListChildViewModel
{
public Card Card { get; private set; }
public ReactiveProperty<bool> IsSelected;
}
ViewModelといいつつ、質的にはUIロジックを持つものだし、PresenterのメンバになるのでPresenterに分類される気がする。
-
Model
- Card
-
View
- CardListChildView
- CardListView
-
Presenter
- CardListChildPresenter
- CardListPresenter
- SortCondition
- FilterCondition
- List<CardListChildViewModel>
これで選択状態の入力〜反映のフローは以下のような感じになるはず。初期化(購読)のフローが加わっているため、実質的には4〜7が入力〜反映のフローになる。
- CardListPresenterが外部からCardのリストを受け取り、CardListChildViewModelとして持つ
- CardListPresenterがCardListChildViewModelをCardListChildPresenterに渡す
- CardListChildPresenterがCardListChildViewModelのReactivePropertyを購読し、選択状態の反映を行うようにする
- CardListChildViewがButtonClickEvent(IObservable<UniRx.Unit>)を発行
- CardListChildPresenterが4.を受け取りCardSelectedEvent(IObservable<Card>)を発行
- CardListPresenterが5.を受け取りCardListChildViewModelに反映
- 3.で生成したストリームによって、CardListChildViewの選択状態の表示反映が行われる
なんかデータバインディングっぽいことをしているのでMVVMっぽい気がする。
ここでMVVMと異なる点は、あくまでPresenterがViewへの表示反映を担当しているという点で、Rxでリアクティブに反映される形になるものの、購読して反映するというコードはPresenter内に書かなければいけない。
という訳で、やっぱりViewModelという命名を使うのはなんだかイケナイ気がする。(余計な混乱を招きそうな感じがするので)
Viewに状態を持たせればいいのでは?
上記のようなことをしなくても、Viewに一時的に選択状態を持たせて、確定時にCardListPresenterが子のViewの選択状態を集約すれば、選択という要件は満たすことができ、処理のフローもシンプルになる。
が、この実装が成り立つのはCardに対応するViewが常に1:1で存在している時に限る。
どういうことかと言うと、いわゆる無限スクロールビュー的な実装をした時、Viewは描画範囲内のリスト要素にのみ生成され、スクロール時にはそれを使い回すため、要素が描画範囲外になった時に状態が保持できなくなる。
まとめ
- リストビューの子要素となるViewのクラスの命名に悩む
- リストビューの子要素となるViewに対応するクラスの命名に悩む
- 追加で層を用意したほうが良いのかも知れない
書いてみてようやく思考が整理されたけど、単純に命名が難しいなという話になっている気がした。
何かいい案とかあれば教えてください。