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

# はじめに
![PlayParticleEffect_01.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/86771de5-7722-38fb-ef4b-e583f864a2c0.jpeg)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/15601855-c7c4-ad08-d4bb-06bc046ff3a3.png)
UE4のアニメーション通知（AnimNotify）機能の一つであるPlay Particle Effect(```UAnimNotify_PlayParticleEffect```)を使って、キャラクタの動きに合わせてエフェクトを再生するようにすることは一般的かと思います。例えば走っているキャラの足が地面に着いた時に砂煙を出したり、攻撃モーション中に火花や軌跡を出したり…
https://docs.unrealengine.com/ja/Engine/Animation/Sequences/Notifies/index.html#playparticleeffect

![sample.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/c37270d3-92d8-97c2-2892-2ebf150d6072.gif)
しかし、大量のキャラクタが動き回るコンテンツの場合、大量のエフェクトが同時に生成・再生されることによる処理負荷が問題になることが多いです…

そこで、本記事では 以前に紹介した ```Spawn Emitter～```ノードの **Pooling Method** を使って負荷を改善する方法について説明します。
[Spawn Emitter～ノードの Pooling Method って何？](https://qiita.com/EGJ-Kaz_Okada/private/7672e29e0fd96bec643c)

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

```cpp: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](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/26f13029-fb33-812e-683f-2ca4e82de1b9.png)
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/afded3b4-c55d-0641-0a0f-f17c8aa2da1c.png)
Before
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/bd001e44-7072-ac90-47cd-c8e5ab471cb0.png)
After
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/62c4f940-dd8e-8eca-5adb-6abb0a92929f.png)

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

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

# おまけ：今回説明した自作AnimNotifyの課題「Socket Nameの予測が出てこない」について
![default.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/44a25c85-f70a-3523-feb8-89890d9aa0ec.gif)
```UAnimNotify_PlayParticleEffect```のプロパティである```Socket Name```、文字を入力すると予測を出してくれます。便利です。
![test.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/363889/e15ddfea-ef43-c720-265e-eefe5b79f176.gif)
一方、今回自作したAnimNotifyは…出してくれません！「アイエエエエ！同じプロパティ名・同じName型なのにナンデ！？」と思ったので調べてみました。

```cpp: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を有効にする必要があります。

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

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

おしまい