关于粒子特效的一些基础知识,可以参考 【UE4】特效之 Particle System 详解(一)—— 综述
举个粒子解释一下池子:
比如你是弓箭手,你会射箭,你会从地上(内存)捡树枝打造弓箭(NewObject)。
以上是个人理解,有问题可以讨论~
值得注意的是:
ParticleSystem(俗称粒子特效)的释放最终调用的都是 CreateParticleSystem
函数,如下所示:
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 = nullptr;
if (FApp::CanEverRender() && World && !World->IsNetMode(NM_DedicatedServer))
{
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->xxx = xx 等一些初始化操作 blablabla...
}
}
return PSC;
}
核心逻辑就是,如果 PoolMethod 不是 None
,则从池子中取;如果是 None
,则会 NewObject
,即每次释放一个特效,都会创建新的。
NiagaraSystem(俗称奶瓜特效),也是一样,接口是 CreateNiagaraSystem
,也是每次 NewObject。
Note: 值得注意的是,两种特效都进行了
World && !World->IsNetMode(NM_DedicatedServer)
的判断,即特效在服务器上是都不会创建的,所以不需要关心服务器上的特效会NewObject
,但是如果是 Actor 上挂载的
ParticleSystemComponent(不管是从代码,还是从蓝图资源),是会在服务器创建 Component 的,这点需要注意。
由于 NewObject 会进行一系列操作,所以对 CPU(GameThread)肯定是有消耗的(虽然一个可能不多,但是架不住数量多啊),所以如果能够利用池子进行缓存,每次不新创建,而是从池子里取,则能对 CPU 性能有所帮助(利用 空间换时间)。
由于 ParticleSystem 和 NiagaraSystem 是两种完全不同的特效,所以在对这两种特效的支持(释放,池化等)都是两套完全独立的代码,但是逻辑大体相似。
ParticleSystem 的池子叫 FWorldPSCPool
,NiagaraSystem 的池子叫 UNiagaraComponentPool
,下边会详细总结。
这里只记录 ParticleSystem 的特效池使用方法。首先需要知道的是,每一个特效资源,在代码中就是一个 UParticleSystem*
,每一个实际出现效果的(不管是火焰,爆炸,闪光,烟雾,粒子等等),都是用 UParticleSystemComponent
来实现的,即 UParticleSystem 是 数据,UParticleSystemComponent 是 实体 的感觉。
比如一刀看下去,三个人身上冒血,那么是三个 UParticleSystemComponent 同时在播放,但是使用的是同一个 UParticleSystem 作为 数据源,理解了这一点,下边就都好说了。
特效池的目的就是同一个数据源,创建出的多个实体进行缓存,从而达到虽然一共播放了 1000 次冒血特效,但是只 New 了 3 个 Object 的效果。
详见 Engine\Source\Runtime\Engine\Classes\Particles\WorldPSCPool.h
。
引擎一共 提供了三种池化操作(其实 EPSCPoolMethod
枚举类型由五个值,但是只关注前三个即可):
None
AutoRelease
ManualRelease
AutoDestroy
选项失效),适用于需要自己控制时长的 "永久" 特效(因为这种特效必须手动回收,否则会内存泄漏,所以肯定也不能是默认值)。 综上,引擎默认是不把特效放入池子的,但是如果想用,只需要把 SpawnEmitter 的接口的默认参数改为需要的即可(建议是一次性效果,如一次爆炸特效,用 AutoRelease
;持续性类似 Buff 的效果,如身上的灼烧火焰效果,用 ManualRelease
,并在火焰时间到,灼烧效果消失的时候,手动 ReleaseToPool
)。
特效的池子很简单,每一个粒子特效(UParticleSystem*
,即特效资源),对应一个数组( TArray
),这个结构也很简单,如下:
USTRUCT()
struct FPSCPoolElem
{
GENERATED_BODY()
UPROPERTY(transient)
UParticleSystemComponent* PSC;
float LastUsedTime;
// 还有两个构造函数
};
USTRUCT()
struct ENGINE_API FWorldPSCPool
{
GENERATED_BODY()
private:
UPROPERTY()
TMap<UParticleSystem*, FPSCPool> WorldParticleSystemPools;
float LastParticleSytemPoolCleanTime;
/** Cached world time last tick just to avoid us needing the world when reclaiming systems. */
float CachedWorldTime;
public:
FWorldPSCPool();
~FWorldPSCPool();
void Cleanup();
UParticleSystemComponent* CreateWorldParticleSystem(UParticleSystem* Template, UWorld* World, EPSCPoolMethod PoolingMethod);
/** Called when an in-use particle component is finished and wishes to be returned to the pool. */
void ReclaimWorldParticleSystem(UParticleSystemComponent* PSC);
/** Call if you want to halt & reclaim all active particle systems and return them to their respective pools. */
void ReclaimActiveParticleSystems();
/** Dumps the current state of the pool to the log. */
void Dump();
};
UE 默认是提供了粒子特效的池子的,叫做 FWorldPSCPool(Niagara 的池子叫 UNiagaraComponentPool),但是如果使用的是 UGameplayStatics 里的释放粒子特效的接口(不管是 SpawnEmitterAtLocation
还是 SpawnEmitterAttached
),都是默认不把特效放入池子的(即 EPSCPoolMethod::None
,原因可能就是在于,引擎不知道应该给什么样的默认逻辑)。
FWorldPSCPool 的生命周期可以认为由 World 管理,在 World 中有一个变量:
UPROPERTY()
FWorldPSCPool PSCPool;
在 World 的 UWorld::CleanupWorldInternal
会调用 PSCPool.Cleanup()
清理特效池。
在 World 析构时会调用 FWorldPSCPool
的析构,会执行 Cleanup()
。
在 CreateParticleSystem
的时候会调用 World->GetPSCPool().CreateWorldParticleSystem
。
FWorldPSCPool 里最重要的就是 TMap
,这个就是存储着所有释放过的特效数据源(UParticleSystem*),与对应的创建出来的实体(UParticleSystemComponent*)的数组的 Map。
至于为什么是数组,就是因为很可能有同时播放很多特效的需求,每一个都是一个单独的 Component,如前边说到的一刀三个冒血。
FWorldPSCPool::CreateWorldParticleSystem
中会从 WorldParticleSystemPools 中以特效为 Key 找出这个特效的小池子,并从中找出可用实体。
FPSCPool& PSCPool = WorldParticleSystemPools.FindOrAdd(Template);
PSC = PSCPool.Acquire(World, Template, PoolingMethod);
USTRUCT()
struct FPSCPool
{
GENERATED_BODY()
//Collection of all currently allocated, free items ready to be grabbed for use.
//TODO: Change this to a FIFO queue to get better usage. May need to make this whole class behave similar to TCircularQueue.
UPROPERTY(transient)
TArray<FPSCPoolElem> FreeElements;
//Array of currently in flight components that will auto release.
UPROPERTY(transient)
TArray<UParticleSystemComponent*> InUseComponents_Auto;
//Array of currently in flight components that need manual release.
UPROPERTY(transient)
TArray<UParticleSystemComponent*> InUseComponents_Manual;
/** Keeping track of max in flight systems to help inform any future pre-population we do. */
int32 MaxUsed;
public:
FPSCPool();
void Cleanup();
/** Gets a PSC from the pool ready for use. */
UParticleSystemComponent* Acquire(UWorld* World, UParticleSystem* Template, EPSCPoolMethod PoolingMethod);
/** Returns a PSC to the pool. */
void Reclaim(UParticleSystemComponent* PSC, const float CurrentTimeSeconds);
/** Kills any components that have not been used since the passed KillTime. */
void KillUnusedComponents(float KillTime, UParticleSystem* Template);
int32 NumComponents() { return FreeElements.Num(); }
};
关键成员变量、函数:
FreeElements
- 就是存储着这个特效可用的实体 Component,正在使用的不在这里,还没有被回收到池子里。InUseComponents_Auto
、InUseComponents_Manual
- 可以不管,可以认为是用来 Debug 的(ENABLE_PSC_POOL_DEBUGGING
)MaxUsed
- 最多用了多少个Acquire()
- 用于从自己的数组中取出可用 Component 的方法Reclaim()
- 放回池子的方法 TArray
数组里边存储的即这个数据源创建出来的每一个实体。
其中数组的大小是有限制的,就是特效资源上配置的 MaxPoolSize
(代码详见 FPSCPool::Reclaim
,如果 FreeElements.Num() < (int32)PSC->Template->MaxPoolSize
,则不会回收这个 Component,而是直接 DestroyComponent)。
USTRUCT()
struct FPSCPoolElem
{
GENERATED_BODY()
UPROPERTY(transient)
UParticleSystemComponent* PSC;
float LastUsedTime;
// 两个构造函数
};
FPSCPoolElem 里边就只有实体(PSC)和这个实体上一次使用的时间(LastUsedTime),用于超时剔除等(详见 FPSCPool::KillUnusedComponents
)。
放回池子的流程稍微麻烦些,因为还有定时清理功能。
void FWorldPSCPool::ReclaimWorldParticleSystem(UParticleSystemComponent* PSC)
{
// Check blablabla
if (GbEnableParticleSystemPooling)
{
float CurrentTime = PSC->GetWorld()->GetTimeSeconds();
//Periodically clear up the pools.
if (CurrentTime - LastParticleSytemPoolCleanTime > GParticleSystemPoolingCleanTime)
{
LastParticleSytemPoolCleanTime = CurrentTime;
for (TPair<UParticleSystem*, FPSCPool>& Pair : WorldParticleSystemPools)
{
Pair.Value.KillUnusedComponents(CurrentTime - GParticleSystemPoolKillUnusedTime, PSC->Template);
}
}
// Check blablabla
PSCPool->Reclaim(PSC, CurrentTime);
}
else
{
PSC->DestroyComponent();
}
}
每次回收一个 Component 的时候,都会判断现在距上次清理的时间隔了多久,如果超过了 GParticleSystemPoolingCleanTime
(默认值是 30.f,即 30 秒),则会对 WorldParticleSystemPools
中 所有元素 进行清理(并不只清理当前这个特效的缓存),除此之外,和 2.2.1 中的流程类似:
在编辑器中的命令行窗口,输入 fx.DumpPSCPoolInfo
,可以在 Output 窗口中看到当前池子的大小,以及每个 PS 有多少个 Free 的,有多少个正在 Used 的。
上边这张图可以看到,当前池子一共占内存 0.7 MB,每一个特效资源(ParticleSystem),会输出对应的数据:
Free
- 当前池子中可用的 Component 实体Used
- 当前正在使用的 Component 实体(Auto、Manual 对应释放时设置的池化方法)MaxUsed
- 最多同一时刻一起使用的 Component 实体数量(就是 FreeElements
数组的大小)System
- 特效资源的路径吐槽一下,第一行的输出,不应该换一行吗。。。。。。看起来好难受。。
以及并不能看到减少了多少次 NewObject
FPSCPool::Acquire
中回进行 RetElem.PSC->Rename(nullptr, World, REN_ForceNoResetLoaders);
,即把这个 Component 的 OwnerPrivate
(GwtOwner())设为世界,官方的注释是:
Rename the PSC to move it into the current PersistentLevel - it may have been spawned in one level but is now needed in another level.
也就是为了防止在一个 Level 中创建了这个特效 Component,又想在另一个 Level 中使用,所以全部存在 World 上,毕竟 PSCPool
就存在 World 上。
然而因为特效的顿帧(即 Tick 的 DeltaTime 是跟 OwnerPrivate
相关的,详见 FActorComponentTickFunction::ExecuteTickHelper
),所以如果希望特效的速率和角色 A 一致,那么需要 PSC->SpawnedParticle->Rename(nullptr, Actor);
将它的 OwnerPrivate
设为角色 A。
这样会导致当角色 A Destroy 的时候,会将身上的 Component 也销毁,会触发 FPSCPool::Acquire
中的 check:
check(!RetElem.PSC->IsPendingKill());
可行的解决方案就是:
ManualRelease
方式,那么可以直接 Rename,只不过需要在 ReleaseToPool
之前在 Rename 回当前 WorldAutoRelease
方式,那就需要用别的方式修改顿帧了但凡设计回收冲利用的机制,就避免不了需要 Reset。但是
总结就是如果需要使用池子,那就不要对返回值 PSC 做任何操作了。
再吐槽一下,修改 Owner 竟然没有 SetOwner 这种函数,而是用 Rename。。感觉很奇怪,可能就是不想让你可以 SetOwner 吧
个人知乎:https://www.zhihu.com/people/gaoy-88