Edited at

グローバルゲームジャムでクラス設計をやった話2019


はじめに

去年はお休みしましたが、今年もゲームジャムに参加してきたのでそのまとめを書きます。

過去の資料はこちら。


グローバルゲームジャムとは

GGJとは全世界同時に行われるゲームジャムのことです。要する、世界規模のゲーム開発ハッカソンです.

プログラマ、デザイナ、プランナ、グラフィッカなど様々な役職の人をごちゃまぜに、3~8人程度のチームを組み、48時間でゲームを1つ作ろうというイベントです。(前回のコピペ)(前回のコピペ)

今回は「ヒューマンアカデミー秋葉原会場」に参加しました。


今年の概要


今年のテーマ

今年のテーマは「WHAT HOME MEANS TO YOU」でした。要約すると「あなたにとってのHOMEとは何か」ですね。


作ったゲーム

作ったゲームは「Psycho Mothers」というタイトルです。

動画はこちら


  • 最大4人まで対戦できるゲーム

  • 2分間でこどもを家にたくさん集めた人の勝ち

  • アイテムが出てきて妨害ができる

という内容のゲームです。

(なんか自分がゲーム作ると毎回4人対戦ゲームになるな…)。


このゲームを作ることになった経緯

「HOMEとはなにか」→「こども達が帰る場所がHOMEだ」→「じゃあこどもをたくさん集めれば点数になるんだな?」→「お母さんたちが子供を家に集めるゲームにしよう!」

という、緻密なロジックに基づいてこの方針でゲームを作ることになりました。

(議論中にチームメイトのフランスの方が「psycho-pass mother...」とボソっとつぶやいたことからこのタイトルになりました)。


使ったもの


  • Unity 2018.3.2f1

  • UniRx 6.2.2

  • Zenject 7.3.1

プロジェクトファイル一式をそのままGitHubに公開しています。ダウンロードしてUnityで開けばそのままゲームとして動きます。


メンバ


  • プログラマ3人(自分含める。全員日本人)

  • アーティスト1人(中国人)

  • サウンドアーティスト 1人(フランス人)

  • プランナ 1人(中国人)

今年はなんとチームメンバーのうち、半分が外国人の方でした。

そのため日本語を使わずに英語で企画を決める必要があり、なかなか初っ端からハードでした。


クラス設計2019

ということで、今年はどのようなクラス設計をしたのかを紹介していきます。


1. 要件

企画段階でFixしたのは次のとおりです。


  • ゲームの目的「 子供を家に集める

  • 最大で4人対戦できる

  • プレイヤができること


    • 歩く

    • こどもを誘導できる

    • アイテムを拾う → アイテムを保持する → 使う


    • アイテムの影響を受ける



  • こどもができること


    • プレイヤから逃げる

    • ステージの角から登場する

    • アイテムの影響を受ける



  • ルール


    • 通常のこどもは1点、見た目の種類がいくつかあってもいい

    • 金のこどもは点数が高い

    • 1試合2分(変更不可)




  • アイテム


    • プレイヤが拾って好きな場所に配置できる

    • アイテムの種類は次


      • 爆弾(吹き飛ばす)

      • バンパー(触れると弾かれる)

      • 砂場(足が遅くなる)


      • ビッグアーム(子どもをつかめる) しれっとボツになった






  • 全体の流れ


    • メニュー画面


      • プレイヤ人数を選択できる

      • ステージや時間は変更できない

      • そのままゲーム画面に移る



    • バトル画面


      • 結果表示はシーンを分ける必要はない

      • 結果で点数と勝者を出す



    • メニュー → バトル → 結果表示 → メニュー で1サイクル



今年は2017年と比べて、かなりシンプルな内容に抑えました。

というのも、2017年の企画は最後まで完遂できず妥協する場面があったからです。

そのため今年は2017年よりも要素を削りかなり控えめな企画で収めました。


