Unityでゲームを開発する前に知っておきたかったこと

2022年2月6日(更新: 2022年4月15日)

ゲームエンジン Unity を使って3Dを使ったゲームを制作し、リリースすることができました。

今回のゲーム制作は、作りながら必要になった機能等をその都度調べて学ぶというスタイルで行っていたため、度々「もっと早く知っておけばよかった!」ということがありました。

総括として、Unityでゲームを作る際に知っておいた方がよいと個人的に思ったことを記事として記録しておきます。

一部は3Dに限定した話ですし、識者からすると言われなくても当たり前だという事柄もあると思いますが、参考になれば幸いです。

レンダリングパイプラインや色空間を最初に決定する

作るゲームがリアリティを重視したフォトリアルなのか、アニメ調のポップな雰囲気なのかなど、作るタイトルがどのようなビジュアルかによって最適なレンダリングパイプラインは異なります。

そこまでリアル調を求めているわけではない場合、今だとURP(Unity Universal Render Pipeline)を使うことが主でしょう。グラフィックを優先するならHDRP(High Definition Render Pipeline)やカスタマイズ可能なSRPなどを用いるようです。

Unityはデフォルトでは Built-in Render Pipeline が設定されています。

パイプラインによって使用する色空間(Gamma / Linear)やシェーダーが異なります。

Unityのレンダリング関連設定

従って、一度作り始めると途中でレンダリングパイプラインを変更することは難しいです。できなくはないですが無駄な手間がかかります。色空間を途中で変えると、光の見え方などが結構変わってしまします。

Unity の方針としては、今後はどのパイプラインにも互換性があるようにしていくということですが、現状はそうなっていません。

Built-in は途中で URP にコンバートできるようになっており、そのためのメニュー(Render Pipeline Converter)があります。

しかし、筆者がプロジェクトの途中でこのコンバートを実行しようとしたところ Unity の不具合なのか、正常に機能しませんでした(シェーダーに変更を加えたため?)。なぜかコンバーターウィンドウを開くと Unity 自体が固まるという謎の現象に遭遇…。

こういった予期せぬ自体を防ぐためにも、作るゲームに応じたレンダリングパイプラインを予め設定しておくことが無難だと思います。

アセットストアを積極的に使う

Unity Asset Store は、Unityでゲームを作る際に便利な機能やリソース(画像、音声、3Dモデル)、サンプルなどが配布・販売されているサイトです。

筆者は初め、ストアの存在は認知していましたが、ここからダウンロードしたものを用いるためには制作側に認可をとったりライセンス表記したりする必要があると誤解していたため、あまり活用していませんでした。

実際はアセットストアから得たリソースは、Unityのゲーム制作に用いる場合は個別に許可を取る必要も配布元のリンクをゲーム内に記載する必要もありません

無料で利用可能な便利な機能や3Dモデルがたくさんあるので、使いそうなアセットを片っ端からプロジェクトに入れておくと良いと思います。不要だった場合は後からまるごとアンインストールすることができます。

既に用意されているものが使えるなら、1から開発する手間も時間も節約できます。

個人的に便利だと感じた拡張機能を以下に記載します。

多言語対応(ローカライズ)ができる Lean Localization は、以前使用した公式のローカライズパッケージよりも使いやすいと感じました。

「Play Mode Save」はプレイモード中にオブジェクトに加えた変更を保存できる機能を提供します。

「プレイモードであることを忘れて編集したら、全部元に戻ってしまった」ということにならないために便利です。Unityの設定でプレイモード中はウィンドウの色が変わるようにしておけばプラグインなしでも大体は防げますが。

町並みや乗り物、木々や岩などの3Dモデルは非常に助かります。

Unityの機能を把握する

「スクリプトで作ったけれど同じことができるコンポーネントやライブラリが最初からあった!」ということが何度かありました。

追従するオブジェクトには「Constraints」、カメラ制御なら「Cinemation」といったものです。

同じようなオブジェクトが度々登場するなら、管理に「ScriptableObject」は必須でしょう。

