第005回 Tick関数がどう処理されているかUnrealC++を追ってみよう!
皆さんグーテンターク(ドイツのあいさつ)!
CC2TechBlog今回の担当は新人プログラマーの親泊です。
今回は毎フレーム呼ばれる関数Tickについてコードを追っていきます。
Tickの基本的な機能
UE4は非常に便利なことにアクタやアクタコンポーネントに毎フレーム呼ばれるTick関数が事前に仕込まれています。このTick関数のおかげで、ブループリントでティックイベントにノードをつないだり、C++でoverrideすることによって、自動的に毎フレーム処理を行ってくれます。
基本的な設定
Tickを回すフラグ
C++側からは,
PrimaryActorTick.bCanEverTick(アクタ)
PrimaryComponentTick.bCanEverTick(コンポーネント)
のフラグでTickを回すかを設定できます。
ただし、この設定はコンストラクタからしか設定できません。
ブループリント側からは、Start with Tick Enabledというフラグが、用意されています。
ただし、Primary(Actor/Component)Tick.bCanEverTickがtrueでないと変更してもTickは回りません。
Tickが回る間隔の設定
UE4ではTickが何秒に1回処理されるかを設定することができます。
C++側からは、
PrimaryActorTick.TickIntercal(アクタ)
PrimaryComponentTick.TickInterval(コンポーネント)
でTickが回る間隔を設定できます。
0秒に設定すると、毎フレーム処理されるようになります。FPSが60に設定されている場合は0.0166秒に1回処理されます。
ブループリント側からはそれぞれのTick設定欄のTick Interval (secs)で設定できます。
Tickが処理されるタイミングの設定
UE4ではTick処理のタイミングを設定できます。それはTickGroupとして定義されています。
PrePhysics
物理計算前に実行されるグループです。トランスフォーム情報の更新や、アニメーションのポーズ計算などが行われています。
StartPhysics
物理計算開始時に実行されるグループです。
DuringPhysics
物理計算実行時に実行されるグループです。光の反射やシーンキャプチャが行われています。また、コンポーネントのデフォルトはDuringPhysicsになっています。
EndPhysics
物理計算終了時に実行されるグループです。
PostPhysics
物理計算後に実行されるグループです。UPhysicsSpringComponentの処理がこのタイミングで行われています。
PostUpdateWork
全てのアップデート処理の後に実行されるグループです。パーティクルエミッターのTick処理が行われています。
LastDemotable
一番最後に実行されるグループです。
NewlySpawned
全てのティックグループの後に、新しくスポーンするものが無くなるまで繰り返し呼ばれるグループです。
設定を動的に切り替える
これらTickの設定は以下の関数にてランタイム中に変更することができます。ただし、Primary(Actor/Component)Tick.bCanEverTickがtrueでないと変更が反映されません。
設定内容 | アクタの関数 | コンポーネントの関数 |
Tick回すかどうかを変更 | SetActorTickEnabled() | SetComponentTickEnabled() |
Tick間隔の変更 | SetActorTickInterval() | SetComponentTickInterval() |
処理グループの変更 | SetActorTickGroup() | SetComponentTickGroup() |
しかし、便利なものは扱いに注意しなければならないものです。Tick関数は毎フレーム処理を行うので、簡単にゲームのパフォーマンスを圧迫します。
“なにもしない”Tick関数でテストしてみる
Tickのオーバーヘッドを測るために、簡単な実験をしてみます。
実験環境は、
CPU: Intel Xeon CPU E5-1630 v4
RAM: 32GB
GPU: NVIDIA GeForce GTX 1060 6GB
OS: Windows 10 pro
UE4: Unreal Engine 4.21.2 Development Editor
で行いました。
仮に以下のようなTick関数を定義してみます。
ATickTestActor::Tick(float)でAActor::Tick(float)を呼び出すだけの関数です。
このアクタをとりあえず5万個レベルに配置します。そして、Stat Unitコマンドで処理負荷を計測しました。
明らかにGameの値が爆発的に上昇しています。
同様にアクタコンポーネントでもためしてみます。
アクタコンポーネントを5万個つけたアクタを1つ配置します。
アクタを5万個置いたときよりは処理負荷が低いものの、やはり爆発的に上昇しています。このようにスーパークラスのTick処理を走らせただけでそこそこ重くなるので、Tick関数を持つアクタやコンポーネントをむやみに増やすのは避けるべきといえます。
実際に私が関わったプロジェクトでは、アクタが5000~10000個配置され、コンポーネントも5~15個ほどくっついていました。5万回は多いと思われるかもしれませんが、大きなレベルを作ると簡単に超えてしまう可能性があります。上記のPrimary(Actor/Component)Tick.bCanEverTickでTickの処理をふさいでしまうのも有効だと思います。
アクタとアクタコンポーネント間でのTick関数の扱い
もう一つ、Tick関数は完全に独立したタスクとして機能しています。※後述
私は当初アクタのTickを停止させると、子に連なるコンポーネントのTickも停止すると勘違いしていたのですが、実際にはTick関数の制御は個々のアクタとコンポーネントを操作しなければなりません。
実際の例を見てみましょう。
分かりやすいように画面にアクタによるTick関数か、アクタコンポーネントによるTick関数かを表示するようにします。緑で表示されているのがアクタのTick関数、青で表示されているのがアクタコンポーネントのTick関数です。
まず、上記のアクタにアクタコンポーネントをくっつけてみます。
両方よばれていますね。
では今度は、アクタの方はTick関数が動かないようにしてみます。
アクタのTick関数が呼ばれなくても、アクタコンポーネントのTick関数は呼ばれています。つまり、アクタとアクタコンポーネントのTick関数には相関関係はありません。それぞれが独立して動いています。
ではなぜこうなっているのかをUE4のコードから読みとっていきましょう。
//———————————————–
// ここからプログラマ向け
//———————————————–
Tick処理を実行しているクラス・構造体達
まず、Tick関数にかかわるclassを紹介していきましょう。
1.FTickFunction
実際に走るTick処理のインターフェースです。
TickGroupやbCanEverTick、ExcuteTick()などのTickを行うタイミングや実行関数を保持しております。
この構造体の継承先で実際の振舞いやインスタンスクラスが設定されています。
2.FTickFunctionTask
FTickFunctionのExcuteTickを実行するタスクとして設定するクラスです。
メンバ変数としてFTickFunction* Targetを保持しており、DoTask()内でFTickFunction::ExcuteTick()を実行する形でタスクとして積まれたTick処理を実行しています。
3.FTickTaskSequencer
Tick処理のタスクの流れを整理し、FTickFunctionTaskをタスクキューへ登録するクラスです。
4.FTickTaskLevel
レベルに存在するFTickFunctionをTSetの配列として保持し、Tick処理の追加、破棄、有効化、無効化を行い、Tick処理全体の流れを制御しています。
5.FTickTaskManager
FTickTaskLevelの管理、FTickFunctionのキューを実行します。
Tickタスクの処理の流れ
まず、Tickのイベントを回すためにはAActorのメンバ変数PrimaryActorTick.bCanEverTickをtrueにする必要があります。
AActorのデフォルトはfalseです。
Tickが回ると重い。古事記にもそう書いてある。
0.BeginPlayでシーケンサーにTick関数を登録する
Tick関数が実行されるためには、TickタスクがTickTaskSequencerに積まれる必要があります。アクタのBeginPlayではRegisterAllActorTickFunctionsとRegisterAllComponentTickFunctionsが実行され、FTickTaskLevelのメンバ変数AllEnabledTickFunctionsに登録されます。
1.StartFrameでTickタスクをタスクキューに積む
FTickTaskManager::StartFrame(InWorld, DeltaSeconds, TickType, LevelsToTick)でFTickTaskSequencer::TickCompletionEvents[TG_MAX]をクリアします。
全てのタスクをクリアしたら、その後FTickTaskLevel::QueueAllTicks()でFTickTaskSequencer::TickCompletionEvents[TG_MAX]の各TickGroupに対応した配列にTickタスクが積まれていきます。
TickFunctionがキューに積まれるときは、FTickTaskManager::StartFrame(InWorld, DeltaSeconds, TickType, LevelsToTick)でGameThreadが指定されています。
アクタのTick処理は必ずGameThreadで実行されてしまうので、アクタのTick処理が多いと、ゲーム全体がカクついてしまいます。
2.全TickGroupのTick処理を行う
UWorld::RunTickGroup(Group, bBlockTillComplete)を全TickGroup分走らせます。
UWorld::RunTickGroup(Group, bBlockTillComplete)では積まれたタスクが順次実行されます。
FTickFunctionTask::DoTask(CurrentThread, MyCompletionGraphEvent)が呼ばれます。
タスクが実行されると、TGraphTask::DoTask(CurrentThread, Subsequents)でタスクの処理が走ります。
FActorTickFunction::ExcuteTick(DeltaTime, TickType, CurrentThread, MyCompletionGraphEvent)からAActor::Tick(DeltaTime)が実行されます。
以上のようにTick関数は独立したタスクとして、毎フレーム振り分けられて処理されています。処理順番は、各TickGroup内でBeginPlay()が走った順番です。
毎フレーム振り分けるメリットはランタイム中にTickGroupを変更できる点とBeginPlay()が走った後は再登録が必要なくなるという点だと思います。
BlueprintのTickイベント
BlueprintのTickノードはC++側ではReceiveTick(DeltaTime)として実装されています。
このReceveTickはAActor::Tick(DeltaTime)の最初に呼ばれています。
まとめ
Tick関数はFTickFunctionクラスがタスクとしてキューに積まれて呼び出されています。そのため、アクタ・コンポーネント同士で不用意に値を上書きしあったり、処理順番に依存したりする処理はTick関数で処理するべきではありません。
また、Tick関数の制御が各オブジェクトで独立しており、Tickそのものにもオーバーヘッドが存在しているので、本当に毎フレーム必要な処理なのかを検討したり、必要のないTick関数はちゃんと回らないように管理しなければならないです。
不用意にアクタやコンポーネントを増やすとパーフォーマンスや制御にも影響がでるので、慎重に運用しましょう。
参考
公式ドキュメントは偉大です。
アクタのティック | Unreal Engine
おわりに
さて、今回のTechBlogいかがだったでしょうか?
今回はUnrealC++のコードを追って調べました。
UE4はエンジンソースを配布しているので、例え公式ドキュメントに詳細が載っていなくてもコードを追って調べられます。(脳筋)
入社してからコードを読む量が圧倒的に増えました。
プログラマーはコードを書くよりも読む方がメインの仕事だと実感しています。
皆さんのご意見、ご要望をお待ちしています。下記 CC2技術ブログのメッセージフォームにて、お気軽にお問い合わせください。
関連記事
-
第001回 UE4のコンテンツブラウザフィルターを自作する
はじめまして サイバーコネクトツーでツール系テクニカルアーティストをしております …
-
第004回 UE4のアセットを一括修正する
ツール系テクニカルアーティストのしばはらです。 プロジェクトが進んで、ある程度ア …
-
第003回 UE4のショートカットキーの仕組み
今回は ツール系テクニカルアーティスト しばはらがお送りします。 突然ですが、P …
-
第006回 コマンドラインからConfig設定を上書きする時の注意点
皆さん、こんにちは。サイバーコネクトツーでテクニカルサポートのマネージャーをして …
-
第008回:ゲームクリエイターは破壊表現が好き。
皆さんボンジョルノ(イタリアのあいさつ)! 乱読派プログラマ親泊です。 現代ゲー …
-
第002回 アクタのトランスフォームの実態
皆さんストラーズドヴィーチェ(ロシアのあいさつ)! 今回の担当は、新人プログラマ …
-
第007回UE4のアセットの参照方法について、そのロードの違い
皆さんボンジュール(フランスのあいさつ)! ランダムのスペルが覚えられないプログ …
- PREV
- 東京スタジオデジタルゲーム大会
- NEXT
- 東京スタジオお花見日和