LoginSignup
17
8

More than 3 years have passed since last update.

[UE4] Spawn Emitter~ノードの Pooling Method って何? -エフェクト用Object Pool機能について-

Last updated at Posted at 2019-12-18

はじめに

image.png
UE4.20でエフェクト(Cascade)を生成するSpawn Emitter~ノードにPooling Methodオプションが追加されました。これは一度生成したパーティクルをPoolに蓄積し、再度必要になった際はそれをPoolから取り出して使い回すことで処理負荷を改善するための機能です。

Pooling Method:None(デフォルト)
image.png
Pooling Method: Auto Release or Manual Release
image.png
実際にプロファイリングしてみると…確かに処理が減って負荷が改善されていますが「思っていたよりは呼ばれる処理が減ってないなぁ…」と感じました。また、Pooling Methodの実装に関する解説資料がない(申し訳ありません!)ので今回調べてみました。あ、少し小難しい話になるので結論を先に書いておきます。

  • Pooling Methodを使うことでエフェクトの生成処理の一部を省略し負荷を削減できます(負荷が0になる訳ではない)
  • 単発のエフェクトをただ再生するだけならAuto Release、再生中にパラメータを変える場合はManual Release設定に
  • Manual Release設定の場合は再生終了後にRelease To Poolノードを呼ぶ必要がある
  • 保持しているエフェクトは定期的に破棄されるため、負荷の調整時には十分に注意。また、常駐用途で使うことはできません
  • UE4.24時点ではNiagaraは未対応

以降は内部実装を紹介しつつ解説

Pooling Methodについて

SpawnEmitter~をそのまま使う場合、Particle System Component(PSC)の生成・初期化処理などのコストが毎回発生してしまいます。この問題を解決するため、まず生成後にエフェクトの再生が完了したPSCは破棄されずに WorldPSCPool というUE4.20で追加されたObject Pool機能に保持されます。

ParticleComponent.cpp
void UParticleSystemComponent::Complete()
{
...
    if (PoolingMethod == EPSCPoolMethod::AutoRelease)
    {
        World->GetPSCPool().ReclaimWorldParticleSystem(this);
    }
    else if (PoolingMethod == EPSCPoolMethod::ManualRelease_OnComplete)
    {
        PoolingMethod = EPSCPoolMethod::ManualRelease;
        World->GetPSCPool().ReclaimWorldParticleSystem(this);
    }
    else if (bAutoDestroy)
    {
        DestroyComponent();
    }
    else if (bAutoManageAttachment)
    {
        CancelAutoAttachment(/*bDetachFromParent=*/ true);
    }
...
}

そして、次にSpawnEmiter~が呼ばれた際は、その WorldPSCPool が保持しているPSCを再利用します。
SpawnEmitter~ (GamaplayStatics.cpp)UGameplayStatics::InternalSpawnEmitterAtLocationCreateParticleSystem (GamaplayStatics.cpp)

GamaplayStatics.cpp
UParticleSystemComponent* CreateParticleSystem(UParticleSystem* EmitterTemplate, UWorld* World, AActor* Actor, bool bAutoDestroy, EPSCPoolMethod PoolingMethod)
{
    //Defaulting to creating systems from a pool. Can be disabled via fx.ParticleSystemPool.Enable 0
    UParticleSystemComponent* PSC;

    if (PoolingMethod != EPSCPoolMethod::None)
    {
        //If system is set to auto destroy the we should be safe to automatically allocate from a the world pool.
        PSC = World->GetPSCPool().CreateWorldParticleSystem(EmitterTemplate, World, PoolingMethod);
    }
    else
    {
        PSC = NewObject<UParticleSystemComponent>((Actor ? Actor : (UObject*)World));
        PSC->bAutoDestroy = bAutoDestroy;
        PSC->bAllowAnyoneToDestroyMe = true;
        PSC->SecondsBeforeInactive = 0.0f;
        PSC->bAutoActivate = false;
        PSC->SetTemplate(EmitterTemplate);
        PSC->bOverrideLODMethod = false;
    }

    return PSC;
}

このようにすることでPSCの生成・初期化コストを排除・削減しています。実際に先程のプロファイリング結果を見比べてみると、NewObject<UParticleSystemComponent>(World)の負荷であるConstructObjectがなくなっていたり、初期化処理UParticleSystemComponent::InitParticles()の一部が省略されています。

Auto ReleaseとManual Releaseの違いについて


image.png
Auto Releaseの場合、エフェクトの再生が終了すると自動的に WorldPSCPool に保持します。
Manual Releaseの場合、エフェクトの再生が終了しても WorldPSCPool に保持されないため、UParticleSystemComponent::ReleaseToPool() を明示的に呼んで WorldPSCPool に保持する必要があります。もしエフェクトの再生途中でReleaseToPoolを使った場合は強制的に再生を終了します。ちなみに、Manual Release設定以外の場合はRelease to Poolを使っても何も処理が走りません(Warningが出ます)。

