皆さん、こんにちは。株式会社スクウェア・エニックスより発売された『FOAMSTARS』で主にギミック周りを担当させていただいたプログラマーのHKです。本記事ではUE4のプール機能、およびその拡張を簡単にご紹介させていただきます。

なお、UEではNiagaraが主流だと思われますが『FOAMSTARS』ではCascadeを使用しております。そのため、Cascadeについてのご紹介であることをご留意いただければと思います。
 

UE4に実装されているエフェクトプール機能の紹介

エンジンのエフェクトプールの使用方法は簡単で、エフェクトを生成するUGameplayStatics::SpawnEmitter~の引数であるPoolingMethod「AutoRelease」または「ManualRelease」に指定するだけです。

この宣言がされたエフェクトは再生が完了しても破棄が行われず、パーティクルの種類ごとのプール構造体FPSCPoolが保持します。

その後にプール使用宣言されたSpawnEmitterを実行するとプールから取り出されたエフェクトを使用することができます。ただ、全てのエフェクトがプールに保持されているとメモリを圧迫してしまいます。そこで、一定時用未使用状態のエフェクトは破棄されるようになっています。


// ------------------------------------------------------------------------------------------------------------------------------
// エフェクトの生成時にプール使用を宣言する
// ------------------------------------------------------------------------------------------------------------------------------
UGameplayStatics::SpawnEmitterAtLocation
(
    const UObject* WorldContextObject, 
    UParticleSystem* EmitterTemplate, 
    FVector SpawnLocation, 
    FRotator SpawnRotation,
    // プール使用宣言をしている場合は無視されます
     bool bAutoDestroy, 
     // プール機能を使用する場合、「EPSCPoolMethod::AutoRelease」または「EPSCPoolMethod::ManualRelease」を指定します
     // 再生するだけなら「AutoRelease」、再生開始後にパラメータ制御を行うなら「ManualRelease」と用途によって使い分けます
     EPSCPoolMethod PoolingMethod,
     bool bAutoActivateSystem
)

// ------------------------------------------------------------------------------------------------------------------------------
// エフェクトの再生終了後に、プールへと保持される
// ------------------------------------------------------------------------------------------------------------------------------
UParticleSystemComponent::Complete()
{
    ・・・
    // AutoReleaseの場合は、完了後に自動的にプールへと保持されます
    if (PoolingMethod == EPSCPoolMethod::AutoRelease)
	{
		World->GetPSCPool().ReclaimWorldParticleSystem(this);
	}
    // ManualReleaseの場合、ReleaseToPool()を実行した後にプールへと保持されます
	else if (PoolingMethod == EPSCPoolMethod::ManualRelease_OnComplete)
	{
		PoolingMethod = EPSCPoolMethod::ManualRelease;
		World->GetPSCPool().ReclaimWorldParticleSystem(this);
	}
    ・・・
}

FWorldPSCPool::ReclaimWorldParticleSystem(UParticleSystemComponent* PSC)
{
    ・・・
    // プールはパーティクルシステムをキーとしたTMapで保持されています
    FPSCPool* PSCPool = WorldParticleSystemPools.Find(PSC->Template);	
	PSCPool->Reclaim(PSC, CurrentTime);
    ・・・
}

FPSCPool::Reclaim(UParticleSystemComponent* PSC, const float CurrentTimeSeconds)
{
    ・・・
    // 上限数未満の場合はプールで保持し、 既にプール上限数以上であれば、破棄されます
    if (GbEnableParticleSystemPooling != 0 && FreeElements.Num() < (int32)PSC->Template->MaxPoolSize)
    {
        PSC->PoolingMethod = EPSCPoolMethod::FreeInPool;
		FreeElements.Push(FPSCPoolElem(PSC, CurrentTimeSeconds));
    }
    else
    {
        PSC->PoolingMethod = EPSCPoolMethod::None;
		PSC->DestroyComponent();
    }
    ・・・
}

// ------------------------------------------------------------------------------------------------------------------------------
// // エフェクトの未使用時間が一定時間以上経っていれば、破棄します
// ------------------------------------------------------------------------------------------------------------------------------
FPSCPool::KillUnusedComponents(float KillTime, UParticleSystem* Template)
{
    ・・・
    int32 i = 0;
	while (i < FreeElements.Num())
	{
		// エフェクトの使用終了時刻から、GParticleSystemPoolKillUnusedTime(デフォルトは180秒)時間経っていたら削除
		if (FreeElements[i].LastUsedTime < KillTime)
		{
			UParticleSystemComponent* PSC = FreeElements[i].PSC;
			if (PSC)
			{
				PSC->PoolingMethod = EPSCPoolMethod::None;
				PSC->DestroyComponent();
			}

			FreeElements.RemoveAtSwap(i, 1, false);
		}
	}
    ・・・
}

