Open8
すいかゲームのクローンを設計する
![0y0](https://res.cloudinary.com/zenn/image/fetch/s--XJSd89lE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/a1277f2d85.jpeg)
趣旨
某ゲームのクローンを作ります。
![0y0](https://res.cloudinary.com/zenn/image/fetch/s--XJSd89lE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/a1277f2d85.jpeg)
実装は以下のリポジトリにあります
![0y0](https://res.cloudinary.com/zenn/image/fetch/s--XJSd89lE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/a1277f2d85.jpeg)
Scripts
概要
オニオンアーキテクチャ+MVPパターンで設計しました
- 各層でAssembly Definitionを定義しています
- 下位層から上位層を参照することはできません
- リアクティブプログラミングのためにUniRxを利用しています
- 非同期処理のためにUniTaskを利用しています
- DIのためにZenjectを利用しています
- UIアニメーションのためのDOTweenを利用しています
Domain層
- 目的
- ゲームのルールやロジックを中心に据えたビジネスロジックを実装
- 依存関係
- どこにも依存しない(UniRxやUniTaskへの依存は許可)
- 主な責務
- アイテムのエンティティを生成
- アイテムのマージ条件を判定
- スコアを計算
- 構成要素
- Interfaces:Domain ServiceおよびInfrastructure層のインタフェースを定義
- Services:Domain Serviceを実装
- ValueObject:Domain Modelを実装
- Entity:Domain Modelを実装
UseCase層
- 目的
- ドメイン層のロジックを活用し、ユースケースごとの処理をまとめる
- 依存関係
- ドメイン層のサービスやリポジトリ(インターフェース経由)を利用
- 主な責務
- プレゼンテーション層からのリクエストを受け取り、適切なドメインロジックを実行
- 構成要素
- Interfaces:UseCaseのインタフェースを定義
- UseCases:ServiceやRepositoryを利用してUseCaseを実装
Infrastructure層
- 目的
- 外部データソースや永続化の管理
- 依存関係
- ドメイン層のモデルやサービスを利用
- 主な責務
- ゲームスコアの保存や読み込み
- サウンドやリソースのロード
- 構成要素
- Repositories:Domain ModelやDomain Serviceを利用してリポジトリを実装
- Services:外部APIなどを利用してServiceを実装
- SODefinitions:スクリプタブルオブジェクトを定義(後述)
Presentation層
- 目的
- UI操作やゲームの見た目を管理
- 依存関係
- Domain層/UseCase層を参照
- 主な責務
- Interfaces:Viewのインタフェースを定義
- Presenter:Presenterを実装(UseCase層とViewのバインド)
- View:Viewを実装(MonoBehaviourの継承許可)
- State:各画面状態の定義とそれに対応する状態管理ハンドラを実装
- DTO:DTOを実装(PresenterからViewにUseCaseのデータ変換して渡す)
- SODefinitions:スクリプタブルオブジェクトを定義(後述)
Installer層
- 目的
- 依存関係の注入。
- 構成
- Zenjectを利用して各層の依存を解決
![0y0](https://res.cloudinary.com/zenn/image/fetch/s--XJSd89lE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/a1277f2d85.jpeg)
シーン
各シーンは以下のような要素を持っています
※DIするMonoBehaviourなスクリプトおよびPrefabは実行するまでヒエラルキーに表示されません
※DIする非MonoBehaviourなスクリプトは実行してもヒエラルキーに表示されません
各シーン共通
プロジェクトコンテクスト(レイヤー”Default”)
実行時にDIされDontDestroyOnLoadになる
- インストーラ
- オーディオBGN
- 常に再生
- オーディオSE
- ユーザインタラクションに応じて再生
- インプットイベントハンドラ
- キーボード入力を検知してストリームソースを生成
タイトルシーン
メインカメラ(レイヤー”Default”)
レイヤー”Default”のオブジェクトを映す
UIカメラ(レイヤー”Default”)
レイヤー”UI”のオブジェクトを映す
バックグラウンドページ(レイヤー”Default”)
- 背景イメージ
タイトルページ(レイヤー”UI”)
- ボタン×4
- メインシーンに遷移
- 詳細なスコアランキング表示
- 設定表示
- ライセンス表示
設定ページ(レイヤー”UI”)
- スライダー×2
- BGM音量
- SE音量
- 名前表示/入力ボタン
- 押下すると名前入力モーダルを表示
- バックボタン
- 押下すると前の画面に戻る
スコアランキングページ(レイヤー”UI”)
- ボタン×1
- 前の画面に戻る
ローディングページ(レイヤー”UI”)
シーン遷移の際に表示される
- ロード画面イメージ
名前入力モーダル(レイヤー”UI”)
- インプットテキストフィールド
- 名前確定ボタン
- 押下するとモーダルを非表示にする
ライセンス表示モーダル(レイヤー”UI”)
- スクロールビュー
インストーラ(レイヤー”Default”)
- タイトルシーンインストーラ
シーンコンテクスト(レイヤー”Default”)
イベントシステム(レイヤー”Default”)
メインシーン
メインカメラ(レイヤー”Default”)
レイヤー”Default”のオブジェクトを映す
UIカメラ(レイヤー”Default”)
レイヤー”UI”のオブジェクトを映す
ステージ(レイヤー”Default”)
- 箱
- マージアイテムとの接触判定を持ち、あふれるとゲームオーバー条件を満たす
- マージアイテム
- マウスを動かしたときに移動
- クリックしたときにマージアイテムを落下、SEを再生
- 同じ種類のマージアイテムが接触するとスコア加算、SEを再生
バックグラウンドページ(レイヤー”Default”)
- 背景イメージ
メインページ(レイヤー”UI”)
- マージアイテムパネル表示(次回に選ばれるアイテム)
- スコアパネル
- スコアランキングパネル
- 左右キーでカテゴリをスクロール
スコアランキングページ(レイヤー”UI”)
- ボタン×1
- 前の画面に戻る
ローディングページ(レイヤー”UI”)
- ロード画面イメージ
ポーズモーダル(レイヤー”UI”)
- ボタン×3
- メインシーンに遷移
- リスタート
- 現在のゲームに戻る
ゲームオーバーモーダル(レイヤー”UI”)
- ゲームオーバー時のスクリーンショット表示
- スコアランキング表示
- ボタン×3
- リスタート
- スコアランキングページ表示
- タイトルシーンに遷移
インストーラ(レイヤー”Default”)
- メインシーンインストーラ
シーンコンテクスト(レイヤー”Default”)
イベントシステム(レイヤー”Default”)
![0y0](https://res.cloudinary.com/zenn/image/fetch/s--XJSd89lE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/a1277f2d85.jpeg)
ModelとViewのバインド
UniRxを用いたMV(R)Pパターンにより、PresenterがModelとViewをバインドします。
UseCase層に実装したクラスをModel、Presentation層のViewに実装したクラスをViewとしました。
- UseCase層
- IReadOnlyReactivePropertyを公開
- Presentation層(View)
- IObservableを公開
- Presentation層(Presenter)
- IReadOnlyReactivePropertyとIObservableを購読
![0y0](https://res.cloudinary.com/zenn/image/fetch/s--XJSd89lE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/a1277f2d85.jpeg)
スクリプタブルオブジェクトの扱い
スクリプタブルオブジェクトはどの層からもアクセスできますが、オニオンアーキテクチャに従うのであればインフラ層からアクセスするべきだと思います。
現状は、SODefinitionsというスクリプタブルオブジェクトを定義するディレクトリがインフラ層とプレゼンテーション層に存在します。後者はViewで表示したい文字情報を定義するために使用されており、インフラ層を介さずプレゼンテーション層から直接アクセスしています。インフラ層→ユースケース層→プレゼンテーション層という流れが冗長に感じたためこのような仕様になっています。
このあたりは改善の余地があります。
![0y0](https://res.cloudinary.com/zenn/image/fetch/s--XJSd89lE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/a1277f2d85.jpeg)
例外処理の実装方針
層 | 責務 | 例外処理 |
---|---|---|
ドメイン層 | ビジネスルールを表現する | - ビジネスルール違反時に例外をスロー - 入力の検証エラー時に例外をスロー |
インフラ層 | 外部システムやデータソースとのやり取りを抽象化する | - 技術的なエラーをキャッチして再スロー - エラーをログに記録 - 必要に応じてフォールバック処理を実行 |
ユースケース層 | ドメイン層とインフラ層を統合し、アプリケーションロジックを提供する | - ドメイン層やインフラ層から発生した例外をプレゼンテーション層に伝播 - プレゼンテーション層が処理可能な例外を提供 |
プレゼンテーション層 | UI操作やアプリケーション全体の動作を制御する | - ユースケース層からの例外をキャッチしてログ記録や通知を実施 - フェイルセーフ機能を実装 - 再試行ロジックを実装 |
![0y0](https://res.cloudinary.com/zenn/image/fetch/s--XJSd89lE--/c_limit%2Cf_auto%2Cfl_progressive%2Cq_auto%2Cw_70/https://storage.googleapis.com/zenn-user-upload/avatar/a1277f2d85.jpeg)
画面状態管理
ステートパターンを用いて、以下のような仕組みでページとモーダルの状態を管理しています。
- PageState / ModalStateを定義
- PageState:シーン内のPage(メイン画面)のバリエーション
- ModalState:オーバーレイ的に表示されるModal(サブ画面)のバリエーション
- Dictionaryによる状態ごとのハンドラクラス実装
- 各ハンドラクラスは「その状態になったときにView(画面)でどのような表示・非表示を行うか」を実装
- UIイベントに応じた状態変更
- Presenterではユーザー操作やアプリのイベントが発生したときに該当のPageStateやModalStateに切り替え
- ReactivePropertyを使った状態変更の検知と画面切り替え
- PresenterではPageStateやModalStateが変更されるたびにUpdatePageStateUI() または UpdateModalStateUI()が呼ばれる