グローバルゲームジャムでクラス設計をやったらスムーズに開発が進んだ話

  • 159
    Like
  • 7
    Comment
More than 1 year has passed since last update.

はじめに Global Game Jam(GGJ)とは

GGJとは全世界同時に行われるゲームジャムのことです。要する、世界規模のゲーム開発ハッカソンです.
プログラマ、デザイナ、プランナ、グラフィッカなど様々な役職の人をごちゃまぜに、3~8人程度のチームを組み、48時間でゲームを1つ作ろうというイベントです。

今回はGGJ2016ドワンゴ会場にプログラマとして参加し、満足行く結果が出せたのでそのことを書きたいと思います。
(要するにポエムです)

作ったゲーム

今回、GGJのテーマが「RITUAL」でした。(意味は「儀式、習慣、行事、慣例」です)
自分たちのチームは「行事」の意味に着目し、節分ゲームを作ることにしました。

こんなゲーム

  • 豆をぶつけて相手を場外に落とす
  • アイテムを拾うと武器が変わる

ダウンロードはこちらからどうぞ。

チーム編成

今回のチーム編成は、プログラマ4人、グラフィッカ2人、プランナ・デザイナ1人のチームでした。
特に、今回はプログラマが4人と人数が多いので、先に設計指針を決めないと破綻するなと思い、先にクラス設計をしてしまうことにしました。

(ちなみにゲームジャムあるあるとして、プログラマ同士でお互いに何をやってるのか知らずコンフリクトしまくる、クラス間のインターフェイスを決めてないので他人が書いたクラスに勝手に手を入れて参照関係をグチャグチャにする、ということがあります。深く考えるよりも思いつくままに実装し、最短の工数で開発するのがゲームジャムとしては正解だ、とは言われますけども…。)

クラス設計をちゃんとやってみた

自分のチームは初日(金曜日)中は企画決定・環境構築を行い、ちゃんと帰って寝よう。開発開始は土曜日朝からにしよう。という方針でした。
そこで、土曜日朝からスムーズに開発が進められるように、初日の夜中に家に帰ってクラス設計だけ先にしてしまうことにしました。

設計指針

  • Unityで開発するので、UnityEngineを意識した設計にする
  • 分担作業しやすいように、できるだけクラス間は疎結合にする
  • インターフェイスやUniRxのIObservableを使うことで依存関係を逆転させる
  • 相互依存・循環参照はさせない
  • ゲーム中のPlayer周りの処理は独立して動作し、Manager系クラスに一切依存させない
  • 「武器」が増えることはわかっていたので、実装パターンを増やしやすい形にしておく

できあがったクラス図(プレイヤ周りのみ)

クラス図はPlantUMLで書きました。さくさく書けるのでオススメです。
図は初日に設計した図ほぼそのままです。(実際は実装していくうちに微妙に変わった箇所もありますが、今回はどういう設計を先にしたかを伝えたかったので反映していません)

PlayerCore.png

PlantUML
@startuml

namespace Items{

    interface Item{

    }

    enum PlayerStatusEnum{
    }

    abstract class WeaponItem {
       + WeaponEnum
    }

    class ShotgunWeaponItem -d|> WeaponItem

    class PlayerStatusItem<<struct>>{
        + PlayerStatusEnum
    }

    WeaponItem -|> Item
    PlayerStatusItem -u|> Item
    PlayerStatusItem *-u> PlayerStatusEnum
}

namespace Player{

    interface IPlayerInput{
       + IObservable<bool> OnAttackButtonObservable
       + IObservable<bool> OnJumpButtonObservable
       + ReactiveProperty<Vector3> MoveDirectionReactiveProperty
    }

    class PlayerInput
    PlayerInput -d-|> IPlayerInput

    class PlayerCore{
        + int PlayerId
        + IObservable<Damage> OnPlayerDamaged
        + IObservable<Item> OnPickUpItem
        + ReactiveProperty<AnimationState> AnimationStateReactiveProperty

    }

    class PlayerMover
    class PlayerAnimation

    PlayerMover -> IPlayerInput

    class WeaponManager{
        - IPlayerInput
        +ChangeWeapon(enum WeaponEnums)
        +Attack()
    }


    Player.WeaponManager --u>Player.IPlayerInput 

    Player.PlayerMover -d-> PlayerCore
    Player.PlayerAnimation -d-> PlayerCore

    Player.WeaponManager -r-> PlayerCore
    PlayerCore -|> Damage.IDamageable
    PlayerCore -|> Damage.IAttacker
    PlayerCore -> Damage.Damage
    Player.WeaponManager -d> Weapons.Weapon 
    PlayerCore -> Items.Item
}



namespace Bullet{
    abstract class BulletComponent{
        +IAttacker Attacker
    }

    Bullet.BulletComponent -> Damage.IDamageable
    Bullet.BulletComponent -> Damage.Damage
    Bullet.BulletComponent *-> Damage.IAttacker
    class NormalBean{
    }
    class ExplosionBean

