关于粒子特效的一些基础知识,可以参考 【UE4】特效之 Particle System 详解(一)—— 综述

一、特效池是干嘛的

  举个粒子解释一下池子:
  比如你是弓箭手,你会射箭,你会从地上(内存)捡树枝打造弓箭(NewObject)。

  • 如果没有箭袋
    那每次想射箭,都需要从地上捡树枝打造弓箭,这个过程想想就很麻烦,所以你的效率很低
  • 如果背上背了箭袋
    那么你可以在从地上捡树枝打造弓箭,并射出去之后,把弓箭捡回来,插到箭袋里,下次想射箭,如果箭袋里有弓箭,那直接拿出来射就行,不需要重新打造

  以上是个人理解,有问题可以讨论~
  值得注意的是:

  1. 能这样做的基础是,每次射出去的弓箭,最后都会捡回来,除非箭袋都没了(即弓箭的生命应该完全由箭袋管理),不捡回来的箭袋就没有意义了,还得背着。。
  2. 箭袋是有大小的,放了 100 支箭之后,就放不下第 101 支了(至于为什么会有第 101 支箭,是因为不是每次射箭之后都有时间把那支箭拿回来,可能一次要放五支箭出去,然后又射了三支,过一会再把这八支一起拿回来,所以在这个过程中,地上的箭和箭袋中的箭加起来,可能超过了箭袋的容量,那么,捡的时候捡满了就不捡了)
  3. 从箭袋中拿箭,把箭放回箭袋的操作都不麻烦,至少一定要比从地上拣树枝子打造弓箭要容易,不然每次弄新的不就好了。

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

1. EPSCPoolMethod

  引擎一共 提供了三种池化操作(其实 EPSCPoolMethod 枚举类型由五个值,但是只关注前三个即可):

  1. None
    即不放入池子,每一次都是重新 Create 新的
  2. AutoRelease
    自动分配入池子,并且自动回收回池子中。适用于一次性效果的特效(one-shot fx),不需要考虑存起来(reference),只需要放就完事了。但是由于会自动回收,所以如果想修改这个 PSC 的属性,可能会不安全(所以默认肯定不能给这个值,因为不知道用户会不会接收释放特效的接口的返回值,并进行什么操作)。
  3. 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_AutoInUseComponents_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());

  可行的解决方案就是:

  1. 如果是 ManualRelease 方式,那么可以直接 Rename,只不过需要在 ReleaseToPool 之前在 Rename 回当前 World
  2. 如果是 AutoRelease 方式,那就需要用别的方式修改顿帧了

3.2 Reset

  但凡设计回收冲利用的机制,就避免不了需要 Reset。但是

  总结就是如果需要使用池子,那就不要对返回值 PSC 做任何操作了。

再吐槽一下,修改 Owner 竟然没有 SetOwner 这种函数,而是用 Rename。。感觉很奇怪,可能就是不想让你可以 SetOwner 吧

个人知乎:https://www.zhihu.com/people/gaoy-88

