はじめに
View実装は、Presenterまでが適切に実装されていれば「単にPresenterから送られてきたデータをPresenterの指示に従って表示するだけ」のシンプルな作業です。(なお、ここがシンプルにならない場合はModelとPresenterの設計にミスがあるか、単純に一つのシーンに機能を詰め込みすぎだと思われます)
したがって、あまり注意すべきTipsは多くありませんし、増やすべきでもありません。
ただし、もちろん何も考えずに書くわけにもいかないので注意点をいくらか書いていきます。
アセンブリを切ろう
ViewのアセンブリはPresenterとUniRxに加えて、色々なライブラリを導入してよいです。
今回の例ではDOTweenのアセンブリを参照に追加しています。
純粋なC#であることを求められたModelやPresenterのアセンブリ参照とは違って、Viewには演出用のライブラリを入れることが許されます。
また、ViewがModelの参照を持つことは構いません。もちろん直接Model自体をViewが操作するのはご法度ですが、Modelが提供しているロジックがあまりないデータクラスをフラグ代わりに使うことは特に問題ないでしょう。
例えば今回では、Modelで定義している列挙型であるPuyoColor
を使っているのでViewがModelを参照しています。(もちろん、ただのswitchのフラグとして使われているだけでModelの操作には完全に無関係です)
Viewはコンポーネントとして実装しよう
ViewはMonobahaviour
を継承したクラスとして実装します。そうしないとほとんどのUnityの機能を十全に使いきれないからです。
また、Presenter編でも解説したことですが、ViewとPresenterのフォルダ階層は同じになるようにしましょう。
逆に、Puzzleフォルダー以下の詳細なフォルダ内容については自由です。
サンプルコードはあまり大きくならなかったのでPrefabやScriptが全部直置きしてありますが、もちろんPrefabフォルダやScriptフォルダを作ってもよいです。
コンポーネントをどこにつけるのかという問題についてもある程度の自由があります。
メインとなるView、サンプルコードならPuzzleView
はに関してはCanvas
や、GameManager
などある程度目立つゲームオブジェクトにつけて、その他のViewはそれぞれの担当分野のゲームオブジェクトにつけることなどが大まかな方針になります。Viewのゲームオブジェクトの構成については、ViewがどのようにPresenterの要求を実現するかによって大きく異なってくる場所ですので、あまり詳しく指定することはできません。
staticな関数を活用しよう
Viewにおいて、Presenterからもらうデータが論理的な「赤ぷよ」であるところを変換して「赤ぷよ画像のSprite」というUnityの存在にする必要があります。このような変換のためにわざわざメインのView担当コンポーネントがついているゲームオブジェクトをFind
してきてGetComponent
してデータを取得するのは面倒ですし非本質な処理です。したがって、その画面のメインのビューにおいてstaticな関数として定義するのがよりシンプルで良い設計です。
具体例として、サンプルコードでは画面の座標変換で以下のような実装をしています。ここさえ書き換えれば、画面の部品のサイズ変更などに対応出ますから、変更場所が確定しやすく、より柔軟性が高まる設計になります。逆に、各コンポーネントが画面サイズに依存する定数を勝手にマジックナンバーとして保持している状態だと、座標依存のコードが偏在してしまい、レイアウト変更が容易に行えないです。
static readonly int PuyoWidth = 100;
static readonly int PuyoHeight = 100;
static readonly Vector2Int ScreenResolution = new Vector2Int(1920, 1080);
public static Vector2 GetScreenPosition(Vector2Int value)
{
return new Vector2(value.x * PuyoWidth, value.y * PuyoHeight);
}
public static bool IsInBoard(Vector2 value)
{
int x = Mathf.FloorToInt((value.x - (ScreenResolution.x / 2 - Model.Width / 2 * PuyoWidth)) / 100);
int y = Mathf.FloorToInt((value.y - (ScreenResolution.y / 2 - Model.Height / 2 * PuyoHeight)) / 100);
return (x >= 0 && x <= Model.Width - 1 && y <= Model.Height - 1 && y >= 0);
}
public static Vector2Int FromScreenToBoard(Vector2 value)
{
int x = Mathf.FloorToInt((value.x - (ScreenResolution.x / 2 - Model.Width / 2 * PuyoWidth)) / 100);
int y = Mathf.FloorToInt((value.y - (ScreenResolution.y / 2 - Model.Height / 2 * PuyoHeight)) / 100);
return new Vector2Int(x, y);
}
インスペクタからstatic変数を指定するテクニック
直前の項では、static関数を利用することの利点を述べました。しかしながら、簡単に画像の差し替えをするにはインスペクタを用いるのがやはり便利です。なぜなら、ViewはUnityの画面を確認して初めてわかる部分だからです。また、Viewはデザイナーが触ることが多いのも理由の一つでしょう。以上のことから、「インスペクタからstatic変数を設定したい」というニーズが生まれます。これを素直にUnityでやることはできないのですが、うまく誤魔化すことができます。実際にぷよぷよのサンプルコードでも行っているので以下を参照してください。
private void Awake()
{
RedPuyoSprite = redPuyoSprite;
BluePuyoSprite = bluePuyoSprite;
YellowPuyoSprite = yellowPuyoSprite;
GreenPuyoSprite = greenPuyoSprite;
PurplePuyoSprite = purplePuyoSprite;
}
[SerializeField]
Sprite redPuyoSprite;
static Sprite RedPuyoSprite;
[SerializeField]
Sprite bluePuyoSprite;
static Sprite BluePuyoSprite;
[SerializeField]
Sprite greenPuyoSprite;
static Sprite GreenPuyoSprite;
[SerializeField]
Sprite yellowPuyoSprite;
static Sprite YellowPuyoSprite;
[SerializeField]
Sprite purplePuyoSprite;
static Sprite PurplePuyoSprite;
ストリームやアニメーションの後始末をしよう
DOTweenのアニメーションやUniRxのストリームは、ゲームオブジェクトをDestroyしただけでは終わりません。
したがって、ViewでSubscribe
したストリームにはAddTo(gameObject)
などで破棄をしましょう。
DOTweenのアニメーションも事前にKill
しておきましょう。
これをしていないと、Destory
したゲームオブジェクトをもういちどDestroy
したり、アニメーションが既に存在しないコンポーネントを参照したりします。Viewにおいては特にこの問題が顕在化しやすいです。なぜなら、Viewは多くのゲームオブジェクトが生成・破棄を繰り返す場所だからです。サンプルコードで例を挙げると、それぞれのぷよは簡単に連鎖で消えたり、なぞって消えたりしています。
具体的な破棄の処理部分を下に示します。
// AddToメソッドは、ゲームオブジェクトの破棄時にストリームを破棄するためのメソッド
presenter.position.Buffer(2, 1).Subscribe(Move).AddTo(gameObject);
presenter.DeletePuyoObservable.Subscribe(Delete).AddTo(gameObject);
presenter.MatchPuyoObservable.Subscribe(Match).AddTo(gameObject);
presenter.IsSelectedObservable.Where(selected => selected).Subscribe(selected => StartSelectAnimation()).AddTo(gameObject);
presenter.IsSelectedObservable.Where(selected => !selected).Subscribe(_ => StopSelectAnimation()).AddTo(gameObject);
private void Match(Unit unit)
{
// selectAnimationにはDOTweenのSequenceオブジェクト(アニメーションを管理している)が入っている
// コルーチン内部でDestoryされるので、その前にしっかり破棄処理をしておくこと!
selectAnimation.Kill();
StartCoroutine(matchCoroutine());
}
おわりに
Unity設計の実践編は以上で完結となります。
現実的な実装のTipsを中心に紹介していったので、Unityプロジェクトへの応用も効きやすい内容だったかと思います。
逆に言えばこのTipsだけでは理論的な背景を学べるものではなく、Unity以外への応用には若干向いていないかもしれません。そのような方は、ぜひより理論的な解説であるUnity設計入門をお読みください。
また、パズルゲームはMVPと非常に相性が良いものでもありますので、Unity設計の上達を目指す人はテトリスや3マッチパズルなどいろいろなゲームで一度自力で実装してみることをお勧めします