結果として、最終日の11時にはほぼ完成しているという、かなり余裕をもった開発ができました。


2.設計指針

普段の業務や趣味プロダクトにおける設計と、ゲームジャムにおける設計とでは最適解が異なります。そこを意識して設計指針を立てます。


  • 分業しやすくするために単一責任原則を遵守し、1クラス1機能に抑える

  • クラスや構造体はできる限りUML図に列挙する



  • クラスの依存関係を満たし、要求する動作をするならば 実装がどれだけ汚くても許容する


    • (オブジェクトプールは無理に使わなくてもよい、GC Allocも多少は許容する、など)




  • 「設計的な正しさ」と「実装の手間」を比較し、コストパフォーマンスがよい方を選択する


    • (switch文を使った処理分岐が一番手っ取り早いなら迷わず使うべき)

    • (あとは簡単な実装の方が学生のプログラマさんに任せやすいから)



  • 用語で混乱しないようにする(ユビキタス言語を決める)


  • 安易なstaticフィールドは禁止


  • Zenjectを使ったDIを利用する


  • UniRxを使う


  • コルーチンとUniTaskは分かる方を使えばよい(そもそも非同期処理がほとんど登場しない)



3.完成したクラス図

完成したクラス図がこれです。

なお、このクラス図はGGJが終わったあとに、改めて整えて清書したものです。

GGJ本番時はもうちょっと雑な図でやってますが、設計内容はほぼ同一です。

(結局実装しなかったクラスが入ってたりするけど、ほぼ同じ)


4.設計の順序


I.要素の洗い出し

まずは「何を作るべきか」を洗い出します。

そこで登場する「概念」を列挙し、名前空間を先に定義していきます。

今回は次の7つの要素を定義しました。


  • プレイヤからの入力を司る「 Inputs

  • 自機を定義する「 Players

  • 自機が拾うモノを表す「 Items

  • 自機や子どもに影響を与える 「ActionEffects

  • プレイヤが任意の場所に配置できる「 Traps

  • 子どもを定義する「 Kids

  • 子どもを最終的に集める「 Houses

今回、一番重要なのはItemsActionEffectsTrapsの定義です。

企画を議論している時には、みんな「アイテムを拾える」「アイテムを使う」「アイテムを配置できる」「アイテムに触れると吹っ飛ぶ」など呼んでいました。


ですが、これはどう考えても 「アイテム」という1つの単語に複数の概念が押し込まれている状態でした。そこで、ここから概念を分離したものが上の3つとなります。


  • プレイヤが拾って持ち運べる、フィールド上に落ちているモノが「Item

  • 持っているItemを消費して、地面に配置できる妨害オブジェクトが「Trap


  • Trapに触れることで、プレイヤや子どもに対して与える影響そのものが「ActionEffect

このように概念を分割しておくことで、用語による混乱を避けることができるようになります。

また、同じActionEffectをパラメータを変えて別のTrapで使う、などの使いまわしもできるようになり後半のレベルデザインで役立ってきます。


Ⅱ. Inputs周りを埋める

まずはInputs周りを埋めていきます。

IInputEvetnProviderは「プレイヤからの入力」を管理するインタフェースです。具象クラスを定義しない理由は、「入力周りは実装の差し替えができると後で楽だから」です。

インタフェースを切っておけばデバッグ中はキーボード操作を行い、実ビルド時はゲームパッドを使う、といった対応もコンポーネントの差し替えだけで実現できるようになります。


Ⅲ. TrapsとItems周りを埋める

TrapsItems周りを埋めていきます。ItemsTrapsの関係をどうするか最後まで悩みましたが、「ItemsTrapsに依存する」という形にしました。

Itemはフィールドに落ちており、自機が拾えるモノという定義です。

このItemはこれ以上抽象化する必要も、派生させる必要もないため単体のコンポーネントで十分です。

TrapEntityは「Trapがフィールド上で振る舞うときの基底クラス」として定義しています。