Youtubeで「Unity tips」などのキーワードで検索すると、こういった便利機能やテクニックを紹介してくれる動画が沢山あるので、作業の合間に見て使えそうな機能の目星をつけておくと良いかもしれません。

Rigidbodyの使用と物理演算を極力避ける

Unity製のゲームでフレームレートが下がるが原因のひとつに重力演算があります。

プロファイラー(Profiler)で確認するとわかりますが Rigidbody を持つオブジェクトが複数重なったりするとオレンジのバー(Physics)によるスパイクが発生し、フレームレートの急激な低下が起こります。

Unityの物理演算がフレームレートを低下させる

Unity公式も、単純な重力に引かれるだけの物体には Rigidbody を使わずスクリプトから計算された擬似重力を使うように推奨しているほど、物理演算による影響は大きいです。

特に大きさのあるオブジェクトが Rigidbody による重力演算を行うときはパフォーマンスに影響することが多かったです。プロファイラーでは PhysicsFixedUpdatePhysX.PxsContext.contactManagerDiscreteUpdatePhysics.SyncColliderTransform がメモリを消費していたと記憶しています。

Kinematic にチェックを入れて外力の影響を受けないようにしたり、制約(Constraints)によって物理演算の影響を受ける移動や回転の向きを制限するとパフォーマンスは改善します。どうしても Rigidbody が必要なときは Kinematic にできないか検討してみるのも良いと思います。

オブジェクトの登場場面によっては Rigidbody を持つものを減らすことができるかもしれません。例えば、遠くにある平らな平面を歩くだけのオブジェクトは重力を計算する必要がないため Rigidbody 取り除くなどが考えられます。

FixedUpdateで行う処理は最小限にする

Monobehaviour を継承したクラスにて、ループ処理が実行される関数には Update と FixedUpdate があります。

特殊な処理でない限り、通常はフレーム毎に一度だけ処理が行われる Update をメインロジックの実装に使用すべきです。

FixedUpdate は実行間隔がフレームレートに依存せず一定であり、1フレーム間に複数回実行されることがあります。

前述の通り、FixedUpdate では物理シミュレーションの処理が行われます。ここで負荷のかかる処理を行ってしまうと、フレームレートが低下した際に繰り返し FixedUpdate が呼び出され、さらにフレームレートが低下するという悪循環に陥る恐れがあります。

場合によっては Project Settings > Time から、FixedUpdate が実行される間隔(Fixed Timestep)やフレームレートが低下した際に掛けられる許容時間(Maximum Allowed Timestep)を調節する必要があるかもしれません。

Unityのプロジェクト設定でタイムステップを変更する

公式マニュアルには以下のように記載があります。

・Fixed Timestep 設定 (Time ウィンドウ内) を調整して、物理演算の更新に費やす時間を削減できます。Timestep を高くすると、物理演算の精度を犠牲にして CPU のオーバーヘッドを削減します。多くの場合、精度を下げることと引き換えに速度を上げるのは許容できるトレードオフです。
・Time ウィンドウで Maximum Allowed Timestep を 8–10FPS の範囲の設定し、最悪の場合の物理計算に要する時間を制限します。
物理演算のパフォーマンス最適化 – Unity マニュアル

読み込む3Dモデルの頂点数は削減する

頂点数が多い複雑な形のモデルになるほどレンダリングコストが増加します。

ポリゴン数が多いキレイな球体のマリモを大量に画面に表示して転がした際にパフォーマンスの低下が確認されました。

この球体の頂点数を減らし、ちょっとカクついているマリモに差し替えたところ動作が改善したので、読み込むモデルの頂点数は少ないに越したことはないと確信しました。

頂点数が多いほどGPUに負荷がかかるとドキュメントにも記述があります。

グラフィックスパフォーマンスの最適化 – Unity マニュアル

同様に、モデルのマテリアルとボーンも少ない方が負荷が減ります。

一体しかいないプレイヤーやボスといったキャラクターなら問題ありませんが、ザリガニやタコのようなアニメーションする足数の多い敵キャラモデルを大量にシーンに登場させるとフレームレートに多大な影響を与えます。