大雑把な使い分けとしては、まず単発のエフェクトを再生するだけならAuto Releaseで問題ないかと思います。そして、再生中のPSCに対して何らかのパラメータ制御を行う場合はManual Releaseにしておいた方が安全です。何故ならWorldPSCPoolは保持しているPSCに対して様々なパラメータ制御や削除処理を行うため、WorldPSCPoolに管理を任せているPSCに対して行ったパラメータ制御がうまく行かないケースが出てくるからです。

軽くまとめるとこんな感じ

  • 単純に再生するだけならAuto ReleaseでOK!
  • Spawn Emitter の Return Valueピンから取得したPSCをBP側で保持・変更を行う場合はManual Releaseに!
  • Manual Release設定の場合、不要になったらReleaseToPoolノードを使って WorldPSCPool に送ること!

Pooling Methodを使用する上での注意点

Spawn Emitterで指定している Emitter Template が異なる場合は再利用されません

image.png
言い換えると、 WorldPSCPool で幾つかのPSCを保持している状態であっても、これまで再生したものとは異なるエフェクトを再生する場合は新規でPSCを作成することになります。何故なら、WorldPSCPool は Emitter Template(UParticleSystem)毎にPSCを保持する領域(FPSCPool)を用意しているためです。

WorldPSCPool.cpp
UParticleSystemComponent* FWorldPSCPool::CreateWorldParticleSystem(UParticleSystem* Template, UWorld* World, EPSCPoolMethod PoolingMethod)
{
...
    UParticleSystemComponent* PSC = nullptr;
    if (GbEnableParticleSystemPooling != 0)
    {
        if (Template->CanBePooled())
        {
            FPSCPool& PSCPool = WorldParticleSystemPools.FindOrAdd(Template);
            PSC = PSCPool.Acquire(World, Template, PoolingMethod);
        }
    }
...
}

もし異なるEmitter Templateでも再利用できる仕様にした場合、Emitter Templateの初期化処理が毎回走ってしまいます。そうなるとWorldPSCPoolの本来の目的である負荷削減の効果が低くなるのでこのような仕様になっています。

ということなので、WorldPSCPool で保持していないエフェクトを新たに再生する際は負荷に気をつけましょう!また、事前にこっそり再生しておいてWorldPSCPoolに貯めておくという戦略もありだと思います。

無限ループするエフェクトの場合は、再生終了処理を呼ばないとWorldPSCPoolに保持されません

image.png
当然といえば当然なのですが…使い終わったPSCを保持して使い回す仕組みなので、永遠に再生し続けるエフェクトの場合は明示的に再生を止めないとWorldPSCPoolに送られません。そのため、不要になったタイミングでSet ActiveノードやDeativateノードを呼び出すようにしましょう。なお、上述の通りManual Release設定の場合はRelease to Poolを呼ぶことで再生終了とWorldPSCPoolでの保持をリクエストできます。

Pooling Methodを使う場合、Spawn Emitter~のAuto Destroyは無視されます

image.png
Auto Destroyにチェックを入れた場合、エフェクトの再生が終了すると自動的に破棄するようになります。しかし、Pooling Methodを使う場合は破棄せずに WorldPSCPool に保持することになるため、Pooling MethodがNone以外の場合は強制的にAuto Destroyは無効になります。

WorldPSCPool.cpp
UParticleSystemComponent* FPSCPool::Acquire(UWorld* World, UParticleSystem* Template, EPSCPoolMethod PoolingMethod)
{
...
    if (FreeElements.Num())
    {
...
    }
    else
    {
        //None in the pool so create a new one.
        RetElem.PSC = NewObject<UParticleSystemComponent>(World);
        RetElem.PSC->bAutoDestroy = false;//<<< We don't auto destroy. We'll just periodically clear up the pool.
        RetElem.PSC->SecondsBeforeInactive = 0.0f;
        RetElem.PSC->bAutoActivate = false;
        RetElem.PSC->SetTemplate(Template);
        RetElem.PSC->bOverrideLODMethod = false;
        RetElem.PSC->bAllowRecycling = true;
    }
...
}

WorldPSCPool は「保持しているけど使われていないPSC」を定期的に破棄します

使用頻度が少ないものを保持し続けることはメモリ消費などの観点からなるべく避けたい所です。そのため、WorldPSCPool は一定時間使われていないPSCを自動的に破棄するようにしています。そして、自動破棄を制御しているパラメータは FX.ParticleSystemPool.KillUnusedTimeFX.ParticleSystemPool.CleanTimeの2つです。

WorldPSCPool.cpp
static float GParticleSystemPoolKillUnusedTime = 180.0f;
static FAutoConsoleVariableRef ParticleSystemPoolKillUnusedTime(
    TEXT("FX.ParticleSystemPool.KillUnusedTime"),
    GParticleSystemPoolKillUnusedTime,
    TEXT("How long a pooled particle component needs to be unused for before it is destroyed.")
);