実際の挙動はそれぞれ派生クラスに定義します。


IV. ActionEffectsを定義する

続いてActionEffectsを定義します。

ここには、「自機や子どもに対して与える影響」そのものを定義します。

まず、ActionEffectという基底クラスを定義します。このクラスを派生させ、「型」でどんな効果をもつかを定義します。

なお、本来はActionEffectに「自機や子どもへ及ぼす効果(AddForceするなど)」を直接定義した方がオープン・クローズ原則的にはよい設計となります。


ですが少し考えてそれを実現する上手い実装方法が思いつかなかったのと、switch文(実際はパターンマッチ)を書く手間を許容したほうがトータルで楽になる、と判断したためこうしました。

そこで、IActionEffectAffectable(もっとマシな名前ありそう)というインタフェースを定義しました。

このインタフェースの実装にActionEffectsをわたし、受け取った側でActionEffectsを解釈してその効果を実現するという形式になっています。


V. Playersを定義する

続いてPlayersを定義します。ぶっちゃけ前回と方針は変わってません。

(Playerと直接関係がない要素は省略)

「自機ができること」を分解し、それぞれをクラスに分けました。

特筆すべき点は、「PlayerActionEffectAffecterPlayerMoverの関係」と「Item周りの取り扱い方」です。

PlayerActionEffectAffecterActionEffectを受け、その影響をPlayerMoverが受ける」という関係になっています。

PlayerMoverが直接IActionEffectAffectableを実装しない理由は、移動以外に影響を及ぼすActionEffectがあとから増える可能性があるからです。

Itemの扱いはそれぞれ、「拾う処理」「持ってるアイテム描画処理」「使う処理」に分離しました。

内容的には1つのクラスにベタ書きしても十分なのですが、あえた分けた理由としては、分業しやすくするためです。

このような、他のパッケージとの境界になるコンポーネントは、相手パッケージの実装待ちが発生するため、継ぎ足しで実装されていく可能性が高いです。

そうなると、1個のクラスを複数人が操作してコンフリクトする可能性が非常に高くなります。

コンフリクトを避けるためにも、細かいながらもクラスは分けて定義しました。

あと、しれっとManagers.TrapGeneratorというものが増えています。

これは単に「Trapを生成するファクトリクラス」だと思ってください。


VI. Kidsを定義する

続いて、Kidsも定義します。基本はPlayersとほぼ同じです。

1点、挙げるとしたらIInputEventProviderをあえて使ってみたというところです。

これは特に深く考えて無く、「まぁ、いけるだろ(何が)」くらいの気持ちで使ってます。


VII. Houseを定義する

最後にHouseを定義します。

KidsCoreを参照するだけで終わりです。


VIII. まとめる

最後にこれらをまとめて1つの図にします。

最終的なクラス図は次のとおりになりました。


実装してみて

実際にクラス設計して開発を行いましたが、やはり設計は重要だなという感想になりました。

進捗管理が重要なゲームジャムにおいて、進捗が見えにくいプログラムの完成度を可視化できるのが非常によいです。



(実際に開発時に使っていた印刷されたクラス図。どこまで作ったかチェックをして進捗管理していた。)

「全体のどこまで開発ができており、次にこれを作らないといけない」が可視化され共有できるため、かなり精神に余裕を持って開発することができました。


また、土曜日の夕方に「ブラックホール」ってアイテム作ろうぜ!となり、急遽アイテムを追加することになりました。

これはBlackHoleTrapEntityを追加実装するだけでアイテム追加が完了しました。

ItemPlayers側に変更作業がほぼ無い)


まとめ


  • 設計はゲームジャムにおいてかなり有効

  • ただしチーム内での合意はあらかじめとっておくこと

  • 企画がブレると設計の意味がなくなる点に注意

  • ゲームジャムは時間が重要。設計で頭を抱えるくらいなら実装が楽な方法を迷わず選ぶべき。

プロジェクトファイル


つづき