【UE4】特效之 Particle System 详解(二)—— 特效池
关于粒子特效的一些基础知识,可以参考 【UE4】特效之 Particle System 详解(一)—— 综述
一、特效池是干嘛的
举个粒子解释一下池子:
比如你是弓箭手,你会射箭,你会从地上(内存)捡树枝打造弓箭(NewObject)。
- 如果没有箭袋
那每次想射箭,都需要从地上捡树枝打造弓箭,这个过程想想就很麻烦,所以你的效率很低 - 如果背上背了箭袋
那么你可以在从地上捡树枝打造弓箭,并射出去之后,把弓箭捡回来,插到箭袋里,下次想射箭,如果箭袋里有弓箭,那直接拿出来射就行,不需要重新打造
以上是个人理解,有问题可以讨论~
值得注意的是:
- 能这样做的基础是,每次射出去的弓箭,最后都会捡回来,除非箭袋都没了(即弓箭的生命应该完全由箭袋管理),不捡回来的箭袋就没有意义了,还得背着。。
- 箭袋是有大小的,放了 100 支箭之后,就放不下第 101 支了(至于为什么会有第 101 支箭,是因为不是每次射箭之后都有时间把那支箭拿回来,可能一次要放五支箭出去,然后又射了三支,过一会再把这八支一起拿回来,所以在这个过程中,地上的箭和箭袋中的箭加起来,可能超过了箭袋的容量,那么,捡的时候捡满了就不捡了)
- 从箭袋中拿箭,把箭放回箭袋的操作都不麻烦,至少一定要比从地上拣树枝子打造弓箭要容易,不然每次弄新的不就好了。
1.1 使用特效池的目的
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 0UParticleSystemComponent* 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 性能有所帮助(利用 空间换时间)。
1.2 UE 中的特效池
由于 ParticleSystem 和 NiagaraSystem 是两种完全不同的特效,所以在对这两种特效的支持(释放,池化等)都是两套完全独立的代码,但是逻辑大体相似。
ParticleSystem 的池子叫 FWorldPSCPool
,NiagaraSystem 的池子叫 UNiagaraComponentPool
,下边会详细总结。
二、特效池使用
这里只记录 ParticleSystem 的特效池使用方法。首先需要知道的是,每一个特效资源,在代码中就是一个 UParticleSystem*
,每一个实际出现效果的(不管是火焰,爆炸,闪光,烟雾,粒子等等),都是用 UParticleSystemComponent
来实现的,即 UParticleSystem 是 数据,UParticleSystemComponent 是 实体 的感觉。
比如一刀看下去,三个人身上冒血,那么是三个 UParticleSystemComponent 同时在播放,但是使用的是同一个 UParticleSystem 作为 数据源,理解了这一点,下边就都好说了。
特效池的目的就是同一个数据源,创建出的多个实体进行缓存,从而达到虽然一共播放了 1000 次冒血特效,但是只 New 了 3 个 Object 的效果。
2.1 关键数据结构
详见 Engine\Source\Runtime\Engine\Classes\Particles\WorldPSCPool.h
。
![](/assets/blank.gif)
1. EPSCPoolMethod
引擎一共 提供了三种池化操作(其实 EPSCPoolMethod
枚举类型由五个值,但是只关注前三个即可):
None
即不放入池子,每一次都是重新 Create 新的AutoRelease
自动分配入池子,并且自动回收回池子中。适用于一次性效果的特效(one-shot fx),不需要考虑存起来(reference),只需要放就完事了。但是由于会自动回收,所以如果想修改这个 PSC 的属性,可能会不安全(所以默认肯定不能给这个值,因为不知道用户会不会接收释放特效的接口的返回值,并进行什么操作)。ManualRelease
需要自己手动调用 ReleaseToPool 才能回收(AutoDestroy
选项失效),适用于需要自己控制时长的 "永久" 特效(因为这种特效必须手动回收,否则会内存泄漏,所以肯定也不能是默认值)。
综上,引擎默认是不把特效放入池子的,但是如果想用,只需要把 SpawnEmitter 的接口的默认参数改为需要的即可(建议是一次性效果,如一次爆炸特效,用 AutoRelease
;持续性类似 Buff 的效果,如身上的灼烧火焰效果,用 ManualRelease
,并在火焰时间到,灼烧效果消失的时候,手动 ReleaseToPool
)。
特效的池子很简单,每一个粒子特效(UParticleSystem*
,即特效资源),对应一个数组( TArray<FPSCPoolElem> FreeElements
),这个结构也很简单,如下:
USTRUCT()
struct FPSCPoolElem
{GENERATED_BODY()UPROPERTY(transient)UParticleSystemComponent* PSC;float LastUsedTime;// 还有两个构造函数
};
2. FWorldPSCPool
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*, FPSCPool> WorldParticleSystemPools;
,这个就是存储着所有释放过的特效数据源(UParticleSystem*),与对应的创建出来的实体(UParticleSystemComponent*)的数组的 Map。
至于为什么是数组,就是因为很可能有同时播放很多特效的需求,每一个都是一个单独的 Component,如前边说到的一刀三个冒血。
3. FPSCPool
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()
- 放回池子的方法
4. FPSCPoolElem
TArray<FPSCPoolElem> FreeElements;
数组里边存储的即这个数据源创建出来的每一个实体。
其中数组的大小是有限制的,就是特效资源上配置的 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
)。
2.2 关键流程
2.2.1 播放特效 / 从池子中取
2.2.2 结束特效 / 放回池子中
放回池子的流程稍微麻烦些,因为还有定时清理功能。
void FWorldPSCPool::ReclaimWorldParticleSystem(UParticleSystemComponent* PSC)
{// Check blablablaif (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 blablablaPSCPool->Reclaim(PSC, CurrentTime);}else{PSC->DestroyComponent();}
}
每次回收一个 Component 的时候,都会判断现在距上次清理的时间隔了多久,如果超过了 GParticleSystemPoolingCleanTime
(默认值是 30.f,即 30 秒),则会对 WorldParticleSystemPools
中 所有元素 进行清理(并不只清理当前这个特效的缓存),除此之外,和 2.2.1 中的流程类似:
2.3 特效池数据查看
在编辑器中的命令行窗口,输入 fx.DumpPSCPoolInfo
,可以在 Output 窗口中看到当前池子的大小,以及每个 PS 有多少个 Free 的,有多少个正在 Used 的。
上边这张图可以看到,当前池子一共占内存 0.7 MB,每一个特效资源(ParticleSystem),会输出对应的数据:
Free
- 当前池子中可用的 Component 实体Used
- 当前正在使用的 Component 实体(Auto、Manual 对应释放时设置的池化方法)MaxUsed
- 最多同一时刻一起使用的 Component 实体数量(就是FreeElements
数组的大小)System
- 特效资源的路径
吐槽一下,第一行的输出,不应该换一行吗。。。。。。看起来好难受。。
以及并不能看到减少了多少次 NewObject
三、特效池需要注意的问题
3.1 生命周期管理
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 回当前 World - 如果是
AutoRelease
方式,那就需要用别的方式修改顿帧了
3.2 Reset
但凡设计回收冲利用的机制,就避免不了需要 Reset。但是
总结就是如果需要使用池子,那就不要对返回值 PSC 做任何操作了。
再吐槽一下,修改 Owner 竟然没有 SetOwner 这种函数,而是用 Rename。。感觉很奇怪,可能就是不想让你可以 SetOwner 吧
个人知乎:https://www.zhihu.com/people/gaoy-88
【UE4】特效之 Particle System 详解(二)—— 特效池相关推荐
- linux 进程间通信 dbus-glib【实例】详解二(下) 消息和消息总线(ListActivatableNames和服务器的自动启动)(附代码)
linux 进程间通信 dbus-glib[实例]详解一(附代码)(d-feet工具使用) linux 进程间通信 dbus-glib[实例]详解二(上) 消息和消息总线(附代码) linux 进程间 ...
- linux 进程间通信 dbus-glib【实例】详解二(上) 消息和消息总线(附代码)
linux 进程间通信 dbus-glib[实例]详解一(附代码)(d-feet工具使用) linux 进程间通信 dbus-glib[实例]详解二(上) 消息和消息总线(附代码) linux 进程间 ...
- Pytorch|YOWO原理及代码详解(二)
Pytorch|YOWO原理及代码详解(二) 本博客上接,Pytorch|YOWO原理及代码详解(一),阅前可看. 1.正式训练 if opt.evaluate:logging('evaluating ...
- Android openGl开发详解(二)
https://zhuanlan.zhihu.com/p/35192609 Android openGl开发详解(二)--通过SurfaceView,TextureView,GlSurfaceView ...
- PackageManagerService启动详解(二)之怎么通过packages.xml对已安装应用信息进行持久化管理?
PKMS启动详解(二)之怎么通过packages.xml对已安装应用信息进行持久化管理? Android PackageManagerService系列博客目录: PKMS启动详解系列博客概要 PKM ...
- Android 融云IM集成以及使用详解(二)
Android 融云IM集成以及使用详解(二) 上篇讲解了集成和好友列表和消息记录的使用,这篇将讲解聊天界面和群聊界面的使用 先附上一张效果图 先介绍布局文件 <LinearLayout xml ...
- 安卓 linux init.rc,[原创]Android init.rc文件解析过程详解(二)
Android init.rc文件解析过程详解(二) 3.parse_new_section代码如下: void parse_new_section(struct parse_state *state ...
- [转]文件IO详解(二)---文件描述符(fd)和inode号的关系
原文:https://www.cnblogs.com/frank-yxs/p/5925563.html 文件IO详解(二)---文件描述符(fd)和inode号的关系 ---------------- ...
- PopUpWindow使用详解(二)——进阶及答疑
相关文章: 1.<PopUpWindow使用详解(一)--基本使用> 2.<PopUpWindow使用详解(二)--进阶及答疑> 上篇为大家基本讲述了有关PopupWindow ...
- Android init.rc文件解析过程详解(二)
Android init.rc文件解析过程详解(二) 3.parse_new_section代码如下: void parse_new_section(struct parse_state *state ...
最新文章
- OpenCV图像处理常用手段
- UVa LA 3882 - And Then There Was One 递推,动态规划 难度: 2
- boost::yap::value相关的测试程序
- c#字符串操作方法实例
- Educational Codeforces Round 101 (Rated for Div. 2) F. Power Sockets 哈希 + 乱搞
- 31 FI配置-财务会计-应收账款和应付账款-定义容差组(供应商)
- eigrp配置实验_EIGRP的认证的配置
- vue 打印出git提交信息_VUE项目构建打包生成Git信息(VERSION和COMMITHASH文件)
- Kong 网关API安装部署以及应用实例----------腾云驾雾
- 【bzoj 入门OJ】[NOIP 热身赛]Problem C: 星球联盟(并查集)
- 开课吧:人工智能是后互联时代的发展路径和方向
- priority_queue的优先级设置
- 《易学Python》——第1章 为何学习Python 1.1 学习编程
- 超像素、语义分割、实例分割、全景分割
- ps复制选中的图层为新图层
- HTML基本标签归纳总结
- 如果睡眠不足,我们的大脑会怎么样?
- Python神经网络编程学习记录(一)
- 修改数据表之添加主键约束
- 迎接Ubuntu Flatpak Remix,预装了Flatpak支持的Ubuntu
热门文章
- 百战程序员怎么样?python介绍和了解python是什么
- echarts地图示例
- step 7在win10上安装教程及安装包
- 《第一本无人驾驶技术书》扫描版PDF分享
- AD PCB画图注意点
- 【转】推荐几本学习MySQL的好书-MySQL 深入的书籍
- osgb转json_基于CAD平台的OSGB数据分级渲染的方法与流程
- Tensorflow+Keras+VGG19 猫狗大战分类
- 小程序专题:14款活动报名小程序
- ailed to send crash report due to a network error: SocketException: OS Error: 信号灯超时时间已到 , errno = 12