static float GParticleSystemPoolingCleanTime = 30.0f;
static FAutoConsoleVariableRef ParticleSystemPoolingCleanTime(
    TEXT("FX.ParticleSystemPool.CleanTime"),
    GParticleSystemPoolingCleanTime,
    TEXT("How often should the pool be cleaned (in seconds).")
);

WorldPSCPoolが使われていないPSCを保持し続ける時間をFX.ParticleSystemPool.KillUnusedTimeで制御します。デフォルト値は180なので、3分以上再利用されていないPSCを自動破棄します。

FX.ParticleSystemPool.CleanTimeは自動破棄が必要な否かのチェック処理の実行間隔を制御するためのものです。WorldPSCPool はPSCがPoolに追加されたタイミング( = Auto Release設定のPSCの再生が終了した or Manual Release設定のPSCに対してRelease To Poolが呼ばれた) に保持している各PSCに対して自動破棄をするかしないかのチェック処理を行います。そして、複数のPSCがほぼ同時に追加された場合にそのチェック処理が複数回走るのは勿体ないので、最後にチェック処理を行ったのが FX.ParticleSystemPool.CleanTime より前だったらチェック処理を行うという仕様になっています。

このような仕様になっているため、使用頻度の低いエフェクトをPooling Methodを有効にした状態で再生する際は注意しましょう。Poolされているから負荷削減される!と思っていても、実は自動破棄されて WorldPSCPool の恩恵を受けれていない可能性があります。

レベルロード時にWorldPSCPoolに保持されているPSCは全て破棄されます

レベルロード時に呼ばれるUWorld::CleanupWorldにて保持している全てのPSCに対して破棄処理FWorldPSCPool::Cleanup()が呼ばれます。そのため、Pooling Methodを使ってエフェクトを常駐という使い方をすることはできません。

さいごに

色々注意点について書きましたが、設定を切り替えるだけでエフェクトのObject Pool処理を実現できる非常に便利な機能です。特に使用頻度の高いエフェクト(キャラクタが走る際の砂煙、攻撃ヒット時の火花などなど)で適応するとそこそこの負荷を削減できるかと思います。是非お試しください!

おまけ:Max Pool Size

image.png
アセット毎にWorldPSCPoolで保持する数の上限を設定することができます。

おまけ:fx.DumpPSCPoolInfo

Cmd: fx.DumpPSCPoolInfo
LogParticles: ***************************************
LogParticles: *Particle System Pool Info - Total Mem = 0.41MB*
LogParticles: ***************************************
LogParticles: Free: 3 (19218B)    |    Used(Auto - Manual): 50 - 0 (412802B)  |    MaxUsed: 53    |    System: ParticleSystem /Game/MobileStarterContent/Particles/P_Explosion.P_Explosion
LogParticles: ***************************************

現在のWorldPSCPoolが保持しているエフェクトの情報を出力できます

おまけ:Niagaraは?

UE4.24だと…

image.png
UE4.24時点ではPooling Methodを指定できるようになっていますが…

NiagaraFunctionLibrary.cpp
UNiagaraComponent* CreateNiagaraSystem(UNiagaraSystem* SystemTemplate, UWorld* World, AActor* Actor, bool bAutoDestroy, EPSCPoolMethod PoolingMethod)
{
    // todo : implement pooling method.

    UNiagaraComponent* NiagaraComponent = NewObject<UNiagaraComponent>((Actor ? Actor : (UObject*)World));
    NiagaraComponent->SetAutoDestroy(bAutoDestroy);
    NiagaraComponent->bAllowAnyoneToDestroyMe = true;
    NiagaraComponent->SetAsset(SystemTemplate);
    return NiagaraComponent;
}

// todo : implement pooling method!

UE4.25だと…

NiagaraFunctionLibrary.cpp
UNiagaraComponent* CreateNiagaraSystem(UNiagaraSystem* SystemTemplate, UWorld* World, AActor* Actor, bool bAutoDestroy, ENCPoolMethod PoolingMethod)
{
    UNiagaraComponent* NiagaraComponent = nullptr;

    if (FApp::CanEverRender() && World && !World->IsNetMode(NM_DedicatedServer))
    {
        if (PoolingMethod == ENCPoolMethod::None)
        {
            NiagaraComponent = NewObject<UNiagaraComponent>((Actor ? Actor : (UObject*)World));
            NiagaraComponent->SetAsset(SystemTemplate);
            NiagaraComponent->bAutoActivate = false;
            NiagaraComponent->SetAutoDestroy(bAutoDestroy);
            NiagaraComponent->bAllowAnyoneToDestroyMe = true;
        }
        else
        {
            UNiagaraComponentPool* ComponentPool = FNiagaraWorldManager::Get(World)->GetComponentPool();
            NiagaraComponent = ComponentPool->CreateWorldParticleSystem(SystemTemplate, World, PoolingMethod);
        }
    }
    return NiagaraComponent;
}

実装キタ!

おしまい

17
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
17
8