以上、簡単ではございますがエンジンのエフェクトプールの機能紹介でした。しかし、これらの機能では足りない部分がありましたのでいくつか拡張しております。本記事ではプール上限数に関わる拡張を2つ紹介させていただきます。

プール上限時の生成制限

1つ目は、再生中のエフェクト+未使用エフェクトがプール上限数以下になるような生成制限の拡張です。デフォルトの挙動ではプール上限数を超えても生成が可能となっており、未使用エフェクトがプール上限数まで保持された状態でエフェクトが終了すると破棄されるようになっています。

こちらを制限するために、プールからエフェクトを取り出す「FPSCPool::Acquire」で未使用エフェクトが無い場合の新規生成を使用中エフェクトがプール上限数以下のときのみ実行するように変更し、「FWorldPSCPool::CreateWorldParticleSystem」のプールから取り出せなかったときの新規生成を行わないように変更しています。

プール上限の設定

2つ目は、プール上限数を変更するためのエディタ拡張です。プール上限数は「UFXSystemAsset::MaxPoolSize」としてアセットごとに用意されております。しかし、数百もあるエフェクトのアセットを1つずつ設定するのはコストが掛かる上にミスが出やすい状態です。

そのため、上限数を変更するためのエディタ拡張を行いました。まずはMaxPoolSizeをエディタから直接変更できないようにし、代わりに「FName ParticleType」を追加し外部公開しています。次にDTでParticleTypeごとのプール上限数を設定し、エフェクトの初回生成時にプール上限数を渡しています。

この変更でDTの値を変えれば同じParticleTypeのエフェクトの上限を一斉に変更することが可能としています。また、「IAssetRegistry」でアセットを取得してParticleTypeを一斉変更できるエディタ拡張も行うことでParticleTypeの変更にもコストが掛からないようにいたしました。


// --------------------------------------------------
// パーティクルの上限数の設定方法を変更
// --------------------------------------------------
UCLASS(Abstract, MinimalAPI, BlueprintType)
class UFXSystemAsset : public UObject
{
	GENERATED_UCLASS_BODY()
public:

    // エディタからの変更を禁止
	// UPROPERTY(EditAnywhere, Category = Performance)
	uint32 MaxPoolSize;

    // その代わりにParticleTypeを用意
    // DTでParticleTypeごとに設定したプール上限数を反映するように変更
	UPROPERTY(EditAnywhere, Category = Performance)
	FName ParticleType = TEXT("Common");
    ...
};

// --------------------------------------------------
// アセットの探索
// --------------------------------------------------
{
	// 検索されたアセットの配列
	TArray AssetDatas;

	// モジュールを名前で検索して取得する
	FAssetRegistryModule& AssetRegistryModule = FModuleManager::LoadModuleChecked(FName("AssetRegistry"));
    IAssetRegistry& AssetRegistry = AssetRegistryModule.Get();

	// 検索フィルターの作成
	FARFilter Filter;
	// 全ファルダを検索するパス
	FName FilePath = TEXT("検索したいパス");
	Filter.PackagePaths.Add(FilePath);

	// 再帰的にパスを検索
	Filter.bRecursivePaths = true;
	// 検索対象のクラス
	Filter.ClassNames.Add(UParticleSystem::StaticClass()->GetFName());

	// アセットの取得
	AssetRegistry.GetAssets(Filter,AssetDatas);
}

// --------------------------------------------------
// 取得したアセットのParticleTypeを一括変更
// --------------------------------------------------
{
	// 新しいParticleTypeを取得
	FString NewType;
	TArray   PackagesToSave;
	for(int32 AssetIdx = 0; AssetIdx < AssetDatas.Num(); ++AssetIdx)
	{
		// アセットデータからパーティクルを取得
		UParticleSystem* Particle = Cast(AssetDatas[AssetIdx].GetAsset());
		if(Particle == nullptr)
		{
			continue;
		}

		// 同じタイプであれば処理しない
		if(Particle->GetFName() != *NewType)
		{
			continue;
		}

		// 指定されたParticleTypeに変更
		Particle->ParticleType = *NewType;
		Particle->MarkPackageDirty();
		
		// セーブリストに追加
		PackagesToSave.Add(AssetDatas[AssetIdx].GetPackage());
	}

	// 保存
	if(PackagesToSave.Num() > 0)
	{
		FEditorFileUtils::PromptForCheckoutAndSave(PackagesToSave,true,true);
	}
}

UEでは、エフェクトのプール機能を簡単に使用することができます。しかし、プロジェクトによっては足りない部分も出てくるかと思います。そのような際に、本記事が少しでも役立つようであれば幸いです。

著者紹介 HK
2022年にトイロジックに新卒入社。『FOAMSTARS』ではギミックを担当の後、HUD周りを担当。最近は、Slaythespireのボードゲームを楽しみにしている。


トイロジックでは現在、一緒に働くプログラマーを募集しています。

不明点などもお気軽にお問い合わせくださいフルリモート採用も行っております、ご応募お待ちしております!
 
© SQUARE ENIX