はじめに
毎年恒例のグローバルゲームジャム。今回も参加してきたのでそのまとめを書きます。
- 2016 -> グローバルゲームジャムでクラス設計をやったらスムーズに開発が進んだ話
- 2017 -> グローバルゲームジャムでクラス設計をやった話2017
- 2019 -> グローバルゲームジャムでクラス設計をやった話2019
グローバルゲームジャムとは
GGJとは全世界同時に行われるゲームジャムのことです。ようするに、世界規模のゲーム開発ハッカソンです。
プログラマ、デザイナ、プランナ、グラフィッカなどさまざまな役職の人をごちゃまぜに、3~8人程度のチームを組み、48時間でゲームを作ろうというイベントです。(前回のコピペ)(前回のコピペ)(前回のコピペ)
今回も「ヒューマンアカデミー秋葉原会場」に参加しました。
ゲームの概要
今年のテーマ
今年のテーマは「REPAIR」でした。シンプルにわかりやすいテーマでいいですね。
これをお題としてゲームを1つ作っていきます。
作ったゲーム
今年作ったゲームは「R.R.R. (Remove Reuse Repair)」というタイトルです。
ゲーム解説
- 最大4人まで遊べるco-opゲーム
- 真ん中におもちゃのパーツが流れてくる
- パーツを回収し、レシピ表のおもちゃを作り直して出荷する
- レシピのおもちゃのパーツがすべて揃っていたら点数アップ
- どれだけ多くスコアを稼げるか
という内容のゲームです。
自分が作ると毎回4人対戦になるのはお決まりのパターンです。
このゲームを作ることになった経緯
- パーツを組み立てて、変なオブジェクトが作られるのをみたい
- オーバークックが名作なのでそれっぽい感じのシステムだと面白そう
- 妖精のおもちゃ工場っていいんじゃない
- パーツを組み立てて、レシピとおりのおもちゃを作って出荷させよう
- 対戦もいいけど、協力も面白そう
といった話を2時間くらいした結果、この形になりました。
使ったもの
- Unity 2019.3.0f6
- UniRx
- UniTask
- Extenject
プロジェクトファイルの管理は「Unity Collaborate」を使いました。
Unity Cloud Build も裏で実行していましたが、結局使いませんでした。
ソースコード
メンバ
- プログラマ 4人(日本人3人、外国人1人)
- デザイナ 2人
- プランナ 2人(1人デザイナと兼任)
の計7人でした。
実際の開発スケジュール
金曜日
- チームビルディング
- 企画会議
- 要件定義
ここまでを金曜日の24時ごろまでに完了させました。
そのあと、いったん解散して帰れる人は帰って寝るように指示を出しました。
この隙に自分は一度帰宅し、深夜にクラス設計をします。
土曜日
家でクラス設計を行い、プリントアウトしてから寝ます。
だいたい寝たのは3時くらいでした。
そして朝からひたすら実装をしていきます。
いろいろ割愛しますが、22時くらいにはほとんどの機能は実装が終わっていました。
あとはデザイナーのリソースが上がってくるのを待つ、といったところで解散しました。
日曜日
日曜日はプログラマの手が若干空き気味でした。
上がってきたリソースをどんどん組み込みつつ、バグっぽいところを潰していく作業を繰り返していました。
全体を通して
作るゲームの規模も小さく、また非常に優秀なプランナさんがいたおかげでかなりスムーズに開発が進みました。
徹夜をする必要もなく、かなり余裕をもって開発することができ精神的な余裕も大きかったです。
クラス設計2020
では、今年もどのようなクラス設計をしたのかを解説します。
1.要件定義
まず金曜日の24時くらいまで何を作るかをFixします。
この時点で決まった仕様は次のとおりです。
- 目的「パーツを集めてレシピのおもちゃを作って点数を稼ぐ」
- ゲームシステム
- 制限時間制(ゲーム中に変更不可)
- プレイ人数は1-4人(1人でも遊べる)
- スコアを多く稼ぐ
- ベルトコンベアでパーツが運ばれてくる
- 中央にトレイがあり、パーツを入れる
- プレイヤーが「出荷ボタン」を押すと出荷される
- 出荷が完了した時点で点数が加算される
- プレイヤー
- 移動
- 掴んで運ぶ/離して置く
- 出荷ボタンを押す
- 敵からダメージを受けて一定時間動けなくなる
- 敵
- プレイヤーに嫌がらせをする
- プレイヤーは敵をもつこともできる
- ロボット掃除機
- 地面に置かれたパーツを消去する
- 犬
- プレイヤーが触れると一定時間動けなくする
- プレイヤーに嫌がらせをする
- パーツ
- 種類がいくつかある
- それぞれに素点がある(マイナス値は無し)
- 金のパーツや壊れたパーツなどがある(クオリティ)
- パーツは地面に置いておくことができる
- 出荷されると消える
- オーダー
- パーツの組み合わせ(レシピ)の表
- オーダーを満たすとコンボボーナスがもらえる
- パーツのクオリティでさらにボーナスが増える
- 全体のフロー
- メニューシーン
- プレイ人数を選ぶ
- バトルシーン
- 結果表示はオーバーレイするだけ
- メニュー → バトル → 結果表示 → メニュー で1サイクル
- メニューシーン
正直、毎年似たようなゲームを作っているのでそれと同じ感じになりました。
この時点でたぶん土曜日中にほぼ実装は終わるだろうなという感想でした。
(実際、土曜日中に実装が終わった)
仕様書
今回はプランナーの方がとても優秀な方で、かなり詳細なリソースリストを作ってくれました。
そのため設計や実装が非常にやりやすくとても助かりました。
(作ってくれたリスト。めっちゃ助かった。)
追記:上記の資料のすべてを金曜日の段階で作ったわけではありません。3日間通して最終的に作られた資料一覧が↑です。
2.設計指針
去年同様、設計の方針を立てます。
- 分業を意識して1コンポーネント1機能に抑える
- 実装が必須なクラスのみをクラス図に書く
- ただしUI周りなどパターン化している部分は省略
- クラス間の関係性が維持されているなら、実装はどれだけ汚くても許容する
- 「妥協するべき点」と「設計上守るべき部分」をハッキリ区分する
- 後述
- DIを使う
- UniRxやUniTaskはどんどん使う
今年はメンバーの1人が設計についてかなり質問を投げてきました。
どのような内容の質問をしてきたかは後にまとめます。
3. 完成したクラス図
作ったクラス図がこちらです。
去年と比べると、登場人物が少ないので結構コンパクトに収まりました。
4.設計の順序
では、どのように設計していったかを解説します。
I.要素を洗い出す
毎回やっていますが、登場人物をまず洗い出します。
概念を列挙して、それぞれに名前空間を定義します。
まず、わかりきっているものを挙げて名前空間を切っていきます。
- プレイヤー Players
- 敵 Enemies
- おもちゃのパーツ Parts
- ステージ Stages
- ゲームのマネージャ群 Managers
これでも十分ですが、もうちょっと概念を切り分けます。
- 持って運ぶの概念を扱う Holders
- ダメージの概念を扱う Damages
これらも追加します。
「レシピ」をどこで扱うかを迷ったのですが、とりあえずこのままいくことにしました。
II. わかりやすいところから埋める
まずは予想がすぐできる「Damages」と「Holders」を先に埋めました。
まずHoldersですが、「このオブジェクトはプレイヤが持ち運べる」ということを表すインタフェースを定義します。
それが Holders.IHoldable
です。
「掴む」というメソッドがbool TryHold()
になっているのは、掴むことに失敗する可能性もありうるだろうなということでこうなっています。
次にDamagesですが、「ダメージという概念そのもの」と「ダメージを受けることができる」を定義します。
今回は「プレイヤを一定時間、操作不能にする」という効果なのでDamage
の中身はただのfloat
値になっています。
じゃあなぜ最初からただのfloat
値にせず、わざわざDamage
オブジェクトを定義したのかというと理由があります。
それは「今後の仕様追加でプレイヤーに対する効果を追加する可能性がある」という予想があるからです。
もちろん予想ですので、取り越し苦労の可能性はあります。
が、float
値を型1つでラップするだけでその仕様追加に簡単に対応できるのであるならば、この選択は全然アリだと判断します。
III. 敵を定義する
つづいて敵を定義します。
(ロボット掃除機のクラス名が「Roomba」なの、ヤバい)
まず敵の基底クラスBaseEnemy
を定義し、その派生としてRoomba
とDog
を定義します。
そしてBaseEnemy
はIHoldable
を実装しているため、「敵」のオブジェクトはプレイヤが持ち運べるという意味がここで表現されています。
また、犬はプレイヤーに対してダメージを与えることができます。
ただ今後、もしかしたら「犬」が他のオブジェクトにダメージを与える可能性も考慮します。
ここではさっき定義したIDamageApplicable
を経由してプレイヤにダメージを与えることにしておきます。
チームメンバーから出た質問「HoldableBehaviour」を定義しちゃだめなのか
質問
ここでチームメンバーから質問がでました。
「持ち運べる」という処理が頻発するならば、「HoldableBehaviour」という基底クラスを定義した方が楽ではないか、というものです。
つまり、こういう形になぜしないのかという質問です。
この設計は自分としてはNGです。理由としては、「is-aの関係を満たしていないから」です。
継承は「本質的に同じ概念のオブジェクトをまとめるものであり、派生クラスは基底クラスと完全に置換可能でないといけない」というルールがあります。
今回は「持ち運ぶことができる」という「性質」についてを扱っています。
これは「本質的に同じ概念であるか」というとNOであり、単に振る舞いの1つを表しているだけにすぎません。
これを基底クラスとして定義することを許してしまうと、「共通した機能は雑に基底クラスにまとめてしまおうwww」ということになりかねません。これは設計の崩壊を招く可能性があり大変危険です。
また今回はIHoldable
だけですが「じゃあここにIDamageApplicable
も載せれば共通化できて便利じゃん」と話が膨らむ可能性は大いにあります。
(いろいろグチャグチャになりはじめた最悪なパターン)
こうなってくると基底クラスはどんどん膨らみ、本来は別個の処理が1つのクラスに集中してしまい身動きが取れなくなっていきます。
SimpleとEasyは違う、ということをしっかり意識しないとすぐに破綻を迎えます。
ではどうするべきか
基底クラスは使ってはいけない。だが頻発する処理を何度も実装はしたくない。
こういう場合は「委譲」を使います。
簡単にいうと、「共通した処理を切り出した別のクラスを作ってそれを呼び出すようにする」というだけです。
こうすると基底クラスが膨らむこともなく、処理の共通化を行うことができます。
IV. パーツを定義する
閑話休題。続いてパーツを定義していきます。
特に特筆することはありません。
Part
はピュアな構造体として定義されており、それを1つだけ保持するPartObject
というGameObject
が定義されています。
V. プレイヤーを定義する
プレイヤーを定義します。
といってもこの辺は毎年やっているので、例年どおりといった感じです。
プレイヤーの制御に必要なコンポーネントを列挙して、それぞれの関係性をつないだだけです。
VI. ステージを定義する
続いてステージを定義します。
- パーツを放り込んで出荷するときに使うエリア「
AssemblyArea
」 - ベルトコンベア「
BeltConveyor
」 - 出荷ボタン「
ShippingButton
」
をそれぞれ定義します。
ここで妥協点が1つあります。「ShippingButton
がIHoldable
を実装している」ということ。
これは「プレイヤーの掴むというアクションと、ボタンを押すというアクションを1つの操作にまとめてしまいたい」という欲求から来ています。
本来、ShippingButton
は持ち運び不可能なオブジェクトです。
しかしそこに「掴む」というイベントを流用したいがためにIHoldable
を実装してしまうという形になっています。
別にインタフェースを切ろうかとかなり悩んだのですが「設計的な正しさと実装時のややこしさ」を天秤にかけた結果ここは妥協しました。
VII. マネージャクラスを定義する
今回はクラス数が少なくて余裕があったので、マネージャ周りの設計もしました。思いつく限り、必要そうなマネージャを列挙しています(実際これだけでは足りませんでしたが)。
「オーダー」の概念はどこで扱うか迷い、ScoreManager
に付随する形で定義することにしました。
(このときまだオーダーという呼び名が決まっていなかったので、仮にComboSet
という名前にしてある)
完成
以上でクラス設計は終わりです。
今回はかなり規模が小さいため、あっさり実装が終わりました。
これはクラス図を印刷して進捗管理をしていた写真です。
土曜日の15時くらいの写真ですが、このときですでに全体の8割ほどは実装が完了していました。
もらった質問など
設計や実装のやり方で、いろんな人から質問をいただくのでこのタイミングで回答しておきます。
Q. プロジェクトファイルの管理はどうしているの
自分は「Unity Collaborate」をゲームジャムでは推しています。
理由としては次のとおり。
- Unity Editorに統合されているので追加インストール不要
- データのPush/Pullさえできればいいならこれで十分
- Gitはデザイナやプランナがわからない場合にコストが高い
- どうせGitを使ったところでブランチ切った開発しない
Unity Collaborate、本来は有料機能なのですが、GGJ向けにUnityの人がライセンスを発行してくれることがあります。
自分はこのライセンスがあるならUnity Collaborateを使いますが、無いなら諦めて Git + GitHub で開発します。
Q. UniRxを入れると初心者プログラマがついてこれないのでは
ゲームジャムでUniRxの凝った使い方はほとんどしません。
ReactiveProperty
とSubscribe()
の使い方さえわかればなんとかなるパターンが大半です。
そのため30分も学習すれば問題なく使える範疇ですので、自分はUniRxを導入しています。
また金曜日の夜中に自分が設計している間は他のプログラマーの手が空きます。
このタイミングで自分が過去に書いたUniRxの資料を渡し、目を通しておいてと頼んでいます。
Q. ゲームジャムで事前に設計するなんて聞いたことない
実際やっているのは自分くらいでしょう。ただ事前に設計することはいろいろメリットが大きいのでかなりオススメです。
- 事前に規模感が把握できる
- インタフェースが決まっているので結合時に揉めない
- タスクを振って分担作業しやすい
- 具体的な作業指示が出しやすい
- 全体の進捗を可視化できる
ただし注意点もあります。
- 設計をミスると負債を抱えることになる
- 一度決めた仕様がブレない、という前提が必要
- 金曜日の24時ごろまでに仕様がfixしないと厳しい
Q. ZenjectなどのDIフレームワーク、ゲームジャムに必要なのか
実際Zenjectは難しいですが、メリットはかなり大きいです。
まず、SerializeField
の設定漏れを防止することができます。
またシーンをまたいだデータの受け渡しや、ScriptableObject
の読み込みなどもZenject経由にするとかなり楽になります。
ちなみにゲームジャムにおいてはInstaller
の定義はほとんど必要ありません。SceneBinding
とZenAutoInjector
を使えばUnity Editor上の操作のみでだいたい解決します。あとはZenjectSceneLoader
あたりも便利なので使っていますが、これもそんなに難しくはないです。
結論としては「便利なのでどんどん使おう。機能を絞って使えばそんなに難しくはない」です。
Q.設計がうまくできない
こればかりは経験と勘、としか言いようがありません。
コツとしては「クラス間の関係性を中心に書く」です。
特にUnityの場合は「実装によってかなりブレる部分」というのがあります。
例:「プレイヤーがどうやって目の前のパーツを拾うのか」
このような処理は、「OnTriggerEnter
を使う」「CircleCast
を使う」「相手の座標から計算して探す」などいろいろ実装方法があります。
ですがこのへんはやってみないとどれが最適なのかわからないですし、のちの調整で変わる可能性もあります。
そのため必要最低限の「クラス間の関係性」に絞って、「どこでどのような処理が実行されるのか」、かまでは踏み込まないほうが書いていて混乱しません。
Q. どうやってタスクを分業しているのか
- 誰がどう実装しても同じような実装になる部分
- 構造が決まっている構造体やクラスの定義
- 設計済みのインタフェース定義
- どんな実装であれ要件を満たすなら問題ない部分
- プレイヤーの移動処理
- オブジェクトの生成処理
こういう部分からまず分業して実装をしていきます。
逆に次のような部分は分業せず、誰か1人に任せたほうがいいです。
- 多くのクラスを用いて処理を行う部分
- ゲーム終了時に点数計算して表示する部分
- シーンの初期化と終了部分
Q. テストって書いたほうがいいの
書ける部分があるなら、書いたほうがいいです。けどわからないなら無理に書かなくてもいいです。
今回は1箇所、点数計算ロジックだけテストが書けそうだったので書いています。
Q. 設計/実装で妥協する部分とそうでない部分の違いがわからない
設計時や実装時に、「面倒だけどちゃんとしたコードにするか」「サボって楽なコードにするか」を迷うことがあるでしょう。
自分はここの指標は次のようにしています。
- それを許した結果、あとに負債となるなら絶対にNG
- クラス名と一致しない処理はそのクラスに実装してはいけない
これだけです。あとはケースバイケースです。
まとめ
- ゲームジャムは学びが多いのでぜひ参加してほしい
- 技術的なスキルよりも、コミュニケーションの面で学びがとにかく多い
- プロジェクトの進捗管理などを経験したいプランナーなどにもお勧め
- 設計はこだわるところと、妥協すべき点をハッキリするのが大事
- チームプレイなので、周りのケアも大事