本記事は Craft Egg Advent Calendar 2022 18日目の記事です。
12/17の記事は @tkp_0529 さんの プロデューサーに必要な力ってなんだ?という話 でした。
1. はじめに
株式会社Craft EggでUnityクライアントエンジニアをしている豊田と申します。2020年に新卒入社してから運用タイトルにジョインし、開発・運用に携わり現在3年目になります。
今回は、サイバーエージェントのゲーム事業部 コア技術本部が刊行した「Unity Performance Tuning Bible」(以下「本書」)を読んで、実際に自作ゲームにその手法を試してみた話を記事にさせていただきます。ノウハウそのものを紹介するというよりも、どんなことをやったか、気づきや学びがあったのかを、記事の中で紹介出来たらと思います。
本書につきましては、サイバーエージェントのDevelopers Blogに紹介、また無料でpdf版が公開されていますのでぜひご覧ください。
パフォーマンスチューニングを試すためには、何かしらのUnityのプロジェクトが必要です。そこで、私が5年前(2017年)の学生時代に作っていたゲームを実験材料としてチューニングしてみることにしました。
ゲーム概要
チューニング対象とするゲームの内容を簡単に紹介します。
・3Dシューティングゲーム
・操作はマウス、クリック、スペースキーのみ
・PC(Windows)向け
・マップの建物は自作でモデリング、敵キャラなどはアセットストアの物を利用
自動で画面が動いていくので、敵をマウスで狙って撃っていくというシンプルなゲームです。Unity入門者の時に作ったもので、中身は乱雑に作られているのもあり、チューニングに向いていそうです。
2. チューニング準備
指標を決める
本書にはチューニングの前にまず、達成したい「指標」「動作保証端末」「品質設定の仕様」を決めるべきと書かれています。モバイルゲーム開発においては、様々な端末やOSで動作することが考えられるので、顧客や市場を分析したり、ゲームの性質から指標を出すべきです。
今回は、定性的ではありますが「私物の開発用Android端末でビルドした時に快適に遊べる」というのを、ひとまずの目標にしてみました。
Android端末のスペックは以下の通りです。
- モデル:AQUOS R2(SH-03K)
- CPU:Qualcomm Snapdragon 845
- RAM:4GB
- 2018年購入(機種変済みのため利用頻度低)
余談ですが、この端末はQualcomm社製のSnapdragon800番台のSoC(System-on-a-chip)が使われており、これはフラッグシップモデル、いわゆるハイエンド端末に搭載されているものです。本書には上記のようなスマートフォン端末のSoCの紹介もされています。
計測できる環境を準備する
プロファイリングはUnity Editor上で行えますが、実機での動作の感覚や、実際の負荷を測るためにも、まずはAndroidでビルドし、プロファイリングできるようにする必要があります。そもそもUnity5時代のプロジェクトだったのでバージョンを上げ、それに伴う廃止メソッド起因のエラー解消等にも一苦労しました。
ゲームをモバイル操作に対応させる
ゲームはPC向けに作っていましたが、幸い操作がマウスのクリックとスペースキー入力だけでしたので、スペースキー入力と同じ操作をするためのボタンを画面上に設置しました。後はSwitch PlatformをAndroidにするだけでモバイル向けのビルド・動作確認が行えます。
Unity Profilerで実機ビルドしたアプリの計測をできるようにする
公式ドキュメントをはじめとして、実機ビルド手順を解説している記事はネットに多くありますが、以下の手順を行いました。
- Development Buildを有効にする(デフォルトではチェックが入っていない)
- Android端末で開発者オプションを起動しUSBデバッグを有効にする
- ADBデバッグのためのSDKダウンロード、環境設定
- 端末とPCをUSB接続し、ビルド実行
- Profilerで端末を選択
上記セットアップにより、実機ビルド後にProfilerで実機のプロファイリングが表示されるようになりました!
また、ログはUSBケーブルで接続している時しか出せないと思っていたのですが、外してもアプリを起動するとログやProfilerに反映されていて驚きました(Wi-Fi経由で接続している模様)。
準備編は以上として、実践編に移ります。
3. チューニング実践:計測する
パフォーマンスチューニングでは、以下の点に気を付けると良いと本書で説明されています。
- 性能低下の原因を切り分け、適切な対処を行うこと
- 「計測」、「改善」、「再度計測(結果の確認)」の一連の流れを必ず行うこと
以下、タイトル画面からインゲームに遷移した流れを計測してみたProfiler画面です。CPU Usageを見ると、Renderingの割合が非常に高く、FPSの値も15とかなり低い状態です。
定常的なRendering負荷の改善が第一に重要だと見て取れるため、まずは描画周りの改善を行いたいところです。レンダリングの詳細を見てみると、ドローコールが1000を超えてしまっているため、描画命令を減らすことに挑戦してみます。
4. チューニング実践:レンダリングの定常負荷を改善する
ドローコールを減らす
DPIを固定する
まずは、簡単な解像度の調整からやってみます。描画する画素数が少ないほど負荷も比例して少なくなる、というシンプルではありますが効果的な方法です。
モバイルプラットフォームのPlayer Settingsの解像度関連の項目に含まれている、Resolution Scaling ModeをFixed DPIに設定することで、特定のDPIをターゲットに解像度を落とせます。以下の記事を参考にしつつ、今回は326に設定しました。
また、Quality SettingsのResolution Scaling Fixed DPI Factorの数値を0.5にまで落としました。最終的な描画解像度は、Target DPIとResolution Scaling DPI Scale Factorの値を乗算して決まります。
上記だけでも、ドローコール数をおよそ200程度減らすことができました。
クオリティ設定を見直す
Project SettingsのQualityでクオリティの設定を行えます。デフォルトの設定のままでしたので、オーバースペックな各種設定を調節しました。テクスチャの設定も低くしたことで、建物のタイル表現なども荒くなりましたが、スマホの画面ならそこまで気にならないと思います。
詳細は以下の記事を参考にさせていただきました。
影を描画しない
シャドウ、特にリアルタイムシャドウの負荷の影響が多く、また、ゲーム性として影は正確に描画する必要はないと判断し、各ライトのシャドウを一旦No Shadowsに設定して描画をOFFにしました。
影を出したいオブジェクトは、建物や敵キャラなどが挙げられますが、建物はライトマッピングで事前にBakeする、敵キャラは疑似シャドウなどで表現することなどで対応できるかと思います。
オクルージョンカリングを活用する
メジャーな手法ですが、オブジェクトに遮蔽されてカメラに映らないオブジェクトをレンダリング計算から外す機能です。マップ上の建物はすべてstaticフラグをONにできるため、活用できます。
「Occlusion Culling」ウィンドウを開き、カリングしたいオブジェクトをすべて覆うようにOcclusion Areaを設定、そしてBakeを行うだけで設定ができます。
実行後、Occlusion CullingウィンドウのVisualizationタブを選択することで、シーンビュー上で実際に描画処理が省かれた様子を確認することができます。以下のgifで、カメラを動かすことで視界にないオブジェクトが消えていることがわかります。
頂点数の問題
ここまで数点の改善を行なってみましたが、改めてProfilerのRenderingを見たときに、致命的な問題があることに気が付きました。Vertices Countがレンダリング負荷の比率が高いということです。
このゲームでは、建物は自作の3Dモデルを使っていますが、素人が初めてモデリングしたもののため、出来は良くありません。無駄に頂点数が多いモデルがMAPに置かれていたことが負荷の大きな要因になっていたために、当然オクルージョンカリングも効果的だったということです。
↓ そこまで複雑でないモデルですが、不自然な頂点・辺が張り巡らされていることが画像から分かります(テクスチャのUVもおかしいですね笑)
改善策としてはモデルの頂点が減るように修正ということになりますが、手間がかかってしまうことと、現状のプロジェクトの状態で簡潔にチューニングしたいということでモデルにはテコ入れしないこととしました。
なお、Unityアセットの「Mesh Optimizer」を使って簡単にリダクションできないかと試してみましたが、UVがおかしくなってしまったので、やはりちゃんと対応するならBlenderなどモデリングソフト側で行った方がよさそうです。
Terrainの負荷を減らす
頂点数問題は建物の3Dモデルに限りません。Frame Debuggerを見ると、Terrainのドローコールも非常に多かったことが分かりました。 Terrainは重いという話はしばしば耳にしていましたが、実際に体感して納得しました。
まず、「Pixcel Error(ピクセル許容誤差)」の設定を変更します。この値は大きいほど遠くの地形の描画が雑になりますが、このゲームでは背景程度にしか使われてないので最大値にしました。
上記設定だけで、Terrainのみで140ほどあった「Draw Mesh Terrain」のドローコールを30にまで減らすことができました。(ゲーム序盤の画面での測定値)
また、オクルージョンカリングをTerraiinにも適用しました。最初にオクルージョンカリングを設定した際は、マップ上の建物をだけを覆うようにしていたですが、Terrainもオクルージョンカリングで分割可能であることがこのタイミングでわかり、Terrain全体を含むようにOcclusion Areaを設定しました。これで一層Terrainの負荷を下げることができます。
ドローコール削減の結果
上記改善を行った結果、当初1100ほどあったドローコールを200台にまで減らすことができました。
SetPass Callも400ありましたが141まで削減されています。
オブジェクトのモデルそのものや配置、実装を変えることなく、特別なことは行わずとも大きく改善することができました。上記数値は、モバイルでは100~200程度が目安と述べている記事も見られたので、それなりの水準まで落とせたのではないでしょうか。
また、実機でのフレームレートも60台まであげることができ、ゲームのプレイ感が良くなりました。
レンダリングの定常負荷の改善はここで一区切りとしようと思いますが、実際は3Dモデルそのものの修正の他、マップ内でいくつか使われているアセット類をローポリのものにする、同一マテリアルを使うようにして動的バッチングを適用させるなどの余地はありそうです。
また、全体的に負荷を落とす設定を行った一方、グラフィック的にはクオリティが下がったともいえるため、実際は負荷とのバランスをとった見栄えの確認・ブラッシュアップは必要かと思います。
5. チューニング実践:瞬間的な負荷を改善する
スパイクの発生
レンダリングを中心とした定常負荷の改善を行いましたが、ゲームをプレイしていて気になる点はまだあります。それは敵を倒し切った後のアニメーション演出に入る際、画面が少し固まることです。記事冒頭のGifを見るだけでも、そのことは感じ取れるのではないでしょうか。
実際にProfilerを見ると、スパイクが発生しています。
原因はすでに予想できていましたが、詳細を見てみるとやはりInstantiateが割合を占めていました。
原因となる実装
本ゲームにおいて、「エリアの敵を倒す → 移動アニメーション → 次のエリアで敵が出現」 の流れをウェーブと呼んでいるのですが、このウェーブに出現する敵キャラはウェーブごとにPrefab化して、移動アニメーションに移った際にInstantiateしています。複数体いるのにもかかわらず1フレーム内で生成、初期化を行っているため当然スパイクも発生するわけです。
string prefabName = "wave" + (currentWaveCount + 1);
GameObject prefab = (GameObject)Resources.Load(prefabName);
Instantiate(prefab, new Vector3(0, 0, 0), Quaternion.identity);
さらにResources.Loadを使っていることもパフォーマンスに良くありません。
事前に生成して対応
改善手法としては非常にシンプルで、インゲーム遷移の初回ロードにすべて事前に生成したのち、ウェーブ事にSetActiveで表示を切り替えることとしました。これだけでもゲーム中に画面が固まることがなくなり、Profiler上でも該当場面でのスパイクは無くなりました。
より良くするならば、ウェーブ毎に敵Prefabが設定されている状態をやめ、敵キャラのオブジェクトをシーン内で使いまわすこと(オブジェクトプーリング)が改善策として考えられます。しかし、元の実装が悪くリファクタが大がかりになってしまいそうだったことから今回の記事のための実装としてはそのままにしました。
その良くない実装を具体的に挙げると、StartやUpdateといったUnityのライフサイクルに大きく依存した作りになっていることや、static領域を様々なオブジェクトが参照しあって構築されていたこと、肥大化したクラスがゲーム全体の制御を行っていたことなどです。Unity初学者の実装にはありがちですね。
こちらの記事もぜひご覧ください
「新卒2年目までに学んだ、コーディングで意識すること」
6. 実機動作比較
最後に、改善前と後との動作を比較した動画を以下に示します。改善前は全体のフレームがカク付いていること、敵を倒した後のアニメーションに入る前に画面が一瞬固まっていますが、これらが改善されています。
7. さいごに
今回はパフォーマンスチューニングの手法の中でもお手軽にできるものを中心にやってみましたが、実際にプレイの体感が変わるほどの改善ができたので良かったです。また、自分自身Profilerを使ったチューニングは今までの業務で行う機会があまりなかったので、計測、分析、最適化、結果の確認といったチューニングの流れを体験できたことが良かったです。
一方、ゲーム性や機能が比較的簡潔であることから、Terrainと建物のモデルが重い!ぐらいが主な要因というシンプルなものでしたが、実際のプロジェクトでは機能のボリュームが多いことから複雑な要因が重なったり、深い領域まで踏み込んで手を入れる必要があったりもすると思います。表面的な改善を行ったのみで、本書で紹介されていた最適化の手法を多くは実践できず、メモリ周りのチューニングやGC Allocの削減などにも触れてないことから、実践できることはまだ沢山あると思います。
「速いコードよりまず良いコードを書く」という言葉がありますが、今回チューニングした自分の昔のゲームは良いコードとは言えない形で作られており、アセットも最適化されておらず、そもそも論になりますが今回のケースで一番効果的な改善策は「設計から作り直す」と言えるかもしれません。
新規開発の現場でも、「作り直した方が結果最適化されて良い」「リリースの期限を考慮して作り直しは難しいから、すぐに効果が出るようなチューニングを行う」というような天秤をかけた判断も起きるのかな、と想像しました。
パフォーマンスチューニングを体系的に学んでみたいという方は「Unity PERFORMANCE TUNING BIBLE」は非常に良い教材になるかと思いますので、ぜひ活用してみてください。
最後まで記事を読んでいただきありがとうございました!
明日の Craft Egg Advent Calenderの記事は @kazu6831 さんの「新人デザイナーが陥りがちなデザインの進め方について」です!
参考リンク