139.3K Views
January 05, 22
スライド概要
講演動画 https://www.youtube.com/watch?v=YL6Ifc4VVV8
2021年11月27-28日に開催されたCEDEC+KYUSHU 2021 ONLINEの講演資料です
Unreal Engine 4(UE4)は現在最新リリースの4.27が最後のバージョンであり、正式リリースのUE5にも繋がる予定です。そこで、本講ではUE4.27で活用できる開発Tipsについてご紹介いたします。
http://cedec-kyushu.jp/2021/session/23.html
Unreal Engineを開発・提供しているエピック ゲームズ ジャパンによる公式アカウントです。 勉強会や配信などで行った講演資料を公開しています。 公式サイトはこちら https://www.unrealengine.com/ja/
UnrealEngineにおける マルチスレッディング入門 タスクグラフ編
はじめに ● UnrealEngineは可能な限り少ない計算量で豊かな表現を行えるエンジン ● 開発者はゲームのロジックやアートの作りこみに集中できる マルチスレッディングに関してデフォルトでは保守的な動作 ゲームスレッドに負荷が蓄積してしまうことも
本セクションについて マルチスレッディングを制御する仕組みである 「タスクグラフ」について学習し 最適化の一つの手段として活用しましょう
覚えおいて欲しいこと ● 良いパフォーマンスを達成するために大切なのは、 適切なデータ構造を適用し、不要な計算をできるだけ省くこと マルチスレッド化は、その次の次あたりに検討すべき課題で 決して銀の弾丸ではない ● プラットフォームによっては、あまりタスクグラフスレッドが走る余地がな い場合もありえるので注意
UEの(主な)マルチスレッド活用方法 ● TaskGraph ● ● 主にワールドのTickに合わせて同期を行いながらタスクを実行する ● ActorやコンポーネントのTickなど PoolThread ● Tickと同期せずにフレームをまたがるような処理を行う ● 例 ) アセットのデコード、ランタイムで大きなデータの生成処理
TaskGraphとは ● ● アクターやコンポーネントを更新する処理(タスク)を 任意の順序・タイミング、スレッドで実行するUnrealEngineの中核システム 対象のオブジェクト・実行する関数を指定して タスクグラフキューに登録することで処理が行われる MyCharacter TickActor() EnemyCharacter GraphTask GameThread GraphTask TickActor() TaskGraph GoodComponent GraphTask TickComponent() TaskGraph RenderThread NiceComponet GraphTask TickComponent() PostTick() RHIThread GraphTask
TaskGraphを利用するための二つの方法 直接TGraphTaskを生成して実行 FTickFunctionを介して登録
FTickFunctionについて ● ● ● ● タスクグラフにまつわる処理をラップしたユーティリティ構造体 TickTaskManagerにTickFunctionを登録して処理を代行してもらう 内部ではGraphTaskを生成し登録している オブジェクトに対して各Tick毎に実行するべき関数が確定している場合、 FTickFunctionを用いる方法がもっとも便利 TickTaskManager
FTickFunctionによるマルチスレッド化 ● ● FTickFunction構造体の bRunOnAnyThread をtrueにするだけでOK 以下のコードは自作したActorComponentのPrimimaryComponentTickをタ スクグラフスレッドに分配するサンプル UMyActorComponent::UMyActorComponent(const FObjectInitializer& ObjectInitializer /*= FObjectInitializer::Get()*/) : Super(ObjectInitializer) { PrimaryComponentTick.bCanEverTick = true; PrimaryComponentTick.bRunOnAnyThread = true; } GAME THREAD TASKGRAPH Actor::Tick Component:: Tick GAME THREAD TASKGRAPH Actor::Tick Component:: Tick
外部参照に気を付ける MyActorのメンバ変数を参照し てコンポーネントを更新する メンバ変数を更新 GAME THREAD MyActor::Tick MyComponent::Tick MyActor::Tick 参照 TASKGRAPH ● TaskGraphに分散していなければ、 二つのTickは同時に行われないので問題なし MyComponent::Tick 参照
外部参照に気を付ける ● MyComponentのTickをTaskGraphに分散した場合 GAME THREAD MyActor::Tick MyActor::Tick 参照 TASKGRAPH MyComponent::Tick 参照 MyComponent::Tick 書き換え中の変数を参照したり、タイミングのズレによって更新前のデータを 参照してしまう可能性がある その結果再現性の低いバグやクラッシュを呼び込んでしまう
外部参照に気を付ける ● タスクの前提条件を設定して依存するTickが同時に起こらないようにする GAME THREAD TASKGRAPH MyActor::Tick MyActor::Tick MyComponent::Tick MyComponent::Tick パラメータを参照している MyActor::Tick が終ってから、 実行を開始する
前提条件(Prerequisite)の記述 ● ● 前提条件となるTickFunctionをAddPrerequisite関数で登録する 複数の前提条件を設定することも可能 UMyActorComponent* MyComponent = InMyComponent; MyComponent->PrimaryComponentTick.AddPrerequisite(Character, Character->PrimaryActorTick);
新規にFTickFunctionを追加する
オブジェクトに対して追加のTickFunctionを登録する場合は
FTickFunctionを継承した構造体を宣言
//追加のTickFunction
USTRUCT()
struct FMyExtraTickFunction : public FTickFunction
{
GENERATED_USTRUCT_BODY()
...
}
//宣言した構造体がコピーできないことを宣言する
template<>
struct TStructOpsTypeTraits <FMyExtraTickFunction > : public TStructOpsTypeTraitsBase2 <FMyExtraTickFunction >
{
enum{ WithCopy = false
}; //FMyComponentExtraTickFunction はコピーできない
};
構造体をメンバ変数として追加し、初期設定が終わった自作FTickFunctionを
オーバーライドしたRegisterComponentTickFunctionsで登録します。
※UCharacterMovementComponent::RegisterComponentTickFunctions等を参照してください
ブループリントとマルチスレッド化 やはりc++じゃなきゃいけないのね・・・ ブループリントで書かせて・・・ ブループリントのコードも マルチスレッド化が可能です!
ブループリントとマルチスレッド化 GAME THREAD MyActor:: Tick MyActor:: PostTick BP Event TASKGRAPH BP Event MyActor:: AsyncTick BP Event
BPをマルチスレッド実行する時の注意点 ● 全てのノードがマルチスレッドセーフで動くわけではない ● ● ● ● ● ✕GameThread上で実行されることを前提とするノード ✕内部的に他のオブジェクトを書き換えるようなノード マルチスレッド化されている部分では素性のはっきりしたノードだけを利用することを推奨 CriticalSectionなどの同期オブジェクトのサポートはありません const関数など、書き換えに関する制約の仕組みがありません あとからブループリントで作成された複雑な処理をマルチスレッド化するのは なかなか難しいので、設計段階から想定しておくことをお勧めします
直接TGraphTaskを生成する ● ● ● 任意の関数を各TickFunctionからTaskgraphに乗せて実行する 要求に応じて生成したりしなかったりすることが可能 なにもしないとTickgroupの完了条件に同期しないので、一般的には呼び出 し元のTickFunctionの終了条件に相乗りする GAME THREAD Actor::Tick ActorB::Tick ActorC::Tick Task 生成 TASKGRAPH TGraphTask Actor::Tickの nulltask Nulltaskは同期用の空タスク 終了条件を設定されたときに 自動で作成される このnulltaskが完了して初めて、 Actor::Tickが完了したことになる
TGraphTaskを実装する
●
まずタスクの宣言を含むクラスを作成します
タスク宣言のコンストラクタ
更新処理に必要な引数を受け取り、
メンバ変数に保存しておきます
class FSampleTask
{
UMyComponent* TargetComponent;
public:
Statsでの集計に使われる定義
FORCEINLINE FSampleTask(UMyComponent* InComponent):TargetComponent(InComponent){}
static FORCEINLINE TStatId GetStatId()
{
RETURN_QUICK_DECLARE_CYCLE_STAT(FSampleTask, STATGROUP_TaskGraphTasks);
どのスレッドで動作するかなどを設定する
}
先頭にAnyがついているものは
FORCEINLINE ENamedThreads::Type GetDesiredThread()
タスクグラフスレッドで実行されることを示す
{
return ENamedThreads::AnyHiPriThreadNormalTask;
}
static FORCEINLINE ESubsequentsMode::Type GetSubsequentsMode()
タスクグラフの同期システムを利用するかどうか
{
独自の同期を利用する場合FireAndForgetに設定
return ESubsequentsMode::TrackSubsequents;
}
void DoTask(ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{
TargetComponent->DoSomething();
}
対象のオブジェクトに対する任意の関数を呼び出す
};
TGraphTaskを実装する2
●
FTickFunctionで呼び出された関数の中で実行され
ます
TickFunctionの任意のタイミングで
先ほどの宣言を使ってタスクを作成しキューに登録する
タスクのインスタンスを生成する
第一引数 Prerequisites
//一つのタスクをキューに入れる
第二引数 どのスレッドから生成されたか
void UMyComponent::DispatchSingleTask(float DeltaTime, FTickFunction& ThisTickFunction)
{
FGraphEventRef EventRef = TGraphTask<FMyFirstTask>::CreateTask(nullptr,ENamedThreads::GameThread).
ConstructAndDispatchWhenReady(*this, DeltaTime, Argument2 );
タスク宣言クラスのコンストラクタ引数
ThisTickFunction.GetCompletionHandle()->DontCompleteUntil(EventRef);
}
任意のパラメータの受け渡しができる
//二つの連続したタスクをキューに入れる
void UMyComponent::Dispatch2ChainedTasks(float DeltaTime, FTickFunction& ThisTickFunction)
このタスクが終わるまで、ThisTickFunctionの完
{
了イベントを延期するように指示
FGraphEventRef EventFirstRef = TGraphTask<FMyFirstTask>::CreateTask(nullptr,ENamedThreads::GameThread).
ConstructAndDispatchWhenReady(*this, DeltaTime);
FGraphEventArray Prerequisites;
複数のGraphTaskを繋ぐ場合、この形で
Prerequisites.Add(EventFirstRef);
FGraphEventRef EventSecondRef = TGraphTask<FMySecondTask>::CreateTask(&Prerequisites,ENamedThreads::GameThread).
Prerequisitesを設定する
ConstructAndDispatchWhenReady(*this);
ThisTickFunction.GetCompletionHandle()->DontCompleteUntil(EventSecondRef);
}
マルチスレッド化検討フローチャート GameThread上での動作を前提とした関 数やサービスを利用している NO 不可 可能ならば処理の一部を 分離する YES 基本的に不可 NO YES マルチスレッドセーフな他のオブジェ クトやアクターの情報、共有されたシ ステムにのみアクセスする YES 別アクターが管理しているパラメータや コンポーネントを書き換える 自分自身の変数にのみアクセスする? NO NO YES 他のスレッドで処理を行う アクターやコンポーネントを参照する NO オブジェクト自身の書き換え中に他の スレッドから参照される YES NO YES 処理順が影響することがある 必要あらばPrerequisiteを 設定する 同時処理されないように Prerequisiteを設定する 可 タスクグラフスレッドに 処理を委譲できる
Managerを利用したマルチスレッド化のコード ● ● ● FParticleSystemWorldManagerの例 コンポーネントなどのTickをマネージャーに登録してまとめて一つの GraphTaskで処理してしまう方法 TaskGraphのディスパッチや同期のオーバーヘッドを大きく軽減できるの で、パーティクルなどの大量の更新処理を捌く時に有用 Manager Component Manager::Tick Component Component Component Component TASKGRAPH Tick Tick Tick Tick Tick
ParallelForでお手軽マルチスレッド
ParallelForを用いるとラムダ関数を使って簡単にタスクグラフを利用したマルチ
スレッド化が可能です。
ある程度大きい単位でマルチスレッド化できるとより効果的です。
::ParallelFor(NumberOfTraces,
[this,World,&Transform, &QueryParams, &OutHitResult, &OutHit](const int32 Index)
{
int X = Index % TraceNumberOnAnAxis - (TraceNumberOnAnAxis / 2 );
int Y = Index / TraceNumberOnAnAxis - (TraceNumberOnAnAxis / 2 );
const FVector Start = Transform.TransformPosition( FVector( X * 100.0f, Y * 100.0f, 0 ) );
const FVector End
= Transform.TransformPosition( FVector( X * 100.0f, Y * 100.0f, -500.0f ) );
OutHit[Index] = World->LineTraceSingleByChannel(
OutHitResult[Index], Start, End, ECC_WorldStatic, QueryParams);
});
ParallelForでお手軽マルチスレッド2
分離可能な処理をParallelForを使って分散することも可能です。
本日紹介した方法の中ではコード量が極端に小さくて済むので
かなりお手軽にマルチスレッド化が可能です。
::ParallelFor(2, [this](const int32 Index){
if( Index == 0 )
{
this->DoSomethingOnThisThread(); //このスレッド上で実行される
}else
{
this->DoSomethingOnAnyThread(); //TaskGraphスレッド上で実行される
}
});
DoSomethingUsingTheResult(); //結果を使って何かする
最後に 『FINAL FANTASY VII REMAKE』におけるプロファイリングと最適化事例 https://www.youtube.com/watch?v=naaDgfKKzFU がとても良い事例となっておりますので是非ご参照ください!