Let's Enjoy Unreal Engine

Unreal Engineを使って遊んでみましょう

UE4 Significance Manager(重要性管理)を使ってTickを抑制する ※サンプル配布あり

今年のGDCUnreal FestなどでEpic Gamesの講演で何度か出てきている、Significance Manager(重要性管理)という仕組みがあります。これは実際にフォートナイトなどで使われており、非常に実用性が高いものです。

www.slideshare.net

上記から引用。

f:id:alwei:20181113224209p:plain

ふむふむ、なるほど…なんとなく何をやっているのかはわかるぞ。

ただ、これまで具体的な情報は一切なく、どのように使うのかも謎でした。調べてみたら、いつのまにかドキュメントがあったようです。

The Significance Manager

正直に言ってこれだけを読んでも使い方は全然わかりません。というわけで実際に使ってみました。そしてそれを試してみたサンプルプロジェクトを丸ごとGitHubで公開します。

github.com

このプロジェクトはC++プロジェクトとなっており、UE4.21とVisual Studio 2017環境での動作確認を行っています。では実際に使ってみた時のポイントや注意点を解説していきます。

Significance Manager(重要性管理)とは

Significance Managerとはその名前の通り、重要性の高いものを管理するための仕組みです。つまり重要性が高いものほど、ゲーム中積極的に更新させたり動作を行わせる必要があります。

この場合、重要性が高いものというのは、プレイヤーとの距離が近く、かつカメラの向きの正面に存在するものとなることが一般的です。これらにスコアをつけておき、そのスコアに応じてそれらの処理や挙動をカスタマイズさせるというものです。

ただし、実際にどのように使うのか全く説明がないので、実際に調べてみる必要があります。最初のセットアップなどについては公式にドキュメントにも書かれているので、割愛します。

最低限やる必要があるのは、Significance Managerプラグインをオンにするという事と、"Build.cs"に"SignificanceManager"を追加しておく事です。

実装内容解説

実際に実装している中身についての解説です。

"SignificanceTestGameModeBase.cpp"に必要な処理はほとんど書かれています。

SignificanceTestGameModeBase.cpp の BeginPlay関数

void ASignificanceTestGameModeBase::BeginPlay()
{
	// レベル上にいるSignificanceActorを配列にセット
	TArray<AActor*> ManageActors;
	UGameplayStatics::GetAllActorsOfClass(this, ASignificanceActor::StaticClass(), ManageActors);

	if (UWorld* World = GetWorld())
	{
		if (USignificanceManager* SignificanceManager = FSignificanceManagerModule::Get(World))
		{
			FName Tag(TEXT("Actor"));
			for (auto Actor : ManageActors)
			{
				// 配列に存在するアクターをSignificanceManagerに登録
				SignificanceManager->RegisterObject(Actor, Tag, MySignificanceFunction, USignificanceManager::EPostSignificanceType::Sequential, MyPostSignificanceFunction);
			}
		}
	}
}

SignificanceManager->RegisterObjectで登録を行っています。事前に登録したいアクタークラスを収集しておきます。SignificanceManager->RegisterObjectにはアクターの登録の他に第3引数"FSignificanceFunction"と第5引数"FPostSignificanceFunction"の登録が必要です。

これらはデリゲート関数として登録され、実際に重要性管理の主要部分を実行します。

FSignificanceFunction

この関数は実際にSignificanceManagerで各オブジェクトに対してスコアを書き込む評価関数です。これは独自の関数をユーザー側で実装する必要があります。スコアはreturnでの戻り値がそのままオブジェクトに対して適用されるようになっています。

SignificanceTestGameModeBase.cpp の MySignificanceFunction関数

// 基準となるSignificance距離値
static const float GSignificanceDistance = 2000.f;

