はじめに
Weaponsは今年度の学祭で展示した私を含め5人ほどのメインメンバーで制作したダクソ風アクションゲームです。
UEを使用したチーム開発をするにあたって、ゲームの設計の面でも色々と考えることが多くありました。
今後チーム開発をする人のために情報を残しておこうと思います。
目指したこと
「各々が各々のアセットで作業を行い、最後にそれらを組み合わせるとゲームが完成している」
という状態を目指して設計を行いました。
これを目指した主な理由は以下の3点です。
- UEをGitで管理するときコンフリクトの処理が面倒
- 忙しい人が多かったので密な連絡の取り合いが困難
- BPに慣れていないプログラマがいる
複数人が一つのアセットを触るような設計にしてしまうと、当然コンフリクトの可能性が高まりますし、互いの実装部分に手を入れることも多くなるので密な連携が必要になります。
また、BPについての知識の差があると互いの実装の意図を伝えるのにも時間がかかりますし、慣れていない側が責任を感じやすくなって心理的にも×です。
では、目指した設計を行うにはどうすれば良いでしょうか。
例えば、過剰な機能が詰め込まれた所謂「神アセット」のようなものがあってはいけません。(みんなが「神アセット」を編集したい場面が多く訪れてしまう)
そのため、チームメンバーの役割分担に合わせて「適切にアセットを分割する」必要があります。
また、「アセット間の依存度を下げる」ことも重要です。
いくらアセットを分割しても、他の人が自分の担当以外のアセットを変更するたびに自分のアセットに影響が出てしまうような設計になっていては意味がありません。
他のアセットに影響されず独立して動けるような設計にしておく必要があります。
まとめると
- 役割分担に合わせた適切なアセット分割
- アセット間の依存度が低い設計
これを行う必要があります。
Weaponsの開発では、主にモデリング担当1人、ゲームロジック担当2人、UI担当2人という分担をしていました。
そのため、この記事ではUIの実装とゲームロジックの実装の切り離しを例に話を進めていきます。
アセットの切り離し
アセットをうまく切り離し、「神アセット」を生まないようにするには「単一責務の原則」を守るようにクラスを設計していくと良いです。
「単一責務の原則」はプログラムを書く人なら聞いたことはあるでしょうが、ざっくり言うと「ひとつのクラスはひとつの責任しか持たず、そのクラスがやるべきことはそのクラスにしかさせない」という考え方です。
プレイヤーについて実装するクラスを考えたとき、それにはプレイヤーが持つべき情報しか持たせず、プレイヤーがするべき行動しか実装しないことが理想です。
例えば、「体力」「スタミナ」といった情報はプレイヤーのものなのでプレイヤーのクラスが変数として持つべきで、その値の変更や「攻撃」「回避」などのプレイヤーが行う行動もプレイヤーのクラス内で行われるべきです。
それに対して、「体力、スタミナバーなどのUIの表示」はプレイヤーがする行動ではないので切り離してUI実装用のクラスで実装するべきですし、逆にUI実装用のクラスで「体力」「スタミナ」の値の変更はするべきではありません。
以上のような原則に基づいてアセットを切り離すことで自然と役割分担に合わせたアセット分割が可能です。
依存関係の切り離し
アセットをうまく分割しても、アセット間の依存関係が強くなっては意味がありません。
例えば、UIで表示したい情報をプレイヤーから取得しようとした際にはどうするのが良いでしょうか?
よくある悪い例は以下です。
これの悪い点は、BP_Playerへのキャストを行っている点、さらにそこからHealth変数の値を直接取得しているという点の2つです。
それぞれどのように悪いのか解説します。
- キャスト
アセットの依存度を下げたい場合「Castは悪」です。
今やりたいことは「プレイヤーの体力を取得する」ことのみであるのに、「プレイヤーの全ての情報を読み書き可能」な状態になってしまっています。
これはどういうことかと言うと、プレイヤーの実装側にとって意図しない変更を知らぬ間に行われてしまっているリスクがあるということです。
Castが使用されている可能性がある場合、バグが発生するなどした際にデバッグで自身の担当範囲外の実装までチェックする必要が出てきます。 - 変数の値の直接取得
こちらは値を読んでいるだけなので大した問題ではない…と思うと大間違いです。
例えばプレイヤー実装側が実装途中に何らかの理由で「体力とスタミナの値は構造体で管理するようにしよう」と考え、Health変数を削除し、新たにST_Status構造体を作成したとします。
そうするとHealth変数が無くなって値を取得できなくなるので、プレイヤー実装側がいじっていないはずのUI実装部分でコンパイルエラーやバグが突如発生します。
恐ろしいですね。
このように、何気ない実装ひとつで依存関係が発生していきます。
これが積み重なると開発終盤でバグ祭りが発生し収拾がつかなくなる可能性が高まるので、設計の段階から依存関係が極力発生しない実装のルールを決めておくことが重要です。
今回のチーム開発で依存関係をなくすために決めたルールは以下です。
- Castは可能な限り使用しない(自分の担当範囲外のBPには特に)
- それに伴い、他BPへの参照を保持する変数の型は、できるだけ標準で用意されている継承元とする
例:BP_Playerは標準のCharacterクラスを継承しているので、BP_Playerへの参照を保持する変数の型はCharacterにする。 - BP間の情報交換には必ずブループリントインターフェースを使用する
このルールを守って先ほどの例を実装するとこのようになります。
BPIF_Playerを新たに作成し、その中でGetHealthを定義しています。
BP_Player側はBPIF_Playerを装備し以下のように実装します。
これによって、キャストを使用していないためプレイヤー実装側が意図しない変更に怯える必要はなくなりました。
また、インターフェースを介してHealthの値を取得しているため、プレイヤー実装側が体力の値の持ち方を変えたとしても、プレイヤーが装備しているインターフェースの実装を以下のように変更すれば良いだけです。
これでプレイヤーとUI実装側に依存関係はなくなったため、プレイヤー側の実装をしていて起きたバグはプレイヤー側、UI側のバグはUI側に原因があるということが明らかになりますし、互いが互いの実装に手を出す必要がなくなりました。
まとめ
- アセットを適切に分割する
-
アセット間の依存関係をなくす
これができるような設計とルール決めを初めに行っておくことで、チーム開発は驚くほど円滑に進めることができます。
サークル内で行われるようなチーム開発では、実装の方針でモメて空中分解、開発終盤になって発生する無限バグ祭りなどで開発が頓挫というのはよく聞く話です。
今回の例のように、各々が各々の役割部分を事前に決めたルールを守って実装しておけばよいという設計になっていれば、他人の実装方針にイライラするということもありませんし、自分の責任の範囲外でのバグというものは非常に起こりにくくなります。
チーム開発は初期設計が肝心です。
私は3年の時もチーム開発をしようとしていましたが、ここが甘くて頓挫しました。
以上になります。今後の代のチーム開発の参考になれば幸いです。