    NormalBean --|> Bullet.BulletComponent
    ExplosionBean --|>Bullet.BulletComponent
}

namespace Weapons{

    enum WeaponEnum{
        SingleShotWeapon
        ShotgunWeapon
    }

    abstract class Weapon{
        + IObservable<Unit> OnFinishedAsync
        + WeaponEnum WeaponType
        + Attack(vector3 direction)
    }

    Weapons.Weapon -> Weapons.WeaponEnum

    class SingleShotWeapon
    class ShotgunWeapon

    ShotgunWeapon -u-|> Weapons.Weapon
    SingleShotWeapon -u-|> Weapons.Weapon

    ShotgunWeapon --> Bullet.BulletComponent
    SingleShotWeapon --> Bullet.BulletComponent
}

namespace Damage{
    interface IDamageable{
        + ApplyDamage(Damage damage)
    }

    interface IAttacker{
        + string AttackerId
        + string AttackerName
    }

    class Damage <<struct>>{
        + Vector3 attackerPosition
        + float damageValue
        + IAttacker Attacker
    }

    Damage.IDamageable ..d> Damage.Damage
    Damage.Damage *-d-> Damage.IAttacker
}

namespace GameManager{
    class PlayerManager
    GameManager.PlayerManager o-> Player.PlayerCore
}


@enduml

クラス設計を行ったメリット

この初日のうちにクラス図を書き上げて朝に共有する作戦ですが、かなり上手くいきました

  • 全体像が見えているので、開発の進捗管理がしやすい
  • 依存関係を辿ることで、どこから手を付けるべきかわかりやすい
  • インターフェイスが定まっているので、分担で開発して後から結合が簡単にできる

などなど、メリットだらけでした。

まとめ

クラス設計、雑でもいいのでとにかく開発に入る前に書くことをオススメします。
時間が無いゲームジャムだからこそ、ちゃんと設計するべきかもしれません。
コンフリクト解消や結合時のトラブルで時間を取られ、レベルデザインに時間を割くことができずゲームジャムが終わってしまうのは本当に勿体無いですしね。


おまけ(設計の解説)

どういう形で設計したのか、軽く解説したいと思います。

分担作業しやすいように、できるだけクラス間は疎結合にする

これは、実クラスではなくインターフェイスを参照する形にするという意味です。

Input.png

例えばInput周り。このように、IPlayerInputインターフェイスを定義し、PlayerMover(移動管理コンポーネント)WeaponManager(プレイヤの装備している武器管理コンポーネント)はこのインターフェイスを参照する形にしています。

  • デバッグ用のPlayerInputDebugだけ先に実装してPlayer本体の実装を進める
  • 複数台コントローラからの入力を扱うMultiPlayerInputの実験と実装を行う

といった風に、インターフェイスを区切ることで同じ箇所をコンフリクトなく並行作業することが可能になります。

また、デバッグ時は固定キー配置のPlayerInputDebugを使う、本番ではMultiPlayerInputを使う、AIを実装したくなったらAIInputを使う、などInputの実装を差し替えるだけで様々な方法でPlayerを操作できる形にできるメリットもあります。

インターフェイスやUniRxのIObservableを使うことで依存関係を逆転させる

Input.png

これもInput周りが参考になります。
IPlayerInput自分が誰に参照されているかを知りません。 IPlayerInputはただInput入力を検知してイベントを発行するだけであり、それがどういった使われ方をするのか知りません。ただIObservableを公開しているだけであり、使いたいコンポーネントがSubscribeするだけで済むようになっています。

このように、UniRxのIObservableを用いることで依存関係を逆転させイベント駆動な処理を非常に綺麗に書けるようになります。(ただのObserverパターンだけどね)
というわけでUnity開発するならUniRx使えばいいんじゃないかな。

相互依存・循環参照はさせない

クラスが相互依存・循環参照していると、初期化のタイミングで死んだり、動作確認が難しくなったりといいことがありません。可能な限り避けるべきです。

クラス図全景を見てもらうとわかるかと思いますが、矢印を辿ってループする箇所がありません。

ゲーム中のPlayer周りの処理は独立して動作し、Manager系クラスに一切依存させない

Manager

(WeaponManagerはManagerって名前だけど実際はPlayerに張り付いてるコンポーネントなので気にしないでください。WeaponManagerって名前の付け方が悪かったです)

このように、Player本体であるPlayerCorePlayerManagerを知らなくていいようにしてあります。
「プレイヤは自分を管理するマネージャの存在を知らない。マネージャがプレイヤを監視し、プレイヤが死んだらIObservable経由でそのことを検知する」という形になっています。

こうすることで、Managerが存在しないデバッグシーンにPlayerPrefabを直接配置し実行してもエラーなく動作させることができます。

おまけのまとめ

  • インターフェイスはすごく使える便利な子
  • SOLID原則を意識して設計すると上手くいく
  • UniRxはいろいろ使えて便利