MeshColliderは必要最低限にする

Mesh Collider はアタッチされたメッシュの形状から自動的にそれっぽい当たり判定の形状を生成してくれる便利なコライダーです。

制作当初、「勝手に形ぴったりの当たり判定を付けてくれるなんて最高!これ以外必要??」と調子に乗ってあらゆるモデルの衝突判定をこれでやっていたら、後にシーンのロードに時間がかかる原因となってしまいました。

シーンのロード時には当たり判定の初期化処理が行われます。

プロファイラー上では Mesh.Bake Mesh PhysX CollisionData または、スケールが変更されている場合は Mesh.Bake Scaled Mesh PhysX CollisionData として確認できます。

MeshCollider をアタッチしたオブジェクトがシーンに多ければ、この処理に時間がかかるためシーンロードが遅くなります。先程のモデルの話とも共通するのですが、頂点数が多い複雑な形状のコライダーになるほどこれに時間がかかるようです。

個人的な体験では、リアルなバケツのモデル(頂点数5万くらいだったと記憶してます)数個にMeshColliderを設定しただけでシーン全体の読み込み時間が数秒遅くなるというものがありました。

Convex にチェックを入れれば自動的に判定の形が簡略化されるから大丈夫だと高を括っていたら、このチェックの有無は特に関係ないようです。

コライダーは可能な限り、プリミティブな球体(SphereCollider)や立方体(BoxCollider)を用いましょう。複雑なモデルでもこれらを複数アタッチして組み合わせることでそれっぽい形の当たり判定を作ることができます。

公式マニュアルにも書いてありました。

メッシュコライダーはプリミティブコライダーよりもはるかに高いパフォーマンスオーバーヘッドを持っているため、使用を控えめ目にしてください。プリミティブコライダー付きの子オブジェクトを使用して、メッシュの形状を近似することが可能な場合もよくあります。子コライダーは、親のリジッドボディによって単一の複合コライダーとして一括して制御されます。
物理演算のパフォーマンス最適化 – Unity マニュアル

背景にいるオブジェクトには精巧な当たり判定は必要ないことが多いので、削減できるか検討してみると良いでしょう。

最もパフォーマンス的に軽い形状は球体と言われています。距離(半径)の情報だけで衝突判定ができるからだそうです。

Unityでのコライダーの種類には、さまざまなパフォーマンス特性があります。
次に示すのは、コライダーをパフォーマンスが最もよいもの(左端)から最も悪いもの(右端)まで順に並べたものです。
メッシュコライダーは、プリミティブコライダーよりはるかにコストが高くなるため、避けることが重要です。

スフィア < カプセル < ボックス <<< メッシュ(凸) < メッシュ(凸なし) Unity のパフォーマンスに関する推奨事項 – Mixed Reality | Microsoft Docs

シーンは分割する

元々、一つのステージをひとつのシーンとして作っていたのですが、ステージが大きくなるに連れロード時間やプレイ中のパフォーマンスに影響が見られるようになってきました。

オブジェクトは画面外にいても存在するだけでメモリを喰う原因になります。オブジェクトが多くなってきたら、同じステージでも前半と後半に分けたりといった工夫が必要になると思います。

まとめて編集がしにくくなるというデメリットはありますが、遊ぶ側の体験を改善するには分割できるシーンは分けたほうが良いと思います。

Prefab Variant の使用を徹底する

あるオブジェクトをベースに一部が異なるオブジェクト(色違いのオブジェクト、装備が違うだけで中身は同じ敵など)を作るなら Prefab Variant (プレハブバリアント)を有効活用しましょう。

ベースとなるオブジェクトの設定に変更を加えたら派生オブジェクトの設定も一括で変更するといったことができ、編集が非常に楽になります。

この仕組み自体はわかりやすく、初めから使いこなせると思いますが、問題はこの機能を中途半端に使ってしまうことです。

