13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

Epic Games Japan #3Advent Calendar 2019

Day 19

[UE4] AnimNotifiy の Play Particle Effect の負荷をUE4標準のObject Pool機能で削減しよう!

Last updated at Posted at 2019-12-18

はじめに

PlayParticleEffect_01.jpg
image.png
UE4のアニメーション通知(AnimNotify)機能の一つであるPlay Particle Effect(UAnimNotify_PlayParticleEffect)を使って、キャラクタの動きに合わせてエフェクトを再生するようにすることは一般的かと思います。例えば走っているキャラの足が地面に着いた時に砂煙を出したり、攻撃モーション中に火花や軌跡を出したり…
https://docs.unrealengine.com/ja/Engine/Animation/Sequences/Notifies/index.html#playparticleeffect

sample.gif
しかし、大量のキャラクタが動き回るコンテンツの場合、大量のエフェクトが同時に生成・再生されることによる処理負荷が問題になることが多いです…

そこで、本記事では 以前に紹介した Spawn Emitter~ノードの Pooling Method を使って負荷を改善する方法について説明します。
Spawn Emitter~ノードの Pooling Method って何?

Pooling Method有効版のPlay Particle EffectをBlueprintで作ろう!

株式会社ヒストリア様の技術ブログ [UE4] 独自のAnimation Notifyの実装方法 で解説されている通り、Blueprintを使って独自のAnimNotify/AnimNotifyStateを作ることができます。この機能を使って、Pooling Methodを有効にしたUAnimNotify_PlayParticleEffectを作れば今回の目標は達成できます!これだけ聞くと難しく感じるかもしれませんが、UAnimNotify_PlayParticleEffectは内部でSpawnEmitter~処理を呼び出しているだけなので簡単です!

AnimNotify_PlayPArticleEffect.cpp
UParticleSystemComponent* UAnimNotify_PlayParticleEffect::SpawnParticleSystem(class USkeletalMeshComponent* MeshComp, class UAnimSequenceBase* Animation)
{
	UParticleSystemComponent* ReturnComp = nullptr;

	if (PSTemplate)
	{
		if (PSTemplate->IsLooping())
		{
			UE_LOG(LogParticles, Warning, TEXT("Particle Notify: Anim '%s' tried to spawn infinitely looping particle system '%s'. Spawning suppressed."), *GetNameSafe(Animation), *GetNameSafe(PSTemplate));
			return ReturnComp;
		}

		if (Attached)
		{
			ReturnComp = UGameplayStatics::SpawnEmitterAttached(PSTemplate, MeshComp, SocketName, LocationOffset, RotationOffset, Scale);
		}
		else
		{
			const FTransform MeshTransform = MeshComp->GetSocketTransform(SocketName);
			FTransform SpawnTransform;
			SpawnTransform.SetLocation(MeshTransform.TransformPosition(LocationOffset));
			SpawnTransform.SetRotation(MeshTransform.GetRotation() * RotationOffsetQuat);
			SpawnTransform.SetScale3D(Scale);
			ReturnComp = UGameplayStatics::SpawnEmitterAtLocation(MeshComp->GetWorld(), PSTemplate, SpawnTransform);
		}
	}
	else
	{
		UE_LOG(LogParticles, Warning, TEXT("Particle Notify: Particle system is null for particle notify '%s' in anim: '%s'"), *GetNotifyName(), *GetPathNameSafe(Animation));
	}

	return ReturnComp;
}

ということで、参考記事で解説されているとおりに 「Notifyクラスを継承したBPを作成」して、「Received_Notify」に上記のコードと同じ実装をBPで書いていきます。

ゴリゴリ…カリカリ…ペタペタ…ポチポチ…

…できました!
image.png
image.png
Before
image.png
After
image.png

超厳密な比較というわけではありませんが、Spawn Emitter~ノードの Pooling Method って何?で説明したとおり、Pooling Methodを活用することで一部処理を省略できたことで負荷が改善しています。今回のプロファイル環境はそこそこ良いPC + シンプルなエフェクト ということで 12 μs という僅かな改善でしたが環境・条件によっては無視できないほどの改善になるケースもあります(例:低スペックなモバイル端末上で動かす、数十体以上のキャラが同時に数個のエフェクトを頻繁に出すコンテンツ)。

プロファイリングした時にUAnimNotify_PlayParticleEffectの負荷が目立つ際は是非Pooling Methodを活用する方法への切り替えを検討してみてください!

おまけ:今回説明した自作AnimNotifyの課題「Socket Nameの予測が出てこない」について

default.gif
UAnimNotify_PlayParticleEffectのプロパティであるSocket Name、文字を入力すると予測を出してくれます。便利です。
test.gif
一方、今回自作したAnimNotifyは…出してくれません!「アイエエエエ!同じプロパティ名・同じName型なのにナンデ!?」と思ったので調べてみました。

AnimNotifyDetails.cpp
bool FAnimNotifyDetails::CustomizeProperty(IDetailCategoryBuilder& CategoryBuilder, UObject* Notify, TSharedPtr<IPropertyHandle> Property)
{
	if(Notify && Notify->GetClass() && Property->IsValidHandle())
	{
		FString ClassName = Notify->GetClass()->GetName();
		FString PropertyName = Property->GetProperty()->GetName();
		bool bIsBoneName = Property->GetBoolMetaData(TEXT("AnimNotifyBoneName"));

		if(ClassName.Find(TEXT("AnimNotify_PlayParticleEffect")) != INDEX_NONE && PropertyName == TEXT("SocketName"))
		{
			AddBoneNameProperty(CategoryBuilder, Notify, Property);
			return true;
		}
		else if(ClassName.Find(TEXT("AnimNotifyState_TimedParticleEffect")) != INDEX_NONE && PropertyName == TEXT("SocketName"))
		{
			AddBoneNameProperty(CategoryBuilder, Notify, Property);
			return true;
		}
		else if(ClassName.Find(TEXT("AnimNotify_PlaySound")) != INDEX_NONE && PropertyName == TEXT("AttachName"))
		{
			AddBoneNameProperty(CategoryBuilder, Notify, Property);
			return true;
		}
		else if (ClassName.Find(TEXT("AnimNotifyState_Trail")) != INDEX_NONE)
		{
			if(PropertyName == TEXT("FirstSocketName") || PropertyName == TEXT("SecondSocketName"))
			{
				AddBoneNameProperty(CategoryBuilder, Notify, Property);
				return true;
			}
			else if(PropertyName == TEXT("WidthScaleCurve"))
			{
				AddCurveNameProperty(CategoryBuilder, Notify, Property);
				return true;
			}
		}
		else if (bIsBoneName)
		{
			AddBoneNameProperty(CategoryBuilder, Notify, Property);
			return true;
		}
	}
	return false;
}

特定のクラスが特定の名前のプロパティを持つ、またはUPROPERTYのmetaでAnimNotifyBoneNameが有効になっている場合はボーン・ソケット名の予測が出るようになるという仕様でした。そのため、上記のコードにないクラスで予測を出すようにする場合は以下のようにしてAnimNotifyBoneNameを有効にする必要があります。

UPROPERTY(EditAnywhere,  meta = (AnimNotifyBoneName = "true"))

BPの場合は…も、申し訳ありません…できないはずです…。
ということで、Pooling Method有効版のUAnimNotify_PlayParticleEffectを本気で作る場合はUAnimNotify_PlayParticleEffectを改造、または継承したクラスにてPooling MethodをAnimNotifyの詳細パネルから設定できる形にするのが良いかと思います。

おしまい

13
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
13
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?