【UE4】特效之 Particle System 详解(二)—— 特效池相关推荐

  1. linux 进程间通信 dbus-glib【实例】详解二(下) 消息和消息总线(ListActivatableNames和服务器的自动启动)(附代码)

    linux 进程间通信 dbus-glib[实例]详解一(附代码)(d-feet工具使用) linux 进程间通信 dbus-glib[实例]详解二(上) 消息和消息总线(附代码) linux 进程间 ...

  2. linux 进程间通信 dbus-glib【实例】详解二(上) 消息和消息总线(附代码)

    linux 进程间通信 dbus-glib[实例]详解一(附代码)(d-feet工具使用) linux 进程间通信 dbus-glib[实例]详解二(上) 消息和消息总线(附代码) linux 进程间 ...

  3. Pytorch|YOWO原理及代码详解(二)

    Pytorch|YOWO原理及代码详解(二) 本博客上接,Pytorch|YOWO原理及代码详解(一),阅前可看. 1.正式训练 if opt.evaluate:logging('evaluating ...

  4. Android openGl开发详解(二)

    https://zhuanlan.zhihu.com/p/35192609 Android openGl开发详解(二)--通过SurfaceView,TextureView,GlSurfaceView ...

  5. PackageManagerService启动详解(二)之怎么通过packages.xml对已安装应用信息进行持久化管理?

    PKMS启动详解(二)之怎么通过packages.xml对已安装应用信息进行持久化管理? Android PackageManagerService系列博客目录: PKMS启动详解系列博客概要 PKM ...

  6. Android 融云IM集成以及使用详解(二)

    Android 融云IM集成以及使用详解(二) 上篇讲解了集成和好友列表和消息记录的使用,这篇将讲解聊天界面和群聊界面的使用 先附上一张效果图 先介绍布局文件 <LinearLayout xml ...

  7. 安卓 linux init.rc,[原创]Android init.rc文件解析过程详解(二)

    Android init.rc文件解析过程详解(二) 3.parse_new_section代码如下: void parse_new_section(struct parse_state *state ...

  8. [转]文件IO详解(二)---文件描述符(fd)和inode号的关系

    原文:https://www.cnblogs.com/frank-yxs/p/5925563.html 文件IO详解(二)---文件描述符(fd)和inode号的关系 ---------------- ...

  9. PopUpWindow使用详解(二)——进阶及答疑

    相关文章: 1.<PopUpWindow使用详解(一)--基本使用> 2.<PopUpWindow使用详解(二)--进阶及答疑> 上篇为大家基本讲述了有关PopupWindow ...

  10. Android init.rc文件解析过程详解(二)

    Android init.rc文件解析过程详解(二) 3.parse_new_section代码如下: void parse_new_section(struct parse_state *state ...

最新文章

  1. OpenCV图像处理常用手段
  2. UVa LA 3882 - And Then There Was One 递推,动态规划 难度: 2
  3. boost::yap::value相关的测试程序
  4. c#字符串操作方法实例
  5. Educational Codeforces Round 101 (Rated for Div. 2) F. Power Sockets 哈希 + 乱搞
  6. 31 FI配置-财务会计-应收账款和应付账款-定义容差组(供应商)
  7. eigrp配置实验_EIGRP的认证的配置
  8. vue 打印出git提交信息_VUE项目构建打包生成Git信息(VERSION和COMMITHASH文件)
  9. Kong 网关API安装部署以及应用实例----------腾云驾雾
  10. 【bzoj 入门OJ】[NOIP 热身赛]Problem C: 星球联盟(并查集)
  11. 开课吧:人工智能是后互联时代的发展路径和方向
  12. priority_queue的优先级设置
  13. 《易学Python》——第1章 为何学习Python 1.1 学习编程
  14. 超像素、语义分割、实例分割、全景分割
  15. ps复制选中的图层为新图层
  16. HTML基本标签归纳总结
  17. 如果睡眠不足,我们的大脑会怎么样?
  18. Python神经网络编程学习记录(一)
  19. 修改数据表之添加主键约束
  20. 迎接Ubuntu Flatpak Remix,预装了Flatpak支持的Ubuntu

热门文章

  1. 百战程序员怎么样?python介绍和了解python是什么
  2. echarts地图示例
  3. step 7在win10上安装教程及安装包
  4. 《第一本无人驾驶技术书》扫描版PDF分享
  5. AD PCB画图注意点
  6. 【转】推荐几本学习MySQL的好书-MySQL 深入的书籍
  7. osgb转json_基于CAD平台的OSGB数据分级渲染的方法与流程
  8. Tensorflow+Keras+VGG19 猫狗大战分类
  9. 小程序专题:14款活动报名小程序
  10. ailed to send crash report due to a network error: SocketException: OS Error: 信号灯超时时间已到 , errno = 12