CC2の楽屋裏

ゲーム制作会社サイバーコネクトツー公式ブログ

*

第005回 Tick関数がどう処理されているかUnrealC++を追ってみよう!

   

皆さんグーテンターク(ドイツのあいさつ)!
CC2TechBlog今回の担当は新人プログラマーの親泊です。
今回は毎フレーム呼ばれる関数Tickについてコードを追っていきます。

Tickの基本的な機能

UE4は非常に便利なことにアクタやアクタコンポーネントに毎フレーム呼ばれるTick関数が事前に仕込まれています。このTick関数のおかげで、ブループリントでティックイベントにノードをつないだり、C++でoverrideすることによって、自動的に毎フレーム処理を行ってくれます。

基本的な設定

Tickを回すフラグ

PrimaryTick

C++側からは,
PrimaryActorTick.bCanEverTick(アクタ)
PrimaryComponentTick.bCanEverTick(コンポーネント)
のフラグでTickを回すかを設定できます。
ただし、この設定はコンストラクタからしか設定できません。

BPflag

ブループリント側からは、Start with Tick Enabledというフラグが、用意されています。
ただし、Primary(Actor/Component)Tick.bCanEverTickがtrueでないと変更してもTickは回りません。

Tickが回る間隔の設定

UE4ではTickが何秒に1回処理されるかを設定することができます。

TickInterval

C++側からは、
PrimaryActorTick.TickIntercal(アクタ)
PrimaryComponentTick.TickInterval(コンポーネント)
でTickが回る間隔を設定できます。
0秒に設定すると、毎フレーム処理されるようになります。FPSが60に設定されている場合は0.0166秒に1回処理されます。

BPInterval

ブループリント側からはそれぞれの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)を呼び出すだけの関数です。

TestTickActor

このアクタをとりあえず5万個レベルに配置します。そして、Stat Unitコマンドで処理負荷を計測しました。

TickPerformance

明らかにGameの値が爆発的に上昇しています。

同様にアクタコンポーネントでもためしてみます。

TickTestActorComponent

アクタコンポーネントを5万個つけたアクタを1つ配置します。

TickComponentPerformance

アクタを5万個置いたときよりは処理負荷が低いものの、やはり爆発的に上昇しています。このようにスーパークラスのTick処理を走らせただけでそこそこ重くなるので、Tick関数を持つアクタやコンポーネントをむやみに増やすのは避けるべきといえます。

実際に私が関わったプロジェクトでは、アクタが5000~10000個配置され、コンポーネントも5~15個ほどくっついていました。5万回は多いと思われるかもしれませんが、大きなレベルを作ると簡単に超えてしまう可能性があります。上記のPrimary(Actor/Component)Tick.bCanEverTickでTickの処理をふさいでしまうのも有効だと思います。

アクタとアクタコンポーネント間でのTick関数の扱い

もう一つ、Tick関数は完全に独立したタスクとして機能しています。※後述
私は当初アクタのTickを停止させると、子に連なるコンポーネントのTickも停止すると勘違いしていたのですが、実際にはTick関数の制御は個々のアクタとコンポーネントを操作しなければなりません。
実際の例を見てみましょう。

分かりやすいように画面にアクタによるTick関数か、アクタコンポーネントによるTick関数かを表示するようにします。緑で表示されているのがアクタのTick関数、青で表示されているのがアクタコンポーネントのTick関数です。

AddDebugMessage

まず、上記のアクタにアクタコンポーネントをくっつけてみます。

CallActorAndComponent

両方よばれていますね。
では今度は、アクタの方はTick関数が動かないようにしてみます。

CallComponentOnly

アクタの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のキューを実行します。

TickClasses

 

Tickタスクの処理の流れ

まず、Tickのイベントを回すためにはAActorのメンバ変数PrimaryActorTick.bCanEverTickをtrueにする必要があります。
tb004_11

AActorのデフォルトはfalseです。
Tickが回ると重い。古事記にもそう書いてある。

0.BeginPlayでシーケンサーにTick関数を登録する

Tick関数が実行されるためには、TickタスクがTickTaskSequencerに積まれる必要があります。アクタのBeginPlayではRegisterAllActorTickFunctionsとRegisterAllComponentTickFunctionsが実行され、FTickTaskLevelのメンバ変数AllEnabledTickFunctionsに登録されます。

RegisterTickFunction

1.StartFrameでTickタスクをタスクキューに積む

FTickTaskManager::StartFrame(InWorld, DeltaSeconds, TickType, LevelsToTick)でFTickTaskSequencer::TickCompletionEvents[TG_MAX]をクリアします。
tb004_18

