ゲーム進捗:仕様を追加したい話

ゲーム開発が進行すると、応急で作ったテスト機能(スクリプト、コンポーネント、オブジェクトなど)をどこかで本番用に切り替えないといけない状況ができます。
いや、本番用だとしても、不具合とかゲームとして物足りないとかアップデートとか追加で機能を実装しないといけない状況が必ず来ます。
それを乗り越えられるかが、ゲームづくりの中盤-終盤の壁になると思った話です。

追加仕様の一例

画像3

自分の自作ゲームMarine School Simulatorでの実体験を元に例をあげますと

・多国言語に対応したい
・着せかえシステム作ったけどポーズや表情の切り替えも組み込みたい、あと衣装のアンロック機能を追加したい(なおUIの空きスペースがたりない)
・メニュー画面作ったけど、イベントシーンや状態異常でメニューの一部が使用不可になるようにしたい
・NPCに好感度を付けて会話が変化するようにしたい
・URPに変えたい
・天候と昼夜の切り替えを追加したい
・アセットのキャラカメラが不具合あるのでCinamechineにかえたい
・InputSystemにしてタッチパネルマウスコントローラ全対応にしたい
・建物建築システムを実装したい
・建物破壊を実装したい
・マップを自動生成して無限に遊べるようにしたい
・いつでもPCとNPCの操作を入れ替えたい

ものによっては他人からやれと言われたら殴り合いの喧嘩になる、プログラム1から作り直さないと無理なものがありますね。

現時点での私の体験からいうと、先のことを想定して拡張性をもたせて作っておくことは無理です。

先を見越して未使用の変数やメソッドを作っておいても、一週間もたたずに存在をきれいさっぱり忘れるんですよ。
後日同じような変数メソッドを作っちゃって、あとから気づいたときには検索妨害になるばかりでした。

はじめから想定してた仕様どおりの拡張工事ならスムーズに行くけど、予定にない仕様を追加するのは、一度完成してまとまってたものに壊して穴を開けて継ぎ足す行為なわけで、その時点で計画はめちゃくちゃです。
機能が追加されるたび、全ての想定した動作は見直しになり、衣装・装備の組み合わせとか、同時押し操作の検証などの動作確認の手間は乗算で増えていきます。
例としてキャラクターのアクションの追加をあげると、「歩く」「走る」「ジャンプする」があるところに「座る」を何も考えず追加した場合、たいてい座るポーズのまま歩いたりジャンプできたりします。
↓の画像は、プレイヤーがテーブルで自作料理を試食してたらNPCが股下に割り込んできて、一緒に食べてるところです

画像4

積み木を積むがごとく一つ一つ機能を追加して動作確認していって、
追加機能と例外対策が違法建築的に積み重なってくのは美しくないんだけど、いきあたりばったりで積み重ねていくしかないのか?
追加機能を何のリスクもなくクールに足す方法はないものか…!!!!

思いつく対策方法


・アセットを使う
やりたいことを実現できるアセットがすでにあるなら、それを使うのがいいでしょう。全てを自力で構築しようとするのは時間がもったいない。難しいことはアセットに頼るのが基本です。
問題は、欲しい仕様を実現するアセットが都合良くあるのかということと、
そのアセットが果たして今のプロジェクトでちゃんと動くかどうかです。
複数のアセットが使われるたびにうまく動かない可能性が高まります。
アセットは当然作者の想定内のことしかできないので、仕様から外れることを更に追加しなくてはいけなくなったら、一転して足かせの障害と化すリスクがあります。
HDRP/URPと各種ポストエフェクトに対応してるか・モバイルに対応してるか・TextMeshProに対応してるか・シーンチェンジに対応してるか・独自のセーブシステムなんて使ってないか・後から変えられるか…
まあすべてクリアしてるアセットなどほぼないと思います。
できなければ、改造して対応させることになりますが、そのために仕様を100%理解するのに必要なコストは決して安くなく、その果ての結論が「無理」だったときの落胆たるや…
似た別のアセットだったら動くというパターンもあるので、機能のかぶってるアセットを複数買っておくこと(=セールで衝動買いともいう)は無駄にはならないでしょう。


