はじめに
今回はModelの実装における注意点について書いていきます。
実際のコードについての注意点と言うよりは、Unityのツールなどの使い方について書いていこうと思います。
コードは公開しているのでそちらを見てください。
Modelの実装方針
何度も書いていますが、Modelは純粋にしましょう。ただし、「やりましょう」で何とかなるわけではないので、仕組みとして純粋性を保証するようにしましょう。今回はその手法についてのポイントを解説していきます。
まずはアセンブリを切ろう
Modelフォルダ直下にScriptsというフォルダを作成して、Scriptsの内部で、Create > Assembly Definition を選択してください。
アセンブリ定義ファイルが作成されるので、名前をModelにしましょう。
これで、Model用の新しいアセンブリが作成されます。これ以降、Asset/Project/Model/Scripts 以下のC#コードはすべてModelアセンブリとして出力されます。
次に、ModelでUniRxが利用できるようにアセンブリの参照にUniRxを設定してください。
アセンブリを切ることは強力な設定です。なぜなら、この設定によりModelはPresenter、Viewを参照することが出来なくなったからです。Modelが表示のことを気にすることが強制的に出来なくなったといっていいでしょう。
internalを使いこなそう
アセンブリを切ることの最大の利点は、internalが使えることだと思います。
internalの規則を簡単に言えば**「同一アセンブリ内ではpublic、別アセンブリからはprivate」**というものです。
MVPのようなレイヤーを意識した開発においては、これは強力なメリットを持ちます。
なぜなら、外部から呼べるメソッドや見える変数を限定することと、内部での柔軟なメソッド呼び出しを両立できるからです。
具体的に見ていきましょう。
internal PuyoBoard(int width, int height, int minMatchPuyoCount)
{
board = new PuyoBase[width, height];
CanNext = true;
Width = width;
Height = height;
MinMatchPuyoCount = minMatchPuyoCount;
}
これはPuyoBoardクラスのコンストラクタですが、これがinternalになっているということは、アセンブリの外側からは直接インスタンス生成が出来ないということです。この縛りをすることで、外部から下手にModelを操作できなくなり、Modelに書くべき処理が誤って外部に流出することが防げます。逆にModel内部からはPuyoBoardは自由にインスタンスを生成できます。したがって、publicメソッドと同じようにいろいろな操作を呼ぶことが出来るので、処理を記述できます。
このメリットは、private変数が(むやみにgetter/setterさえ書かなければ)クラス内部にのみ通用するために、private変数関連のバグの範囲を絞れたり、余計な複雑性が外部に漏れだすのを防げるのと全く同じです。
Modelだけでもテストを書こう
どんなゲームでもそうですが、パズルゲームは特に、Modelでバグが発生しやすいです。
ゲームロジック自体が複雑なので、どうしてもバグをおこしやすのです。
ここをテストしておくことは効率的だといえるでしょう。
加えて、Modelのテストは書きやすいです。
確かめるために、Unity Test Runnerを用いて書いてみましょう。
まず、Modelフォルダー直下でCreate > Tests > Tests Assembly Folder でテストフォルダを作成しましょう。
そして、新しく作成されたTestsフォルダの直下にあるTestsというadfに、Modelアセンブリの参照を追加してください。
Modelのテストはエディタ上で動けば十分なので、Platformを以下のように設定してください。
次にModel/Scripts/AssemblyInfo.csを作成します。
#if UNITY_EDITOR
using System.Runtime.CompilerServices;
[assembly: InternalsVisibleTo("Tests")]
#endif
この設定によって、Modelの内部でInternalに設定されているものを、Testsに見せることが出来ます。
ModelのテストはPlayModeを使わないので、EditModeのテストを書いてみます。
Model/Tests/ExampleTest.csを作成してください。
DeletePuyo、GeneratePuyo、MatchPuyo、MovePuyoの4つについてが一番複雑なので、バグが発生しそうですね。
中でも、MatchPuyoは再帰的なコードを書いていて、相当複雑です。
テストコード自体はGithubにあるものを参考にしてください。
では、このテストを実行してみましょう。
上のようにWindow > General > Test Runner を選択しましょう。
その後、EditModeを選択して、左上のRun Allを押します。すると、上のようにテスト結果が出てきます。
テストケースは今回はおのおの1パターンだけ書いておきます。どのようなテストケースがいいのかなどのトピックについては、私の技量不足もあるので、深入りは避けます。あまりテストに詳しくない私が、なぜこの機能を詳しく紹介したのかというと**個人開発においては忌避されがちなテストですが、本当にコスパがいいからです。**理由は以下に述べます。
- プログラミング全般で良くありますが、人間はすぐにケアレスミスをします。これは人間に気づくのは難しいです。しかし、動かしてみたら割と簡単に見つかります
- バグはテストの最初に大量に見つかり、テストが多くなっていくほどに見つかりづらくなっていきます。したがって、出来るだけさっさと動作確認をするのが大事です。
- バグは小さなプログラムほど簡単に見つかります。Model単体だと比較的楽です。むしろ、Modelのバグを抱えたまま、Presenterを書いてそこでもバグを書いてしまえば、どこがおかしいかわからなくなりますよね。
これらの理由から、MVPにしたがってアーキテクチャを分けたなら、品質保証が重要でない個人開発のゲームでさえもテストが有用なものになります。
UniRxで状態を通知しよう
ここまでは、Modelをすっきりと記述するための工夫について書きました。ここからは、ModelとPresenterの結合について書いていきます。
Unity開発において、疎結合にする方法としては「インターフェースを利用した依存性逆転の利用」「イベント・Observerパターンの利用」の2通りがメジャーです。
前者は上位モジュールと下位モジュールをロジック内でつなげるための手法、後者はイベント発行側がデータ受け取り側を全く意識しないでつなげる方法であり、利用目的が違います。
Modelの理想はただのModelとして単体で存在することであり、下位モジュールを呼ぶ形でPresenterを想定するのはあまりいい設計とは言えません。したがって、今回はObserverパターンを利用します。
この通知を実現するライブラリがUniRxです。
SubjectやReactivePropertyを使って、Model内部の状態変更を通知することが出来ます。
メッセージクラスを作ろう
UniRxのReactivePropertyやSubjectなどによって、Modelの状態を通知することが可能である、と述べましたが、実はそれだけだと微妙に表現力不足に直面することがあります。
例えば今回なら、「二次元配列の(x,y)から(x+1,y)へ移動した」と言った情報は、単純にReactivePropertyを使っても難しいです。(二次元配列であることと、「移動」を表現することがちょっと辛い)
また、単純に「盤面からぷよが消えた」と言うだけでも、「なぞって削除された」or「連鎖で消えた」では扱いが変わってくるわけですね。
もちろん、いろいろ工夫をしてUniRxのデフォルトの機能だけで何とかしても行けないことはないんですが、結構難しい(というか面倒な)こともたくさんあります。そのような場合は、Modelの状態変更を通知するだけのデータを持ったメッセージクラスを定義しましょう。
今回のサンプルプログラムでは、DeleteMessageクラスをはじめとしたメッセージクラスを作成しています。
このクラスは、Modelの外側から見れば単にIEnumerableを持っているだけのクラスです。
また、DeletePuyoクラスもまったく同様に読み取り専用のVector2Intを持っているだけのクラスです。
つまり外部からは読み取り専用のクラスです。
public class DeleteMessage
{
List<DeletePuyo> deletePuyos { get; set; } = new List<DeletePuyo>();
internal void AddDelete(Vector2Int position)
{
deletePuyos.Add(new DeletePuyo(position));
}
public IEnumerable<DeletePuyo> DeletePuyos => deletePuyos;
}
public class DeletePuyo
{
public Vector2Int Position { get; private set; }
internal DeletePuyo(Vector2Int position)
{
Position = position;
}
}
このようなクラスをIObservableとしてPuyoBoardクラスで公開しています。
Subject<DeleteMessage> deletePuyo { get; set; } = new Subject<DeleteMessage>();
Subject<GenerateMessage> generatePuyo { get; set; } = new Subject<GenerateMessage>();
Subject<MatchMessage> matchPuyo { get; set; } = new Subject<MatchMessage>();
Subject<MoveMessage> movePuyo { get; set; } = new Subject<MoveMessage>();
public IObservable<DeleteMessage> DeletePuyo => deletePuyo;
public IObservable<GenerateMessage> GeneratePuyo => generatePuyo;
public IObservable<MatchMessage> MatchPuyo => matchPuyo;
public IObservable<MoveMessage> MovePuyo => movePuyo;
複雑な情報を通知する場合は以上のように書くことですっきりと記述できます。
もちろん、メッセージクラスはただの情報を入れるだけのクラスであって、通常のクラスのように処理とデータをカプセル化するものではありません。したがって、データの処理をメッセージクラスに入れることは控えましょう。
インターフェースを活用しよう
今までのコードで、ListをわざわざIEnumerableで公開したり、SubjectをわざわざIObservableで公開しているコードがありました。これは、インターフェース活用の利点のうち一つ、外部からのアクセス方法を制限できることの恩恵を受けられるからです。
なぜなら、Listを直接公開してしまえば、Listのメソッド全部を呼べてしまいますが、IEnumerableだとIEnumerableに定義されているメソッドしか呼べないですし、SubjectをIObservableとして公開すれば、誤って外部の人間がOnNext()を実行したりすることが出来なくなるからです。
特にModelとPresenterとの接点となりうる場所では、このメリットを最大限活用しましょう。
Modelクラスで公開しよう
ここまで作ったModelをUnityから使うためには、何らかの場所でインスタンスを生成したり参照を持つ必要があります。
Modelの公開方法としては、以下のような内容のstaticクラスを作成するのが良いです
public static class Model
{
// 設定ファイルにくくりだしたくなるような内容はここにおいておく
public static readonly int Width = 8;
public static readonly int Height = 6;
public static readonly int MinMatchPuyoCount = 4;
public static readonly int MaxSelectPuyoCount = 5;
// ModelのセットアップもModel側で行う。Presenter側でやってはならない。
static Model()
{
Board = new PuyoBoard(Width, Height, MinMatchPuyoCount);
}
// 公開用の読み取り専用変数で公開
public static PuyoBoard Board { get; }
}
この方法の利点は、staticであることからシーンに依存しない点です。これはModelの特性である、Unityにも画面にも無関係であるという点と一致していますね。
加えて、ModelクラスにModelの設定項目を書いておくのもおすすめです。
これは、Modelの設定管理を一元化することにもつながります。
完全にUnity非依存でなくてもよい
Modelの純粋性について、これまでさんざん強調してきましたが、現実的にはUnityを使いたいときもあります。
具体的にはMathf
やRandom
、Vector3
などのUnityの描画とは完全に切り離しが可能なクラス群です。
実際、サンプルコードでもVector2Intを使っています。Unityのクラス群を使うことがModelの純粋性を崩さない限りは、使うことは悪いことではないでしょう。
フレームワークがUnityでなくても使えるようなロジックがModelだ、という理解でModelを分離したとしても、現実問題、別のフレームワークに移植することはそこまでありません。そもそもUnityがあればマルチプラットフォームにビルドが出来ます。したがって、ModelがUnityに依存しても、描画・画面に依存しない限りは設計上の問題にはなりません。
おわりに
ここでは、Modelにおける実装の注意点について書きました。
案外単純に書くだけと思われがちのModelですが、実は意外と注意点があります。