1.6K Views
July 22, 26
スライド概要
サークル内で実施した講習会の資料を外部公開向けに再編したものです。
ゲームプログラマのための 設計講習会 > Architectural Design for Game Development 2026/07/22 #設計講習会 1
# なぜ設計が必要なのか #設計講習会 2
- 競プロのような書き捨てのコードで設計が必要になることはまずない - Web系(フロントエンド/バックエンド)などでも 普段から複雑な設計を意識して作っている人は少ないはず - なぜゲーム開発の文脈で「設計」という話が広く用いられがちなのか? #設計講習会 # # なぜ設計なのか 3
- 現代のフロントエンド開発では、 ほぼ確実に何らかのフレームワークを用いる - それぞれのフレームワークには強力な設計思想があり、 ユーザーはそれに従ってコードを書く - フロントエンドの責務はユーザー向けのUIであり、 そもそも役割自体がそこまで大きくない - 俗に言う「JSON色付け係」 #設計講習会 # # Webフロントエンドの場合 4
- 基本的にはフロントエンド向けに HTTPなどでAPIを用意するまでが責務になる - ドメインロジックが集約される箇所であるため、 要件によっては複雑化しがち - 適切なAPI設計が必要 - DBを利用する場合、スキーマの設計も重要 - とはいえそこまで大きな流れは変わらない APIを用意し、DBを操作するところまでは概ね同じ #設計講習会 # # Webバックエンドの場合 5
- 要件が非常に複雑、何なら途中からの変更さえあり得る - 一般的なサービスの設計とは異なり、そもそもドメインロジックのレイヤーから不安定 - ジャンルによって作り方が大きく異なる - 形式化ができないのでノウハウが暗黙知になりがち - ゲームエンジン(フレームワーク)が適切な設計を提供しない - 対応すべき範囲が広すぎるため、汎用的にならざるを得ない - 作るゲームに応じてユーザーが独自の設計をエンジン上に築くことを前提としている #設計講習会 # # ではゲーム開発は? 6
- 複雑怪奇なゲーム開発に立ち向かうための 手段として「設計」が必要 - Web文脈で培われたドメイン駆動、 Clean Architecture、MVほにゃららなどの 概念をUnityでも活かす試みが行われた ref: https: / / #設計講習会 # # ゲーム開発における設計の必要性 learning.unity3d.jp/6159/ 7
- この講習会では - SOLID原則 - DI/DIコンテナ - ドメイン駆動設計 - アーキテクチャ - これらの話を、**しません** #設計講習会 # # 本日の話 8
- 抽象的な話をいきなりしてもあまり意味がない - もちろん知識として知っていて欲しい話ではあるし、役に立つ話でもあるが - 雑な理解で実践されるとかえって逆効果になりかねない - 本当に必要なのは、設計という考え方に至るまでの道筋 - 既にスパゲッティな設計にDIコンテナを持ち込んでも何も解決しないように、 アーキテクチャなどの知識だけ持ち込んでも何も解決はしない - 設計原則は適切に実装したら自然とこうなる、という形の言語化であって 無理に全てに適用するための道具ではない 9 . . . #設計講習会 # # Why?
- エントリーポイントと制御フロー - GameObjectと適切なクラス分割 - ModelとViewの境界 - より良い設計を目指すために #設計講習会 # # 改めて本日の話 10
# エントリーポイントと制御フロー #設計講習会 11
- アプリケーション起動時に最初に呼ばれる関数などは 「エントリーポイント」と呼ばれる - 例: C言語のmain関数 - プログラムの処理のフローを追う際には 必ずここが起点となる - 問題はゲームエンジンの場合、 エントリーポイント自体が不明瞭なことが多い #設計講習会 # # ゲームとエントリーポイント 12
#設計講習会 # # エントリーポイントが明確な例 (1) 13
#設計講習会 # # エントリーポイントが明確な例 (2) 14
- MonoBehaviourのAwake/Start/Updateあたりの イベントがエントリーポイントに相当する - 適当にGameObjectを置いてコンポーネントを 貼れば動いてhappy? - 問題は、コンポーネントごとに際限なく エントリーポイントを増やせてしまうこと #設計講習会 # # Unityの場合 15
- それぞれのStart/Updateが独立しているうちは そこまで問題にならない - ゲーム全体にまつわる処理や他のComponentの 処理に依存し始めたとき、崩壊が始まる - 内部状態を初期化する処理がGameManagerに あるためPlayerが単体で動作しない - なぜか1フレーム待つと動く - コードがスパゲッティ化し、制御フローを追いきれない事態に #設計講習会 # # 制御フローの爆発 16
- 複数のManagerクラスが勝手に状態を初期化・更新するような事態は避ける - 必ず単一のエントリーポイントを経由して初期化処理を呼び出す - 外部の状態に依存する処理が必要なら、それを受け渡すメソッドを公開して外部から叩く - UpdateContent(FooData)みたいなのを用意する、など - 決してStart/Updateを使うな、という話ではない - 内部で完結している(外部への副作用がない)分には何も問題ない - 処理が外部のGameObjectなどに依存する場合の話 #設計講習会 # # エントリーポイントを乱立させない 17
- 相互に呼び出しを行うような設計はなるべく避ける - 複数のManagerが相互に依存しているくらいなら、 GameManagerに集約した方が100倍マシ - メソッド呼び出しの方向を一方通行にする - UIなら必ず外部からデータを受け取る、自発的に 他のオブジェクトにアクセスしに行かない - Playerが直接GameManagerのメソッドを叩いて ゲームの状態を操作しない #設計講習会 # # 命令を下す側・下される側を区別する 18
- 複数のManagerクラスが乱立し、好き勝手に 初期化処理を行なっている - 処理の流れがめちゃくちゃ - Startの実行順次第で壊れる可能性もある #設計講習会 # # 悪い例: 複数のManager 19
- 初期化処理を単一のエントリーポイントに まとめる - メソッド呼び出しの方向が GameEntryPoint で統一されている その他 20 > - #設計講習会 # # 良い例: 単一のエントリーポイント
# GameObjectと適切なクラス分割 #設計講習会 21
- OOPにおけるコードの単位はクラス - 単一責任原則に則り、大抵の場合は 責務ごとにクラスが分割される - というのが、教科書的な話 #設計講習会 # # クラスを分割する 22
- クラスを分割すると当然コードが分散し、認知負荷が上がる - GameManagerに全部書いてあった方がわかりやすい!という 初心者の意見は軽んじられがちだが、確かに一理ある - クラス分けが適切でない場合、かえって追いづらいコードが出来上がる - Unityの場合はどうなる ? - UnityはGameObject中心のコンポーネント指向 - 教科書的なオブジェクト指向を当てはめるだけでは不十分 23 . . #設計講習会 # # もう少し深掘りしてみる
- Unityの世界は全てGameObjectが中心 - シリアライズと一体化した設計 - コンポーネント単位の分割・カプセル化 - Prefabによる高い再利用性 - 良くも悪くもツールに最適化された仕様 - MonoBehaviourひとつ使うにも GameObjectが付いてくる - エンジン側で管理されるため、 初期化・更新のタイミングを細かく制御できない #設計講習会 # # GameObjectとコンポーネント指向 24
- Unityで作る以上、設計はGameObjectを1つの単位として捉えるべき - GameObjectごとの責務を明確にする - GameObject単位・コンポーネント単位で適切にカプセル化する - GameObjectが必要ないなら、MonoBehaviourである必要も特にない - これは純粋なC#クラスとして分離する - パズルのコアロジックにGameObjectを使う必要はない、ですよね? #設計講習会 # # 捉えるべきはGameObject 25
- Clean Architectureではコンポーネントの凝集性・結合性という概念が登場する - ここでのコンポーネントはUnityのそれではなく、単体でデプロイ可能な単位を指す - Unityでコンポーネントの切り分けを考慮するときにもこの考え方は重要 - 難しい単語を使ってますが、要はいい感じに分割するための指標の話です #設計講習会 # # 凝集度を高める・結合度を下げる 26
- 下のような設定画面のUIの実装を例に考えてみる #設計講習会 # # 例: 設定画面のUI 27
#設計講習会 # # 悪い実装例 28
- 表示以外の処理が含まれている - PlayerPrefsから値を取得・更新している - ゲーム内の音量を直接書き換えている - 配下のuGUIを直接参照として抱えている - もし設定のスライダの構造が変わったら? - Reset/Closeの呼び出しがコードから見えない - 今回はInspectorのOnClickに直接設定されている想定 #設計講習会 # # 悪い実装例: 単一のクラスに詰め込む 29
- Sliderと数値のTextを 単独のコンポーネントに切り出す - 内部の構造はカプセル化する - 公開するのは内部値の更新を行う UpdateValueと変更通知用の OnValueChangedのみ - (なお、OnValueChangedはRxで Observable化した方が扱いやすいので お勧め) #設計講習会 # # 改善1: VolumeSliderとして分離 30
#設計講習会 # # 改善2: UI以外の責務を分離 31
- UIとしての責務以外を全て削除 - 子オブジェクトの詳細は全てカプセル化 - UI以外の必要な部分はイベントとして公開 - データの更新処理などはUIの外で行う #設計講習会 # # 改善2: UI以外の責務を分離 32
- GameObjectはシーン上で単独で動作する1単位 - 可能な限りそれ単体で動作するように責務を分離する - テスタビリティのためにもこれは重要 - GameObjectを基準にカプセル化を心がけると綺麗になりやすい - 外部から隠蔽されている範囲で密結合していてもあまり問題ない - 複雑なViewなどは内部で下手に疎結合化させる方が良くない #設計講習会 # # GameObjectとカプセル化 33
# ModelとViewの境界 #設計講習会 34
- 設計のことはじめとして「ModelとViewを分離しよう」という話がある - 所謂MVPやMVVMのようなアレ - 一般的なGUIアプリケーションの場合、この二つは概ね綺麗に分離できる - しかし、ゲームだとそうはいかない #設計講習会 # # ModelとView 35
- そもそもModelとViewとは何か - Modelとはアプリケーションの中心となる データやロジックをまとめたもの - ViewはModelの状態を画面等に表示するもの - これらは独立していることが多いため、 分離することで変更の波及を抑えられる #設計講習会 # # なぜModelとViewを分離する? 36
- ゲームの場合、ドメインロジックそのものにViewが影響することが多い - 物理挙動で動くアクションゲームのプレイヤーはModel?View? - 常にゲームループが回っているため、リアクティブな変更を前提とした MVx系統のアーキテクチャと相性が悪い - そもそも普通のGUIアプリケーションは毎フレーム値が変わったりしない - 必ずどこかで密結合を妥協しなければならない #設計講習会 # # ゲームにおけるModelとView 37
- ゲームロジックがUnityの機能を必要としないジャンル - パズルやRPG、シミュレーションなど - コアロジックを綺麗に純粋なC#として分離できる - UI周り - UIは純粋にViewとして振る舞うことが大半 - これはちゃんと分離させておきたい #設計講習会 # # いつ・どこで分離をすべきなのか? 38
- 誤解している人を時々見るが、Modelが純粋なC#クラスである必要は全くない - 別にMonoBahaviourがModelでもいい - Unityに依存するタイプのプロジェクトならむしろその方が自然ですらある - Unityの場合Viewは概ねMonoBehaviourになる - uGUIのコンポーネントでも良いし、そうでなくてもいい - 複雑なViewの場合、先述の通り単一のコンポーネントとして まとめてカプセル化しておくと良い #設計講習会 # # Modelの定義・Viewの定義 39
- ModelとViewは分離されているため、お互いの存在を知らない - これを繋ぐための方法はいくつかあるが、今回はMVPパターンを紹介する #設計講習会 # # ModelとViewを繋ぐ 40
- Model-View-Presenterパターン - Presenterという仲介役を用意し、 ModelとViewを繋ぐ ref: https://qiita.com/toRisouP/items/ 5365936fc14c7e7eabf9 #設計講習会 # # MVPパターン 41
- 右のようなHP表示のUIを考える - プレイヤーがとげに当たるとHPが減る - HPの変更をUIに反映させる #設計講習会 # # 例: プレイヤーのHP表示 42
- 移動・ダメージ判定を持つ Playerコンポーネント - 普通のUnityのコンポーネントだが、 HPのデータを持つModelとも見れる - OnHpChangedで変更を通知する - プレイヤーの表示を行うという意味では Viewの役割も兼ねている、とも言える - ここはUIとは異なり、分離する必要性が 薄いので密結合していても問題ない #設計講習会 # # Model: Player 43
- HPの数だけアイコンを表示するView - アイコンはPrefabとして用意しておく - Horizontal Layout Groupで 子要素が自動で並ぶようにしている - 外部に公開するのはUpdateValue メソッドのみ #設計講習会 # # View: HpView 44
- PlayerとHpViewを繋ぐPresenter - 中身はStart関数でイベントの 購読をするだけ - これをCanvas等の適当なGameObjectに アタッチする #設計講習会 # # Presenter 45
- Presenterがいなければ「ModelとViewが完全に分離される」のがポイント - Model/Viewそれぞれを単体でテストできる - 片側だけ差し替えることも容易 - Presenter自体は状態やロジックを持ってはいけない - あくまでModelとViewを繋ぐだけの薄い層 - 動作がPresenterに依存するような状況は絶対避ける #設計講習会 # # MVPパターンのポイント 46
- C#の素のevent構文は若干使いづらい - 購読の解除が非常に面倒 - イベントの合成ができない - Reactive Extensions(Rx)を用いると イベント周りを綺麗に記述できる - 昔はUniRxが活躍していたが、 現在は後継のR3を利用する #設計講習会 # # [発展]Rxを使う 47
# より良い設計を目指すために #設計講習会 48
- 今日扱った内容は、設計の基礎となる考え方であり、 より良い設計を目指すための前提知識 - とりすーぷさんの設計レベルで言うと1-2くらい - ここでは少し背伸びをして、やや高度な設計のための技術や考え方を紹介 #設計講習会 # # より良い設計を目指すために 49
- クラスの振る舞いを抽象化するための言語機能 - 機能を抽象化し、実装の差し替えを容易に - Unity非依存のC#コードを書くようになると ひたすら使うようになる - あえて今日話さなかったのは、 Unity依存のコードだと恩恵が薄いから - 依存性逆転とセットで覚えよう #設計講習会 # # interfaceと抽象化 50
- オブジェクト間の依存関係を解決してくれる 便利なやつ - 利用する型をあらかじめ登録しておき、 自動で生成・解決をしてくれる - Unityだと現在はVContainer一択 #設計講習会 # # DIコンテナ 51
- コードの規模が巨大になってくると、 MVP等の分類では不十分になってくる - Domain、Presenter、Infrastructureなどの 役割ごとにレイヤーを区切る - 依存方向を限定するため、Unityだと .asmdefが用いられる - 個人開発でここまで複雑な設計が必要なケースは稀 中期~長期プロジェクト向けの話 #設計講習会 # # アーキテクチャの概念 52
# おわりに #設計講習会 53
- 設計は一朝一夕で身につけられるものではない - 知識としてではなく、経験として学ぶことが大事 - 今日学んだ考え方を実践しながら理解していこう - 設計に正解はない、作るゲームに合わせた設計ができるようになろう! #設計講習会 # # おわりに 54
# Thank you for listening! #設計講習会 55