全てのタスクをクリアしたら、その後FTickTaskLevel::QueueAllTicks()でFTickTaskSequencer::TickCompletionEvents[TG_MAX]の各TickGroupに対応した配列にTickタスクが積まれていきます。
tb004_19

TickFunctionがキューに積まれるときは、FTickTaskManager::StartFrame(InWorld, DeltaSeconds, TickType, LevelsToTick)でGameThreadが指定されています。
アクタのTick処理は必ずGameThreadで実行されてしまうので、アクタのTick処理が多いと、ゲーム全体がカクついてしまいます。
tb004_26

2.全TickGroupのTick処理を行う

UWorld::RunTickGroup(Group, bBlockTillComplete)を全TickGroup分走らせます。
tb004_20

UWorld::RunTickGroup(Group, bBlockTillComplete)では積まれたタスクが順次実行されます。
tb004_22
FTickFunctionTask::DoTask(CurrentThread, MyCompletionGraphEvent)が呼ばれます。
タスクが実行されると、TGraphTask::DoTask(CurrentThread, Subsequents)でタスクの処理が走ります。
tb004_23
FActorTickFunction::ExcuteTick(DeltaTime, TickType, CurrentThread, MyCompletionGraphEvent)からAActor::Tick(DeltaTime)が実行されます。
tb004_24
tb004_25

以上のようにTick関数は独立したタスクとして、毎フレーム振り分けられて処理されています。処理順番は、各TickGroup内でBeginPlay()が走った順番です。
毎フレーム振り分けるメリットはランタイム中にTickGroupを変更できる点とBeginPlay()が走った後は再登録が必要なくなるという点だと思います。

BlueprintのTickイベント

BlueprintのTickノードはC++側ではReceiveTick(DeltaTime)として実装されています。
tb004_01

このReceveTickはAActor::Tick(DeltaTime)の最初に呼ばれています。
tb004_02

まとめ

Tick関数はFTickFunctionクラスがタスクとしてキューに積まれて呼び出されています。そのため、アクタ・コンポーネント同士で不用意に値を上書きしあったり、処理順番に依存したりする処理はTick関数で処理するべきではありません。
また、Tick関数の制御が各オブジェクトで独立しており、Tickそのものにもオーバーヘッドが存在しているので、本当に毎フレーム必要な処理なのかを検討したり、必要のないTick関数はちゃんと回らないように管理しなければならないです。

不用意にアクタやコンポーネントを増やすとパーフォーマンスや制御にも影響がでるので、慎重に運用しましょう。

参考

公式ドキュメントは偉大です。
アクタのティック | Unreal Engine

おわりに

さて、今回のTechBlogいかがだったでしょうか?
今回はUnrealC++のコードを追って調べました。
UE4はエンジンソースを配布しているので、例え公式ドキュメントに詳細が載っていなくてもコードを追って調べられます。(脳筋)
入社してからコードを読む量が圧倒的に増えました。
プログラマーはコードを書くよりも読む方がメインの仕事だと実感しています。

皆さんのご意見、ご要望をお待ちしています。下記 CC2技術ブログのメッセージフォームにて、お気軽にお問い合わせください。

「CC2技術ブログ」へのご意見・ご要望はこちらから

 - CC2技術ブログ , ,

  関連記事

第001回 UE4のコンテンツブラウザフィルターを自作する

はじめまして サイバーコネクトツーでツール系テクニカルアーティストをしております …

tb004_09
第004回 UE4のアセットを一括修正する

ツール系テクニカルアーティストのしばはらです。 プロジェクトが進んで、ある程度ア …

第003回 UE4のショートカットキーの仕組み

今回は ツール系テクニカルアーティスト しばはらがお送りします。 突然ですが、P …

CC2技術ブログ
第006回 コマンドラインからConfig設定を上書きする時の注意点

皆さん、こんにちは。サイバーコネクトツーでテクニカルサポートのマネージャーをして …

第008回:ゲームクリエイターは破壊表現が好き。

皆さんボンジョルノ(イタリアのあいさつ)! 乱読派プログラマ親泊です。 現代ゲー …

tb002_04
第002回 アクタのトランスフォームの実態

皆さんストラーズドヴィーチェ(ロシアのあいさつ)! 今回の担当は、新人プログラマ …

スクリーンショット (47)
第007回UE4のアセットの参照方法について、そのロードの違い

皆さんボンジュール(フランスのあいさつ)! ランダムのスペルが覚えられないプログ …