・継承を使う

円満に動いてるスクリプトに想定外の仕様を追加するのは、動作の調和を乱す危険行為な訳です。
ここまでは確実に動く、というとこを区分するには継承を使うという手があります。継承を使えば、どこまでが新規に追加したコードなのかがわかります。
購入したアセットでも時々そういう継承した(ほぼ白紙の)コードがアタッチされてるサンプルを見かけました。
でもプロジェクトの途中からこれを導入というのは、無理があります。
結局は新規のスクリプトを元のと差し替えるわけで、キャラクターコントローラとか、他のコンポーネントと連動するのがほとんどなんだから、シーン上に何十何百とあるキャラの個別設定が全部書き直しになるわけで、それは無理だ。
最初からやっておけば話は別なんで、同じようなシステムのゲームアプリを連続で作る場合に有効でしょう。
それでも、継承元を書き換えないとどうにもならないケースもあるでしょうけど。

・ScriptableoObjectを使う
仕様追加の際のデバッグで一番面倒なのは不具合を起こしそうな変数をすぐ監視できるようにすることです。[SerializeField]で変数をインスペクターに表示させる、デバッグログで表示するのが手軽ですが、複数のクラスが参照しそうな重要な変数やメソッドは、一つのScriptableoObjectデータベースにひとまとめに記述して、そことだけやり取りしてしまうのがいいんじゃないかと。

画像2

プレイヤーの座標とかステートやフラグなど、リアルタイムで変動するようなものの前後数フレームの値の変動を記録ができる、ゲームを停止しても実行時のデータが残せる、複数のコンポーネントの主要な変化と連動がひと目で見渡せる、など、不具合を検証するのに役立ち開発が捗るはずです。
※データが残られるとまずい場合は、実行時にすべてリセットするコードを書く必要があります。初期設定が書き換わってほしくない場合は、事前にデータベースを複製してそこから反映などするといいでしょう
当然変数がグローバル変数と化すので、間違っていじるリスクはあります。でも個人開発では、変数名とコメントをちゃんとしとけば、間違って使うなんてほぼありえない。(複数人開発ではどうかって?まあそれは)
あと、ScriptableoObjectにまとめて記録するような変数はたいてい、セーブデータにも保存するべきようなものが多く、セーブ機能を実装するときにまた役立つでしょう。

ゲームマネージャーを使う
ゲームマネージャーのオブジェクトには、シーン内で使うすべてのScriptableoObjectのデータベース、プレイヤーのキャラクターコントローラ、カメラ、UI、敵キャラを一元管理するコンポーネント、天候アセット、その他ゲームに関わる主要なゲームオブジェクトやコンポーネントを保持させておきます。

画像1

このズラっと並んでるオブジェクトやコンポーネントのは全て後付けで、なにか仕様を追加するたびに伸ばしてます。
※このゲームマネージャー自体は登録してるだけで何もしないのが重要です。スケベ心を出すと汎用性が失われます
ゲームマネージャーのスクリプトの名前がGameManageHub.csだとして、ゲームで使う自作コードにはすべてこう書きたします。

public class DSKaiwaComponent : MonoBehaviour
{
    [SerializeField, Header("ゲームマネージャーの名前"), Tooltip("ゲームデータベース、会話データベース、プレイヤーカメラ、プレイヤーターゲットを保持する")]
    string GameManagerName = "ゲームシステム";
    GameManageHub _GameManageHub;
       
    void Awake()
    {
           var a = GameObject.Find(GameManagerName);
           _GameManageHub = a.GetComponent<GameManageHub>();}
    }
}    

これで新規に追加したクラスはゲームマネージャーを自動取得して、気軽に既存の主要コンポーネントと変数が取り扱えます。
ゲームマネージャーが必ずシーン内にある必要があります。シングルトンである必要はありません。nullエラー対策はそれなりに必要です。
気軽に取得できないものにすぐ手が届くようにするのが目的なので、Getcomponentなどで即取得できるようなものは登録しません。
注意事項は、ゲームマネージャの参照タイミングはStart()以降でないといけないことです。Awake()のタイミングでゲームマネージャの登録したものを参照しようとしても、高確率でnullが返ってきます。
(つまり、非アクティブのコンポーネントはアクティブになるまで完全にセットアップできないし、そのコンポーネントを参照するようなコンポーネントも影響がでます)


