この記事はUnityアドベントカレンダー2021、10日目の記事です。
…がUnity要素がかなり薄い内容になってしまいました…。何卒ご容赦ください🙇
はじめに
少し前からDDDの勉強をしはじめてようやくDDDの雰囲気が分かってきたので、実践とアウトプットを兼ねてチェスを題材に一人DDDごっこをしてみました。組み上げたドメインモデルをView(今回はUnity)につなぎこんでちゃんとゲームとして遊べるところまで作ってみました。
今回はドメインモデリングやドメインとUnityのつなぎこみについての解説や得た学びについて書いていこうと思います。
ゲームを対象にDDDをして組まれたコードは世の中にあまり公開されていないようだったので、 この記事 + 併せて公開したソースコード がこれからゲーム開発でDDDをしようとしている人の助けになれば幸いです。
当方DDDについて学び始めたばかりというのもあり間違った認識をしている点もあるかもしれませんが、間違い見つけた際はご指摘いただけると大変ありがたいです…!
前提知識
この記事を読むにあたって必要な知識は以下の3つです。
- DDDについての基本的な知識
- Unityについての基本的な知識
- UniRxについての基本的な知識
ソースコード
ドメイン知識
チェスのドメイン知識についてはチェス入門を参考にしました。
簡単にチェスのドメイン知識を列挙すると以下のようなものがあげられます。
- 駒は複数種類あり、それぞれ動き方が違う(参考)
- 盤面に規定の配置で駒が並べられた状態で試合開始(参考)
- 先にチェックメイトされたプレイヤーの負け
- 自殺になる手は打てない
- いくつかの引き分けになる条件がある(参考)
- スティルメイトになったとき
- お互いがチェックメイトできる戦力を失ったとき
- どちらかが引き分けを提案して、相手がそれを受け入れたとき
- 50手の間、駒の取り合いが起こらなかったとき
- 3回、盤上が同じ形になったとき
- 以下の特殊ルールが存在する
これらのドメイン知識を元にドメインモデリングをしていきます。
ドメインモデリング
次にドメインモデリングについてです。
まず最初に全体像がこちら。
最初からこの形が出来上がっていた訳ではありません。ドメインモデリング→実装→ドメインモデリング→実装→…を何度か繰り返してこの形に至りました。
ですが、その過程を全部書いていたらすごく長いことになってしまいます🥲ので、今回は最終的に残った要素を定義したときに考えていたことを書きます。
ゲーム
チェスは2人のプレイヤーが交代で手をうっていくゲームなので、いまどちらのターンなのかをどこかに記録する必要がありそうです。
また、ゲームが進行中なのか終了したのかなどの「GameStatus(試合の状態)」もどこかしらで保持しないといけなさそうです。
ちょっと雑な命名ですがここは「Game(ゲーム)」という要素を作って上記の2つを保持させることにしましょう。
盤面とコマ
チェスには盤面があり、そこにいくつかの駒が配置されています。
「Board(盤面)」は「Game」に1つです。「Board」には「Piece(駒)」が複数あります。
「Piece」は「PieceColor(色)」「PieceType(駒種)」「Position(位置)」「IsDead(取られたかどうか)」の情報あればよさそうです。
コマの移動方法
「Piece」はいくつかの「Movement(移動方法)」を持っています。
移動方法の持つ情報としては「MoveAmount(移動量)」があればよさそうです。
ただし、ルーク・ビショップ・クイーンは特定の方向に盤面内であれば無限に進める移動方法を持っているので、「Movement」には複数の「MoveAmount」を設定できるようにするのがよさそうです。これは「Board」が8x8のマスで構成されているため、1方向への無限移動は7個の「MoveAmount」で表現できると考えたからです。
特殊ルール
チェスにはいくつかの「SpecialRule(特殊ルール)」が存在します。
「SpecialRule」自体がデータとして保持すべきものはなさそうです。こういうものってドメインモデルで定義すべなのでしょうか?🤔
それぞれ発動条件は違いますが、「Piece」を動かした後に何かしらが起きるということは共通しているのでとりあえずあげておきました。
ログ
「GameStatus」や「SpecialRule」は過去にうたれた手に基づいて発動の可否が決まるものがあります。
例えば、50手の間、駒の取り合いが起こらなかったとき引き分けになります。アンパッサンが発動できるのは対象の敵ポーンが直前に2マス移動したときのみです。
そのため、「PieceMovementLog(移動ログ)」と「BoardLog(盤面ログ)」は記録しておいた方が良さそうです。
ログの置き場所を「PieceMovementLogger(移動ロガー)」と「BoardLogger(盤面ロガー)」としましょう。
モデリング完了
そしてできあがったモデルがこちら。
このモデルをベースにエンティティ、値オブジェクト、ドメインサービスを実装していきました。コードはこちら→ https://github.com/chromee/ddd-training-chess/tree/master/Assets/Chess/Scripts/Domains 。
モデルだけ見ると綺麗な依存の流れになってるように見えますが、実際に実装してみると集約間で循環参照が発生してしまいました😇プログラミング力がまだまだ足りておらず…頑張ってプログラミング筋を鍛えていきます…。
Unityへのつなぎこみ
ドメインモデルから組み上げたコードとUnityをつないでゲームとして遊べるようにするまでについてです。
まずアーキテクチャ図がこちら。
今回はDomain層・Application層・Presentation層の3層構成にしました。層の名前はレイヤードアーキテクチャから借りました。
Domain層
ドメインモデルを元に実装したコードを置く層です。
この層のコードは基本的にテストコードを書くことを徹底しました。
Application層
Application層がドメインとUnityのつなぎこみの主役になったので、ここについては他よりも詳細に紹介します。
UseCase
Domainの操作を実行する役割のクラス群です。
粒度は諸説ありですが、今回は1操作につき1クラスにしました。
Presenter
DomainとViewをつなぐ役割のクラス群です。
Viewに反映したいDomainのパラメーターはReactivePropertyにして、Presenter内でViewに反映させます。
逆にViewの操作を受け取ってDomainを操作をするのもPresenterで行います。ここでのDomainの操作にはUsaCaseを使います。
Presenterは本当はPresentation層に置く方がいいと思ったのですが、今回は横着してApplication層に置いちゃいました。
IView
Viewの振る舞いを定義する役割のインタフェース。
依存方向を制御するため、依存関係逆転の原則を適用した結果生まれたものとも言えます。
その他
UseCaseでもPresenterでもないけど機能的に必要なクラスが色々ありました。
今回は生成したドメインインスタンスを保持するクラスや一番最初にゲームを初期化するクラスなどが登場しました。
Presentation層
主にApplication層で定義したIViewの実装クラスを置く層です。
MonoBehaviourを継承したクラスを置けるのはこの層のみです。
得た知見・学び
ドメインモデリングをするときの学び
1回のモデリングに時間をかけすぎない
すでに各所で言われていることではるのですが、1回のモデリングに時間をかけすぎず、ある程度の時間を最初に決めてその時間内でできた範囲で一回実装してみるのが良さそうです。
自分は最初モデリングに悩みすぎてモデリングだけで半日つぶれるみたいなことをしちゃってました。特に時間がかかったのは「これどう表現するのがきれいだ?」みたいな悩みでした。「これどう表現すればいいんだ?」みたいな悩みは意外とすぐ解決法が見つかるのですが、「これどう表現するのがきれいだ?」みたいな悩みは明確な答えがない分、ドツボにハマってしまったときはそこそこの時間を浪費してしまいました。
ドメイン駆動設計 モデリング/実装ガイドによると、(規模感にもよるが)モデリングはだいたい1~2時間ほどの制限時間を設けて取り組むのがいいとされていたので、だいたいそれぐらいを目安にするのがよさそうです。
ドメイン知識の精査は最初にやる
当たり前といえば当たり前なのですが、ドメインモデリングを始めるまでにドメイン知識の精査は先にやっておきましょう。自分は「チェスのルールはだいたい知ってるから大丈夫だろ~」という思考で雑にモデリングをし始めてしまったのですが、後からぽろぽろ考慮できていない仕様(特殊ルールや引き分けなど)が出てきて後から大幅に修正することになりました😇
とはいえきちんとドメインが切り出されていたので、全体としての修正はそこまで大変ではありませんでした。意図せずDDDは変更に強いということが体験できたので実践練習としてはよかったのかもしれません(?)
ドメインとUnityのつなぎこむときの学び
Presenterに全部詰め込まない
今回のアーキテクチャではApplication層にUseCaseを置きました。しかし、その気になればUseCaseを作らず全てPresenterに詰め込むこともできます。というか最初の方はそういう風に実装してました。そうしたとき、Presenterはあっという間に肥大化しますし、Application層のテストが書きづらくなるように感じました。そのため、Presenterに処理を全部詰め込むのではなくドメインの操作はUsaCaseとして切り出すなどの分離をするのが良さそうでした。
勇気をもってエンティティでReactivePropertyを使う
今回はViewに反映したいDomainのパラメーターはReactivePropertyにしました。
DomainでReactivePropertyを使いはじめたのは開発の中盤からで、序盤の方では使っていませんでした。ドメインの状態をC#組み込み型として公開して、駒を動かした後にViewに反映させるという実装にしていました。なぜかというと、DomainにReactivePropertyを使うと「これはドメインがApplication層のことを意識したつくりになっているのでは…?」というあったからでした。
とはいえ、ReactivePropertyを使ってもDomain層には大きな影響はありませんがApplication層ではスッキリしたコードが書けるようになります。なので、「これはApplication層を意識しているわけではない。モジュール(ドメイン層のこと)を外部から利用するときの利便性を上げるためにReactivePropertyを使う」という決断をしなといけませんでした。変な思い込みをしていたというだけの話ではあるのですが個人的には学びでした。
その他の学び
ゲームをCUIで操作するとしたとき必要になるコマンドは何かを考える
これはドメイン層の実装をするときの話なのですが、一回ゲーム画面のことは忘れて「ゲームをCUIで操作するとしたとき、必要になるコマンドは何か?」みたいに考えると個人的にはやりやすかったです。より具体的に言うと CUIの1コマンド = 1UseCase くらいになるようコードを組むと処理の粒度がいい具合になる気がしました。
テストは本当にすごい
Unityプロジェクトでテストをがっつり書くのは今回が初めてだったのですが、テストは本当にすごい。チェスがたまたまほとんどの仕様をドメインで表現できるゲームだったというのもあり、強烈にテストの強力さを体感できました。特に個人的にすごいと感じたのは、Viewを書かずともドメインの動作確認ができる点でした。開発効率が上がったのをビンビン感じました。テスト信者になりました。
まとめ
チェスを題材に一人DDDごっこしてUnityで動かしてみました。規模の小さいゲームの開発でも慣れるとすごく効率が上がるのを体感できました。
今回のチェスのようなコードに落とし込みやすいゲームだとDDDはうまいことやれそうなので、興味のある方はぜひチャレンジしてみてください!
おまけ
せっかくなので自分がDDDの学習する際に、お世話になった文献を紹介しておきます。
どれも読みやすくて分かりやすかったのでおすすめです。