float MySignificanceFunction(USignificanceManager::FManagedObjectInfo* Obj, const FTransform& InTransform)
{
	// 関数内はSignificanceManagerからParallelForによって、並列処理で呼ばれるので注意
	if (ASignificanceActor* Actor = Cast<ASignificanceActor>(Obj->GetObject()))
	{
		// プレイヤーと自分の距離からSignificance計算
		FVector Distance = InTransform.GetLocation() - Actor->GetActorLocation();
		if (Distance.Size() < GSignificanceDistance)
		{
			float Significance = 1.f - Distance.Size() / GSignificanceDistance;
			return Significance;
		}
	}

	return 0.f;
}

注意点としてはこの関数はUE4が用意している並列処理を実行するためのParallelForによって実行されており、実行順序が不定です。基本的に何かに書き込むような操作はアウトだと思ってください。おそらく大量に実行される可能性があるからこのようになっているのかと。

FPostSignificanceFunction

上記のFSignificanceFunctionの関数の後に呼び出される関数です。こちらは結果を受け取り、後処理をする事が可能です。この関数内で実際に評価値を使ってアクターのTickをオフにしています。

SignificanceTestGameModeBase.cpp の MyPostSignificanceFunction関数

void MyPostSignificanceFunction(USignificanceManager::FManagedObjectInfo* Obj, float OldSignificance, float Significance, bool bUnregistered)
{
	if (ASignificanceActor* Actor = Cast<ASignificanceActor>(Obj->GetObject()))
	{
		if (Significance > 0.f)
		{
			// Significanceが0よりも上の場合はTickをオンにし、TextColorを赤に変更
			Actor->SetActorTickEnabled(true);
			Actor->SetTextColor(FColor::Red);
		}
		else
		{
			// Significanceが0になった場合はTickをオフにし、TextColorを白に変更
			Actor->SetActorTickEnabled(false);
			Actor->SetTextColor(FColor::White);
		}
	}
}

ただし、SignificanceManager->RegisterObjectの第4引数が"USignificanceManager::EPostSignificanceType::Sequential"を指定しておかないとParallelForによって並列に処理される可能性があります。

次に更新の仕方についてです。

SignificanceTestGameModeBase.cpp の Tick関数

void ASignificanceTestGameModeBase::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (UWorld* World = GetWorld())
	{
		if (USignificanceManager* SignificanceManager = FSignificanceManagerModule::Get(World))
		{
			if (APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(World, 0))
			{
				TArray<FTransform> TransformArray;
				TransformArray.Add(PlayerPawn->GetActorTransform());

				// PlayerPawnをViewPointとして設定
				SignificanceManager->Update(TArrayView<FTransform>(TransformArray));
			}
		}
	}
}

SignificanceManager->Updateで、プレイヤーのトランスフォーム情報を渡して判断材料にしています。ここで渡す、TArrayViewに設定されたトランスフォームの個数だけ、各オブジェクト全て評価関数であるFSignificanceFunctionとFPostSignificanceFunctionが呼び出されます。

この例ではプレイヤーだけなので、各オブジェクトに対しては1度しか呼び出されませんが、もし複数渡した場合には評価関数をどのように実装するかしっかり考えておく必要があります。

あとは各アクターやオブジェクト内で、Significanceをどのように利用するかです。

ASignificanceActor.cpp の Tick関数

void ASignificanceActor::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime);

	if (UWorld* World = GetWorld())
	{
		if (USignificanceManager* SignificanceManager = FSignificanceManagerModule::Get(World))
		{
			// Significanceを取得し、TextRenderに表示
			float Significance = SignificanceManager->GetSignificance(this);
			SignificanceText->SetText(FText::AsNumber(Significance));
		}
	}
}

これは実際に登録されたアクター内のTick処理ですが、単純にSignificanceManagerからGetSignificanceでSignificanceを取得したものをテキストとして表示しています。

テスト動画。


f:id:alwei:20181113233217p:plain

上記のような動作となりました。あとはサンプルを見ていただければ実際にどのように使えばいいのかはわかると思います。

総評として、SignificanceManagerを上手く使えば、アクターのTickなどを効率よく抑制したり、重要なものだけを動かしたいなど、色々なものに応用可能です。ゲームの動作を最適化したい場合にはぜひ利用しましょう!