ちょっと思いつきで新しい敵をヒエラルキー上で作ってプレハブ化したけどバリアントにしてなかったり、色々イジるためにプレハブ化が解除されたただのオブジェクトにしていてそのままにしていたり……みたいなことをすると、後々一括で変えたつもりでも一部のオブジェクトが変更の対象から外れてしまい、せっかくまとめて変更できる利点が台無しになります。

一見すると問題なさそうでも、テストプレイ中に一括変更を免れた一体が予期せぬ挙動をするのを見つけて発覚するということがあり、混乱の原因になります。

シーン内のオブジェクトが増えてくると目的のオブジェクトを見つけ出すのが難しくなってきますので、一体だけ変更されないみたいな状況は避けたいですね。

Unityのバージョンを変えない・Windowsで開発する?

今回の制作で遭遇したトラブルの原因のほとんどがこれを守らなかったためだと推測されます。

Unityは長期安定版(LTS)の他に、一応安定化している最新版がダウンロードできるようになっています。

この最新版は結構頻繁にアップデートされ、不具合の修正などが含まれているのですが、意外にとんでもないバグを抱えていて作業を中断せざるを得なくなることがあります。

最近あった大きな不具合を持つアップデートは 2021.2.2f1 と 2021.2.3f1 で Editor でのカメラ移動やオブジェクト選択がカクカクでまったく作業ができない状態になるというものでした。

これ以外にも、なぜか設定が変わったりすることがありました。

こういう事があるとダウングレードして作業を再開させるのですが、このダウングレードによっても予想外のエラーが発生したりすることがあります。バックアップをとっていないとプロジェクトが破損する恐れあるので注意しましょう。

基本的には、最初に使ったバージョンを完成まで使い続けるのが良いと言えます。完成した後で、メンテのためにバージョンアップが必要となればそのときにするという形が理想的かと思います。

また、フォーラムを見ている限り、大きな不具合は macOS の Unity でよく起こっているように思えます。

macOS は Windows に比べて大型アップデートが頻繁にありますし、新型のM1プロセッサ搭載型機種が出てきたというのも影響しているのかもしれません。

もしかしたら Windows で Unity を使うほうがストレスのない安定的な開発ができるのかもしれません。

Findするよりインスペクターから渡す

スクリプト内からオブジェクトへアクセスする方法は色々あって迷いますが、通常はインスペクター上からオブジェクトを設定できるように、変数をシリアライズ化可能なフィールド [SerializeField] としてそこに渡すのがいいでしょう。

めんどくさがってオブジェクト名から取得する Transform.Find を多用すると、後々オブジェクトの名前が変わったり親子関係に変化があったりしたときアクセスが切れてしまいます。

SerializeField の変数の場合も変数名を変更するとアクセスが切れますが、これは FormerlySerializedAs を使えば解決できます。

どちらが変わりやすいかと言えば当然オブジェクトの階層構造や名前の方なので、頻繁に煩わされたくなければ SerializeField としてアクセスする方法をオススメします。

ランタイムでなければ設定できないオブジェクトに関しては Find を使うことになります。

無駄な最適化に囚われない

最適化は Profiler を Deep Profile をオンにしてテストし、その分析結果から行うといいでしょう。

各フレームごとのデータを見て、望むFPSを超えた部分の処理にのみ着目し、それを改善していきます。

通常、スクリプトの処理がパフォーマンスに影響することは稀で、モデルの設定や物理演算、レンダリングに問題があることが多いと思います。

割り算を掛け算に置き換える、数値の掛け算を行ってからVectorに掛ける、重いと言われている Vector の処理を自作の関数に書き換えるのような、メモリカツカツな環境だった昔に行われていた最適化は不要です。

気にしていてはキリがありませんし、どうしてもしたいならリリース後の整理時がいいでしょう。

おしまい

もっと色々あったと思いますので、思い出したら追記したおこうと思います。

制作したゲームは横スクロールシューティング「マリモ -VS- 外来種」です。Steamにて販売しております。

興味がございましたら、ぜひ遊んでみて下さい!

コメントを残す

メールアドレスが公開されることはありません。