・ビジュアルスクリプトを使う
ビジュアルスクリプトで作れば、変数やメソッドのつながりが可視化できます。ここに何かしら仕様を追加するとどうなるかが直感的にわかるでしょう。
視認できる恩恵が最も強いのは、キャラコン、AI、ステートマシンまわりですね。その場合追加実装とはつまり、新たな挙動を追加する、ですが。
ステートの管理はもっとも複雑なジャンルなので、たいていステートを一つ追加するだけで地獄絵図となります。インスペクタとテキストエディタでAIの動作を直感的に把握するのは困難です。ここに導入するメリットは大きいかと思われます。
問題は、どのビジュアルスクリプトが使いやすいのか・部分的にビジュアルスクリプトを使うことができるのかですが…
無料で使える公式のビジュアルスクリプト(BOLT)は、処理の流れがアニメーションで表示されるのが特徴ですが、使い勝手はどうかなー
正直、満足に使えるようになるまでのハードルが高いのは否めません。ノード一つ置くのも超苦労して投げた。
BehaviorDesignerは、C#一通り使える前提ならビヘイビアツリーの見た目のわかりやすさとノードを配置する操作性の良さがなかなかでしたが、当てにしてた視界ノードとか全然仕事してくれなかったり独自仕様が訳わからなくて、ついにはUnityが固まったりしたので投げました。痛し痒し。



・疎結合だのモジュール化だのはあきらめる
プログラミングの作法としては、各機能がコンポーネントで独立して、それらが必要最小限のつながりしかないか、なくても動くまである疎結合なほどよしといいますが、そういうのは仕様追加でメソッド・変数を増やすと大抵足かせになるか、ぶち壊しとなります。
「このシステムは複数人が共同開発で使うことを想定している」「テンプレートアセット販売を目指してて、もう絶対この仕様は変えない」という場合だけ疎結合な作り方は有効ですが、
試しにビルドしてプレイして「やべあまり面白くない」ならつくりなおすしかない。「これは面白いぞ」と確信するまでいけたらコードを整理しましょう…そんな余力があればですが
多分のちのちのアップデートを考えると整理しないままのほうが楽まである。

・むつかしいことしない

構造体とかstaticとか使うとクールにみえるし、実際Transformのように位置、回転、スケールを全部記憶できる変数あったらいいのにと思う時はあるし実際誰かがすでに作ったものがネットにあって、プロジェクトに入れてあるけど、まったく使わなくて忘れてた。
身の丈に合わないテクニックはよけいに苦しむだけ
大抵のことはifとforで代用が効くんですよ…
プログラム覚えたての頃はコード一行、機能ひとつを追加するたびに実行して動作確認しながら作り進めてました。
慣れてきたら、複数の機能を動作確認せず即まとめてコーディングして、それでも割とちゃんと動くようになっていったのですけど、
不具合が出たら結局全て一行ずつ見直す作業に戻されてしまってます。
不具合が直らなかったら機能を止めたりデバッグログ出したりして、問題箇所を絞り込んでいきます。
…だったら、コード一行ずつ少しずつ堅実にバグがでないように固めていくほうが結局早いのでは?という気がしてきます。
確実に動作する部分だけ確保して間違って外部から操作しないように変数やメソッドをpublicにしないとか逐一"絶対さわるな"と注釈を入れるとかして保護するシールド工法みたいな進め方が一番早くて確実、なのでしょうか。

…長々と書いたけど、追加のリスクは減らせても、動作確認は避けられてないですね。
一人でテストプレイしてると色々麻痺してくるし、デバッグしてくれる人を探すしかない?

すべての仕様を把握してるはずの個人開発ですらこの苦しみなら、グループで開発してたら地獄絵図だよな…と実感する今日この頃。



この記事が気に入ったらサポートをしてみませんか?