開発中のゲームのフレームレートが低いのに、その原因が分からないなんて事はありませんか? 一度に生成される敵の数が多すぎるのか、それとも特定の敵のコストが高すぎるのか? もしくは大量の視覚効果を使っているからか、あるいはとんでもなく工夫されたスキルシステムを書いたからでしょうか?
でも大丈夫。早合点する必要はありません!
あらゆるパフォーマンスの問題を解決する第一歩は、必要な情報のそろった状態で解決方針を決められるように、情報を集めることになります。もちろん、「そりゃ遅いに決まってる! アクター数が 100,000 体を超えてるじゃないか!」と決め付けてしまうのは簡単です。ですが適切なデータ収集を省いてしまうと、たとえ問題が全く関係ない単純なものであっても、レベル内のアクター数を減らすのに多くの時間を割いてしまう事になります。
了解! 最初は何をしたらいいですか?
一番最初に収集すべきデータは、ボトルネックが発生している場所がゲーム (Game) スレッド内か、レンダリング/描画 (Draw) スレッドなのか、それとも GPU なのかというものです。これを調べるには、ゲームを非デバッグ用ビルドで起動してコンソール コマンド「stat unit」を入力し、それぞれの作業にどれ程の時間が掛かったかを表示します。
Frame (フレーム) 時間というのは、ゲームの 1 フレームを生成するのに必要な合計時間を指します。Game スレッドと Draw スレッドはどちらもフレームの完成前に同期し、Frame 時間は多くの場合いずれかのスレッドの時間に近似します。GPU 時間はグラフィックカードはシーンのレンダリングに掛かる時間を測った数値です。GPU 時間はフレームに同期するため、こちらも Frame 時間と似た値を示すはずです。
もし Frame 時間が Game 時間に非常に近い場合は、ゲーム スレッドによってボトルネックが発生しています。もし Frame 時間が Draw 時間に非常に近い場合は、レンダリング スレッドによってボトルネックが発生しています。もしどちらの時間も近似ではなく、代わって GPU 時間が近い場合は、グラフィックカードによってボトルネックが発生しています。
本記事では、ゲーム スレッド内で発生した問題への対処法のみ記載します。
よし! ゲーム スレッドでボトルネックが起きている事は分かりました。次は?
ゲーム スレッドのパフォーマンスを確認するなら, stat profile (統計プロフィール) の利用が最適です。[半角/全角]キー (英語キーボードの場合は [~] キー) を押してコンソールを開き、「stat startfile」と入力する事でプロフィールが開始されます。この動作を最低でも 10 秒間、もしくは多数のフレーム間での十分な平均が取得できるまで走らせます。長めのプロフィールも長時間の断続的な問題を検出するためなどに役立ちますが、あまりに長い間走らせるとファイル サイズが肥大化するため、動作時間を 30 分以下に留めることを推奨します。一旦十分な時間のサンプルが取得できたら、コンソールに「stat stopfile」を入力して終了させましょう。プロジェクト フォルダ内のパス「Saved/Profiling/UnrealStats」上に ue4stats ファイルが作成されます、
プロフィールを取得できました。生成されたファイルを開く方法は?
キャプチャしたプロフィールを開くには、UE4Editor と同じフォルダにある「UnrealFrontend」を使用するか、エディタ内の「Window」メニューにある [Session Frontend] タブを使用します。[Session Frontend] タブが開いたら、[Profiler] サブタブを選択します。ここで先程キャプチャーした ue4stats プロフィールを読み込むことができます。
プロフィールを開く事はできましたが、ここから何をどうすればいいのですか?
重要な情報が記されているのが、一番下にある関数ツリーです。「GameThread」アイテムを開き、その中で枝をあまり含まない (もしくは全く含まない)数ミリ秒以上ある 「Inc Time」 (Inclusive Time - 包括時間) という項目が見つかるまで探索します。stat が各フレーム毎に stat コールされた平均回数が「Calls」列に表示されます。この時「CPU Stall」項目に惑わされないようにしましょう。この数値はスレッドが別のものを待っていた待機時間を示すものであり、フレーム レートが制限されていたりゲーム スレッドがボトルネックになっていない場合にのみ表示されるため、今回は関係ありません。以下のプロフィールでは、フォントのキャッシュに予想外の時間が掛かっていることが分かります。
この問題は、実は今週の Fortnite で実際に見つかったものだったりします! この場合、大量のテキストが、カメラと重要なゲーム内オブジェクトの間の距離に応じてサイズが変わりながら、表示されていました。そしてテキストのサイズがフレーム毎にリサイズされていたため、アンリアル・エンジンの UI システム「スレート (Slate)」内のフォント キャッシュには、同じ文字列のサイズだけが異なる数百種類ものバリエーションで一杯になっていました。この問題を修正方法として、距離ベースでのテキストの動的なサイズ変更が排除されることになりましたが、特定距離でのしきい値に応じてテキスト サイズが段階的に変更されるように修正することもできたでしょう。
Fortnite については結構ですが、直面している問題が「フォント キャッシュ」以外に因るものである場合は?
注意すべき要素は複数あります。
重要な要素の一つが、「FTickFunctionTask」の項目です。この項目内には、チックによって動作するあらゆるアクターとコンポーネントが含まれます。フレーム毎のチックで動作するアクターとコンポーネントの数を減らす事は、ゲームの速度を上昇させる方法として最適です。
開発されているゲーム内にチックを行う予定の無いアクターがいて、C++ コードを使用している場合は、そのアクターのコンストラクタに次の内容を組み込むことで、決してチックしないように設定することができます。
PrimaryActorTick.bCanEverTick = false;
アクターが一定時間のみチックする場合は、代わりに次をコンストラクタに組み込むことができます:
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = false;
関数「SetActorTickEnabled」を使用することで、チック機能を有効・無効に設定することが可能です。
もう一つ注目すべき項目が、「BlueprintTime」です。こちらは、 inclusive (coalesced) ビュー - 包括ビューに切り替えて、リスト内から探すのが一番簡単に表示できます。これにより全ての「BlueprintTime」項目がひとつの行に集約されます。「BlueprintTime」を選択したら、再び Hierarchical view - 階層ビューに切り替えることで、ブループリント コードが実行された全ての場所を選択する事ができます。これにより、どのブループリントにどれくらいの時間が使われたかを概ね確認できるようになります。
他によく問題を起こしやすいのが「TickWidgets」です。この数値が高い場合は、同時に表示されているウィジェットの数が多すぎるか、それらウィジェットの属性に使用されるデリゲートが複雑すぎるかのいずれかの問題になります。スレートの一部属性 (可視性など) は 1 フレーム毎に複数回コールされる場合があるので、小さくかつ素早く送り返せるようにしておくのが得策です。
ではゲームに大量のスケレタルメッシュがある場合はどうでしょうか。「SkinnedMeshComp チック」時間はコストがかさんでしまう場合があります。そこでプロフィールに出現するスケルトンのボーン数を減らすか、そのアニメーション ブループリントの複雑さを減らすことを考えてみましょう。もしスケレタルメッシュが見えない時にアニメーションを更新する必要が無い場合は、スケレタルメッシュの「MeshComponentUpdateFlag」を「OnlyTickPoseWhenRendered」に設定してみましょう。ただしこのフラグを有効化する場合は、これらメッシュがレンダリングされてない時には AnimNotifies が起動しませんので、この点にはご注意ください。
実は、定期的にフレームレートが突然落ちる問題を解決したいのですが。
タイムラインでスパイクになっている箇所を見つけ、その周辺のフレームを選択し、ビューを「Average」 (平均) から「Maximum」(最大) に切り替えるのが一番効果的です。これにより表示される全ての数値が、選択したフレーム範囲の平均値ではなく、最大値になります。
ありがとう、助かりました!
Profiler (プロファイラ) の使用はゲーム全体のパフォーマンスにおいて重要であり、実際の問題に関する憶測を避けることで、不要な面倒を避けることができます。詳しくはこのドキュメント ページをご覧ください。https://docs.unrealengine.com/latest/INT/Engine/Performance/Profiler/index.html