はじめに
作るもの
第1章で作成したアプリケーションをModelとViewに分離することを目指します。
とりあえず分離したもの
Model
スコア計算部分は確実にModelです。
public static class NineNumberModel
{
public static int CalculateScore(int accordCount, int discordCount)
{
return accordCount * discordCount;
}
}
View
ボタンの色を変えたり、スコアを書き換えたりする処理は確実にViewですね。
また、ボタンのクリックの座標を取得するのもViewの役割であることは明らかでしょう。
public class NineNumberView : MonoBehaviour
{
private NineNumberPresenter presenter;
private Image[] buttons;
private Text score;
void Start()
{
presenter = new NineNumberPresenter();
var one = transform.Find("1").GetComponent<Image>();
var two = transform.Find("2").GetComponent<Image>();
var three = transform.Find("3").GetComponent<Image>();
var four = transform.Find("4").GetComponent<Image>();
var five = transform.Find("5").GetComponent<Image>();
var six = transform.Find("6").GetComponent<Image>();
var seven = transform.Find("7").GetComponent<Image>();
var eight = transform.Find("8").GetComponent<Image>();
var nine = transform.Find("9").GetComponent<Image>();
buttons = new Image[] { one, two, three, four, five, six, seven, eight, nine };
score = transform.Find("Score").GetComponent<Text>();
}
// 「選択状態」という論理的な画面状態を、「Color.red」などの具体的画面描画に書き換える処理
void ChangeNumberStatus(NumberStatus[] numberStatuses)
{
for(int i = 0;i < numberStatuses.Length; i++)
{
switch (numberStatuses[i])
{
case NumberStatus.AccordSelected:
buttons[i].color = Color.red;
break;
case NumberStatus.DiscordSelected:
buttons[i].color = Color.blue;
break;
case NumberStatus.notSelected:
buttons[i].color = Color.white;
break;
}
}
}
// 「スコア」という論理的な状態を具体的な画面描画に書き換える処理
void ChangeScore(int score)
{
this.score.text = $"{score}";
}
void Update()
{
// とりあえずマウス座標の取得はこう書かざるを得ない
Input.mousePosition;
}
}
// ボタンの選択状態を表す列挙型
public enum NumberStatus
{
notSelected, AccordSelected, DiscordSelected
}
問題となる処理
理論編では、演出用一次変数、画面依存情報、低レベルの入力情報、などが問題になるという指摘をしました。
このレベルのシンプルなアプリケーションでも十分に問題の種を確認することが出来ます。
今回は、入力から1点、出力から1点を考えてみましょう。
画面の選択状態はどちらに置くのか(色がついているかいないかの判定情報)
出力に関する情報です。ボタン情報をどの程度ゲーム全体で流用していくのかによってModelか否かは変化するものですが、少なくとも「色があるかないか」というのは特定の画面でしか使えない情報です。
まさに、理論編その1での「Viewでの条件判断(複雑性)を引き起こすが、特定の画面でしか使えない情報」という条件を満たしたデータですね。
Modelに入れてみる
public static NumberStatus[] Statuses = new NumberStatus[9];
static void Clicked(int clickedButtonIndex)
{
// Statusesを数えてAccord/Discordを判断する
// それによってStatuses[clickedButtonIndex]の中身を書き換える
}
void Update()
{
// クリックを判定してModelに対して「Clicked(クリックされたボタン番号);」を呼ぶ
// Viewは毎フレームModelの値を監視する
ChangeNumberStauts(NineNumberModel.Statuses);
}
こうなると、Modelに対して、特定の画面でしか使えない情報が大量に紛れ込むのが分かると思います。実質的にこのModelはViewと言っていいのです。なぜなら、Viewから呼ばれることだけを想定した「NineNumberModel.Statuses」というフィールドがあるからです。このフィールドは、NineNumberViewがないと使えません。また、Viewの変化によって、Statusesの構造は変わります(例えば、ボタンの数の変化など)。つまり、依存関係が悪い方向に逆転しているのです。そもそもpublic staticは実質的なグローバル変数であり、特定の場所でしか使えない情報をカプセル化せずにグローバルに公開するのは最悪です。
Viewに入れてみる
実装は以下のようになるでしょう。一瞬で想像がつくと思いますので、若干雑なサンプルコードになっています。
NumberStatus[] Statuses = new NumberStatus[9];
// ボタンを押すたびにStatusesを更新する
// ボタンを押すたびにStatusesを参照して、それによってifで分岐して、ボタンに反応するかしないかを決める
// 結局のところいろいろModelでやっていた処理を全部Viewで一元管理するイメージです
これだと、Viewが非常に複雑なのが分かると思います。また、Viewの網羅的なテストをするためには、長時間Unityを起動したり、デバッガでいちいちMonobehaviourの値を観察したりする必要が生まれます。なかなか辛い実装と言わざるを得ないです。
ボタンクリックの判定をどちらに置くのか
入力に関する情報です。処理行数から考えるとViewには入れたくないですが、Modelに入れても辛いことは経験的に容易に想像がつく人も多いのではないでしょうか。
Modelに入れてみる
実装はこのようになります。
// 「マウス座標」という具体的な画面の情報を、「クリックされたボタン番号」という論理的な画面のデータに書き換える処理
static int DetectClickPosition(Vector2 mousePosition)
{
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
return 1;
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
return 2;
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
return 3;
}
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
return 4;
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
return 5;
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
return 6;
}
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
return 7;
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
return 8;
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
return 9;
}
return -1;
}
void Update()
{
int detectedPosition = NineNumberModel.DetectClickPosition(Input.mousePosition);
// この後どう書くかは選択状態をViewとModelのどちらが持つかによって異なる
}
こう書くことで、ややこしい処理をUnityに依存しないstatic関数に追い出すことが出来ました。Model側に書いたのでテストも簡単ですね。また、Viewも確かにシンプルですね。しかし、このModel、特定の画面にがっつり依存しています。なぜなら、「画面のボタン座標」を知っているからです。この画面じゃないと使えない情報を知っているので、これは実質的に依存です。「依存とは単に参照することだ」と考える場合もありますが、**現実のアプリケーション開発では違います。**参照がなくても別のレイヤーの知識をマジックナンバーなどを用いてエスパーしていたらそれは依存です。
したがって、このModelの再利用は実質的に不可能であり、この設計ではModelとViewを分離しているとはいいがたいです。
また、このような画面に依存するModelを作り続けていると、「1つのModelに10画面分の処理が入っているから、下手に触ると画面が壊れる」などのこの世の地獄が発生してしまいます。
Viewに入れてみる
実装はこのようになります。
void Update()
{
int clickedPosition = DetectClickPosition(Input.mousePosition);
// この後どう書くかは選択状態をViewとModelのどちらが持つかによって異なる
}
// 「マウス座標」という具体的な画面の情報を、「クリックされたボタン番号」という論理的な画面のデータに書き換える処理
int DetectClickPosition(Vector2 mousePosition)
{
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
return 1;
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
return 2;
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 940 && mousePosition.y >= 740)
{
return 3;
}
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
return 4;
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
return 5;
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 640 && mousePosition.y >= 440)
{
return 6;
}
if (mousePosition.x <= 760 && mousePosition.x >= 560 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
return 7;
}
if (mousePosition.x <= 1060 && mousePosition.x >= 860 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
return 8;
}
if (mousePosition.x <= 1360 && mousePosition.x >= 1160 && mousePosition.y <= 340 && mousePosition.y >= 140)
{
return 9;
}
return -1;
}
さて、Update()でのその後の処理は、画面の選択状態をModelで管理しているのか、Viewで管理しているのかによって話が変わってきます。
-
選択状態がModelで管理されている場合:Update()では、Modelに対してクリックされたことを通知するだけ。残りはModelがいい感じに処理するように設計する。
→この場合は、結局ModelがViewに依存する問題が悪化します。ここまでくると、Modelの実質的な役割がViewになっている悪い設計と言わざるを得ないです。 -
選択状態をViewで管理している場合:クリック状態から「既に選択状態のものを押したのか」「まだ選択していなかったものを押したのか」の分岐を行い、結果によって画面を書き換える。そして、スコア計算だけはModelを使う。
→Modelは極端にスリムになって、スコア計算しか残りませんでしたね。素晴らしいです。Modelは理想形になりました。
しかし、この場合、Viewが非常に大きくなります。ここまでViewが大きいと、ほとんど第一章の実践編で紹介したViewとModelが密結合であるアプリケーションと同じです。
加えて、いずれの場合にも、座標が絡んだ場合、どんなにややこしい処理でも、Unityを起動するオーバーヘッドや、手作業でテストする苦行をしないと動作を確認できないという問題が存在しています。やはり、悪い設計を言わざるを得ないです。
おわりに
今回焦点を当てたModelとViewの仲介処理は、理論編でも述べたように、ModelとViewだけでは扱いが難しいことが実際のコードからも分かりました。その2ではそれを解決するためのMVXの方法論について書いていきます。
また、今回はModelをstaticな関数として用意しましたが、Modelの用意方法にもいろいろ考えられますね。
このあたりの論点については、続編のUnity・MVP設計実践にて詳しく解説していきます。