この記事は、Unity Advent Calendar 2021 4日目の記事になります。
前日締切だったコンペへの提出が終わったあとのテンションで書きなぐったため、割と文章がおかしかったらごめんなさい。
もしかしたらあとで体裁を整えるかもしれません。
概要
Unityで弾幕STG等のオブジェクトを大量に出せるゲームを作るため、軽量な描画コマンドを記載する資料になります。
※実際のところBatchRendererGroupだったり、当たり判定組み込む場合JobSystem使えばもうちょっと速度出せるとおもうんですが、踏み込んだ最適化アプローチだったりするので、まずは楽にオブジェクトを出せるアプローチを記載することにしてます。
こんな描画してるよ
割と手加減しているのですが、この時点で敵・エフェクト合わせてオブジェクトが1000個くらい画面に出てます。
更に言うと以下の状態です。
- フレーム制御かけてる(つまりCPU負荷が大きくなったらフレーム更新がされなくなる)
- 当たり判定を付けてる(量が膨大すぎたので2フレームで1回程度。CPU負荷に影響)
- 各オブジェクトに対して行動を指定している(CPU負荷に影響。スクリプト言語使っているため負荷は多め)
- アニメーションを制御している(UnityのAnimationがかゆいところに手がとどかないヤツなので自分で制御する仕組みを作った。内部ではマテリアルの切り替えが発生しているため、割とGPU負荷多め)
- クォータニオンで変形かけてる(その分CPU/GPUに負荷あり)
- PostProcessをかけてる(GPU負荷が高くなる)
これで問題なく、しかも楽に書けるので、Unityってすげえと思いました。
とりあえず描画であれば大量にできるわけですね。
バッチがどうなってるかは状況に依存しますが、割とまとまった描画になってると考えます。
どういうアプローチで描画するか
DrawMesh命令で描画するだけ。
- どういう図形で(立体で)
- どういうマテリアルで(テクスチャ・合成方法で)
- どこの座標に
- どういう回転や拡大縮小して
描画するかを指定できます。
API自体は3Dの関数になるため、2D想定だと、以下の制御が必要と思料。
- スプライト扱いで描画するため2枚のポリゴンを生成
- Z方向も指定(ビルボードであれば方向は固定)
- テクスチャはMaterialに設定し反映
これで見た目2Dの描画ができてしまいます。
実際にはカメラを平行投影にする・カメラ描画の順位を変更する等の制御が必要ですが。
Unityで描画命令を書いたことないはずだが
Unityでは通常GameObjectにRendererをアタッチ、もしくはシーンに登録した時点で描画されます。
GameObjectに各種Rendererをアタッチしている状態だと、言ってしまうと「勝手に描画される」んですね。
そのおかげであまり描画を気にせず取り扱うことができるんですね。
たとえば、図形をシーンにドラッグ&ドロップすると即時に描画されますよね。
あれは、最初からレンダラーがコンポーネントとして登録されているため、即時に描画される仕組みになっています。
(実際コンポーネント見てみるとレンダラーがアタッチされているはず)
他のコンポーネントとの連携
上でGameObjectとは関係なく描画コマンドを実行できることは記載しました。
じゃあGameObject・Collider等との関連性はどうなるのか?
答えは「GameObject等と関連しない形で描画ができる」です。
各コンポーネントは本来GameObject上にアタッチすることで効果を発揮するため、
DrawMeshを行った場合、実際の座標に関係させないことが可能になります。
GameObjectを取り扱う場合、各コンポーネントをアタッチすれば即動いてくれる敷居の低さがUnityの便利さではあるのですが、これらを使わないという選択肢も可能になるわけですね。
逆に言うと描画した座標と関連するように各コンポーネントを使うためにはC#で座標を制御する等での処理が必要になるため、Editor上での動作にはむいてない形になります。
描画を別コマンドで実行して何がしたいのか
自分の場合だと、「GameObjectを1個のオブジェクトジェネレータとしたい」です。
Unityにおいて、GameObjectをInstantiateするのは処理が重い、というのは割と知られていますが、同時にデータ量も多く、メモリ周りにも処理負荷がかかります。(ヒープもその分使っちゃうと、GC周りにも影響があるかと思います)
よって、大量のオブジェクトを動かす場合、GameObjectを敵や弾のマネージャとして、描画は別コマンドで実行、みたいなことを行うアプローチも出てくるわけです。
上記のケースではGameObjectで描画命令を発行するというアプローチは有用かと思います。
Unityの便利機能を使わない理由はあるのか
Unityのレンダリングエンジンを組み込むことができ、
かつCPU・メモリの負荷をへらす動きができます。
激しい動きときれいな画面が同居する形になります。
よって、派手なアプローチがいっぱいできるわけです。
その代わり最終的な実装は面倒になります。
具体的にはGameObjectのUpdate関数に依存するコンポーネントと対象座標を紐付ける場合、そのバインド処理を書く必要がありますし、上記の通り描画命令を書く必要があります。
なお、描画命令についてはパイプラインやバッチング、カメラの設定に依存するので、書き方によっては性能が劣化したり、描画順によっては想定したように描画ができなかったりします。
自分の場合、描画のバッチングが有効になるようにマテリアルごとに描画をまとめたりしてできる限り描画が高速に行えるように内部で制御してます。
(実装の関係上アニメーションさせる際各オブジェクトが別マテリアル扱いになってしまうので、メモリにアニメごとのポリゴンを置いてそれを大量描画する形にしてバッチングが有効になるようにする等)
これは、自前でフレーム制御を行っていることとそれに伴って当たり判定やコントロールのタイミングを制御する必要があるために、上記のアプローチを取っています。
終わりに
割とこういうの書く機会もないので、一度かいてみました。
せっかくだからこういう需要が少ない記事を紛れ込ませるのもひとつということで。
(自分はUnityでも頑張って弾幕STG作りたい派です)
宣伝
現在インディーゲーム「Cry Pic.」開発中です。
上記の知識や描画を使って、大量の弾やエフェクトを描画してます。