自己紹介
この記事はCA Tech Lounge Advent Calendar 2023の17日目の記事になります。
CA Tech Lounge2期生 ゲームクライアントエンジニア志望の薄衣 毅です!
普段はバンタンゲームアカデミーという所でC#やC++、UnityやUE5などを学んでいます。
今回は先日までうけていたGame Client College~設計編~のまとめもかねてレイヤードアーキテクチャを使ってゲームを作ったことについて記事を書きます。
そもそもどうしてレイヤードアーキテクチャを使おうと思ったのか
今年に入ってからクラス設計に興味を持ち、個人開発やチーム開発で設計をやってきました。個人開発ではMVPパターンなどのデザインパターンを使ってそこそこうまく出来ていたのですが、チーム開発ではとりあえず動くけど設計に統一感をもてず微妙な感じになることが多々ありました。
近々またチーム開発を行うこととそこで使えるようなちゃんとした設計を学びたいと思っていたので挑戦してみました。
レイヤードアーキテクチャとは
ものすごくざっくりいうと、
「クラスを同じ関連のあるもので層に分けて」「その層どうしが一方通行になっている」アーキテクチャです。
まずクラスを同じ関連のあるもので分けることに関してですが、レイヤードアーキテクチャには以下の表のようにクラスを4層に分けるという考えかたがあります。
POSAのLayerパターンとDDD由来のレイヤードアーキテクチャで層の分け方は変わるらしいです。今回はDDDの話でよく出てくる4層レイヤーにしてみました。
層 | 役割 |
---|---|
プレゼンテーション | UIの部分 |
アプリケーション | ドメインを操作してあれやこれやする |
ドメイン | このプロジェクトの中心となる部分、必要不可欠な存在 |
インフラストラクチャ | データベースや他の層に当てはまらないもの |
インフラストラクチャ層
主にデータベースなどが当てはまります。ゲームでいうと敵の動きや技やパラメータ、セーブデータとかです。Singletonの基底クラスなどどこでも使える基底処理もここになります。
ドメイン層
「ビジネスモデルの表現を行う」層です。そのプロジェクトにおいて必要不可欠な存在を表すものが当てはまります。マ〇オブラザーズでいうところのマ〇オと敵です。こいつらがいないとゲームが成り立ちません。
アプリケーション層
ドメイン層を使ってあれやこれやします。例えばマ〇オが死んでいるかを判定するクラスやその判定を用いてゲームの状態を管理するクラスなどが挙げられます。
プレゼンテーション層
UI処理の役割を持ちます。ゲームでいうとスコアやテキストを表示する部分です(MVPパターンを知っているならViewの部分と言えばしっくりくると思います。)。
以上がレイヤードアーキテクチャの基本的な層の分け方です。そしてこの層は上から下に依存関係が向いています。
下から上に向くことは基本ありません。
これがレイヤードアーキテクチャの基本的な考え方です。
インフォメーション
同じ層内では依存関係は持てます
メリット
・関心の分離ができる。
「関心」とはそのプログラムが何をしたいのか、どういう役割をもっているのかということです。よって「関心の分離」とは一つのプログラムが持つ役割は一つにするということです。
例えば「プレイヤーはこの入力で動くようにする」としたときにそのままプログラムを書くと入力処理と移動処理が合わさることになります。その場合、後から入力処理を変更しようとした際に関係ない部分が多くなりどこに目的の処理があるのか分かりづらかったり、別の部分でも同じような入力処理をしているなど非効率な部分が増えてしまいます。
しかし入力処理と移動処理を分離しておけば途中から仕様が変わったとしても変わった部分を探しに行くのが楽になり、他の部分でも使いまわすことができるようになります。
デメリット
・ドメイン層の仕様が「大幅に」変わるとほぼ書き直しになる
これはレイヤード特有とは言えないかもしれませんが、ドメイン層が大きく変更されると上の層は下の層に依存しているので影響が広がります。
・コードを書く量が増える
一つのクラスで書けるような処理を分割して書く必要がでるのでモックの開発や短期間での開発には向かないです。
実際にクラスを層に分ける
今回は練習もかねて簡単なランゲームを作りました。
上から流れてくる障害物を避けながらスコアを稼ぐゲームです。
インフラストラクチャーレイヤー
- 障害物がどのように流れるのかのデータ(スクリプタブルオブジェクト)
- 入力処理
インフラストラクチャーレイヤーにはデータベースやどこの層にも属さない共通的なシステム処理や入力処理を選出しました。
ドメインレイヤー
- プレイヤー
- 障害物
- スコア
- 障害物の動きのクラス
- プレイヤーの動きのクラス。
ドメインレイヤーには「このゲームに必要不可欠なもの」とそれに付随する振る舞いのクラスを選出しました。
アプリケーションレイヤー
- スコア
- 当たり判定
- ステート
- 敵生成
- マネージャ
アプリケーションレイヤーには「ドメインレイヤー(Model)を管理するもの」を選出しました。マネージャではおもにステート(タイトル、インゲーム、リザルト)の管理のみをおこなっています。
プレゼンテーションレイヤー
- タイトルの表示処理とアプリケーション層とのつなぎこみ全般
- インゲームの表示処理とアプリケーション層とのつなぎこみ全般
- リザルトの表示処理とアプリケーション層とのつなぎこみ全般
プレゼンテーションレイヤーには「表示における関心事」に関連するものを選出しました。ステート単位で表示処理のクラスを作っています。
結果
※説明の都合上インゲームのみ
最終的なクラス間の関係はこのようになりました。各層は上から下に関係を持っており、ところどころ間にインターフェースをかませて依存性逆転を行っています。
これにより各クラス間の関係が明確になり、あらたに機能を拡張する際にどこを見ればいいかがわかりやすくなりました。また、インターフェースをかませたことで下位層の変更による影響を抑えられるようになりました。
反省点
微妙な点としてはGameManagerが派生先のステートに依存してしまっているところですね。本当ならすべてインターフェースを経由して行いたかったのですが、初めて使ったVContainerという依存性注入ライブラリに引っ張られてしまいました。
ここに関してはあとで修正したいと思っています。
※追記
敵のパラメータや移動の処理をインフラストラクチャー層に書いたのですが、これは「ドメイン層」にあてはまるものではないかという指摘を受けました(修正済み)。
これら「ふるまい」はDDDでいうEntityやValueObjectになります。
インフラストラクチャー層は厳密には「上位のレイヤを支える一般的な技術的機能」なのでこれらの「ふるまい」はこの層には当てはまりません。よって「ドメイン層」が正しいです。
参考