[UE4] Detailed explanation of Particle System for special effects (2) - Special effects pool

  For some basic knowledge about particle special effects, you can refer to [UE4] Special Effects Particle System Detailed Explanation (1) - Overview

1. What is the special effects pool for?

  Let’s use a particle to explain the pool:
  For example, if you are an archer, you can shoot arrows, and you can pick up branches from the ground (memory) to make bows and arrows (NewObject).

  • If you don’t have a quiver
    , every time you want to shoot an arrow, you need to pick up branches from the ground to make a bow and arrow. This process is very troublesome, so your efficiency is very low.
  • If you have a quiver on your back
    , you can pick up branches from the ground to make a bow and arrow, and after shooting them, pick up the bow and arrow and insert them into the quiver. Next time you want to shoot, if there are bows and arrows in the quiver, just take them out. Just shoot it, no need to recreate it

  The above is my personal understanding. If you have any questions, you can discuss them.
  It is worth noting that:

  1. The basis for doing this is that every time a bow and arrow is shot, it will be picked up eventually. Unless the quiver is gone (that is, the life of the bow and arrow should be completely managed by the quiver), the quiver that is not picked up will be meaningless. Have to carry it. .
  2. The quiver has its own size. After putting 100 arrows in it, there will be no room for the 101st arrow. (As for why there is a 101st arrow, it is because not every time you shoot an arrow, you have time to get that arrow back. Maybe once. You need to release five arrows, then shoot three more, and then bring back the eight arrows together after a while. Therefore, during this process, the sum of the arrows on the ground and the arrows in the quiver may exceed the number of arrows in the quiver. Capacity, then if it is full when picking up, it will not be picked up)
  3. The operation of taking arrows from the quiver and putting the arrows back into the quiver is not troublesome, at least it must be easier than picking branches from the ground to make bows and arrows, otherwise it would be a waste to get new ones every time.

1.1 The purpose of using the special effects pool

  The release of ParticleSystem (commonly known as particle effects) ultimately calls CreateParticleSystemthe function, as shown below:

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;
}

  The core logic is that if the PoolMethod is not None, it will be taken from the pool; if it is None, it will be NewObject, that is, every time a special effect is released, a new one will be created.
  NiagaraSystem (commonly known as Naigua special effects) is the same, the interface CreateNiagaraSystemis NewObject every time.

Note: It is worth noting that both special effects are judgedWorld && !World->IsNetMode(NM_DedicatedServer)
, that is, the special effects will not be created on the server, so you do not need to care about the special effects on the serverNewObject, but if it is
the ParticleSystemComponent mounted on the Actor (whether Whether from code or blueprint resources), the Component will be created on the server. This needs to be noted.

  Since NewObject will perform a series of operations, it will definitely consume the CPU (GameThread) (although one may not be many, but it cannot support a large number), so if you can use the pool for caching, it will not be created new every time, but from Taking it from the pool can help CPU performance (using space for time ).

1.2 Special effects pool in UE

  Since ParticleSystem and NiagaraSystem are two completely different special effects, the support for these two special effects (release, pooling, etc.) are two completely independent sets of codes, but the logic is generally similar.
  The pool of ParticleSystem is called FWorldPSCPool, and the pool of NiagaraSystem is called UNiagaraComponentPool, which will be summarized in detail below.

2. Use of special effects pool

  Here we only record how to use the special effects pool of ParticleSystem. The first thing you need to know is that every special effects resource is one in the code. UParticleSystem*Every actual effect (whether it is flame, explosion, flash, smoke, particles, etc.) is UParticleSystemComponentimplemented by , that is, UParticleSystem is data , UParticleSystemComponent is an entity .
  For example, if you look down and see three people bleeding, then three UParticleSystemComponents are playing at the same time, but they use the same UParticleSystem as the data source . Once you understand this, the following will be easy to explain.
  The purpose of the special effects pool is to cache multiple entities created from the same data source, thereby achieving the effect that although the blood effect was played a total of 1,000 times, only 3 Objects were New.

2.1 Key data structures

  See details Engine\Source\Runtime\Engine\Classes\Particles\WorldPSCPool.h.

1. EPSCPoolMethod

  The engine provides a total of three pooling operations (in fact, EPSCPoolMethodthe enumeration type has five values, but only the first three are worth paying attention to):

  1. None
    That is, it is not put into the pool, and a new one is created every time.
  2. AutoRelease
    Automatically allocated into the pool and automatically recycled back into the pool. For special effects that are suitable for one-shot effects (one-shot fx), there is no need to think about saving them (reference), just put them in and that's it. However, since it will be automatically recycled, it may be unsafe if you want to modify the properties of this PSC (so this value cannot be given by default, because it is not known whether the user will receive the return value of the interface that releases the special effect and what operations will be performed).
  3. ManualRelease
    You need to manually call ReleaseToPool to recycle ( AutoDestroythe option is invalid), which is suitable for "permanent" special effects that need to be controlled by yourself (because this kind of special effect must be manually recycled, otherwise it will cause memory leaks, so it certainly cannot be the default value).

  To sum up, the engine does not put special effects into the pool by default, but if you want to use it, you only need to change the default parameters of the SpawnEmitter interface to what you need (it is recommended to use one-time effects, such as an explosion special effect; use; AutoReleasepersistence Buff-like effects, such as the burning flame effect on the body, use it ManualRelease, and when the flame time is up and the burning effect disappears, manually ReleaseToPool).

  The special effects pool is very simple. Each particle special effect ( UParticleSystem*, that is, special effects resource) corresponds to an array ( TArray<FPSCPoolElem> FreeElements). This structure is also very simple, as follows:

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 provides a pool for particle special effects by default, called FWorldPSCPool (Niagara's pool is called UNiagaraComponentPool ), but if you use the interface for releasing particle special effects in UGameplayStatics (whether or not SpawnEmitterAtLocation) SpawnEmitterAttached, the special effects will not be put into the pool by default. (That is EPSCPoolMethod::None, the reason may be that the engine does not know what default logic it should give).
  The life cycle of FWorldPSCPool can be considered to be managed by World. There is a variable in World:

UPROPERTY()
FWorldPSCPool PSCPool;

  In World, UWorld::CleanupWorldInternalit will be called to PSCPool.Cleanup()clean up the special effects pool.
  When World is destructed, FWorldPSCPoolthe destructor will be called and executed Cleanup(). Will be called
  when .CreateParticleSystemWorld->GetPSCPool().CreateWorldParticleSystem

  The most important thing in FWorldPSCPoolTMap<UParticleSystem*, FPSCPool> WorldParticleSystemPools; is that this is a Map that stores arrays of all released special effects data sources (UParticleSystem*) and corresponding created entities (UParticleSystemComponent*).

  As for why it is an array, it is because there may be a need to play many special effects at the same time, each of which is a separate Component, such as the three swords that produce blood as mentioned earlier.

3. FPSCPool

  FWorldPSCPool::CreateWorldParticleSystemWe will use the special effect as the key to find out the small pool of this special effect from WorldParticleSystemPools, and find out the available entities from it.

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(); }
};

  Key member variables and functions:

  • FreeElements- It stores the entity Component available for this special effect. The one currently being used is not here and has not been recycled into the pool.
  • InUseComponents_Auto, InUseComponents_Manual- You can leave it alone, it can be considered to be used for debugging ( ENABLE_PSC_POOL_DEBUGGING)
  • MaxUsed- How many are used at most?
  • Acquire()- Method used to extract available Components from its own array
  • Reclaim()- How to put it back into the pool
4. FPSCPoolElem

  TArray<FPSCPoolElem> FreeElements;What is stored in the array is each entity created by this data source.
  There is a limit to the size of the array, which is configured on the special effects resource MaxPoolSize(see the code for details FPSCPool::Reclaim, if FreeElements.Num() < (int32)PSC->Template->MaxPoolSize, this Component will not be recycled, but will be DestroyComponent directly).
Insert image description here

USTRUCT()
struct FPSCPoolElem
{
    
    
	GENERATED_BODY()

	UPROPERTY(transient)
	UParticleSystemComponent* PSC;

	float LastUsedTime;

	// 两个构造函数
};

  FPSCPoolElem only contains the entity (PSC) and the last time this entity was used (LastUsedTime), which is used for timeout elimination, etc. (see details FPSCPool::KillUnusedComponents).

2.2 Key processes

2.2.1 Play special effects/take from pool

Insert image description here

2.2.2 End special effects/put back into the pool

  The process of putting it back into the pool is a little more troublesome because there is also a scheduled cleaning function.

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();
	}
}

  Every time a Component is recycled, it will be judged how long it has been since the last time it was cleaned. If it exceeds GParticleSystemPoolingCleanTime(the default value is 30.f, which is 30 seconds), all elementsWorldParticleSystemPools in will be cleaned (not just the current one) cache of this special effect), except that the process is similar to the process in 2.2.1:
Insert image description here

2.3 View special effects pool data

  In the command line window in the editor, enter fx.DumpPSCPoolInfo, and you can see the current pool size in the Output window, as well as how many Free and How many are Used in each PS.
Insert image description here
  As you can see from the picture above, the current pool occupies a total of 0.7 MB of memory. Each special effects resource (ParticleSystem) will output corresponding data:

  • Free- Component entities available in the current pool
  • Used- The Component entity currently in use (Auto and Manual correspond to the pooling method set when releasing)
  • MaxUsed- The maximum number of Component entities used together at the same time (that is, FreeElementsthe size of the array)
  • System- Path to special effects resources

Let me complain, shouldn't the first line of output be changed to another line? . . . . . It looks so uncomfortable. .
And you can't see how many times NewObject has been reduced

3. Issues that need attention in the special effects pool

3.1 Life cycle management

  FPSCPool::AcquireIn the middle step RetElem.PSC->Rename(nullptr, World, REN_ForceNoResetLoaders);, set the Component OwnerPrivate(GwtOwner()) to the world. The official comment is:

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.

  That is to prevent you from creating this special effect component in one level and wanting to use it in another level, so it all exists in the world. After all, it exists in the world PSCPool.
  However, because the frame of the special effect (that is, the DeltaTime of the Tick is related to OwnerPrivate, see details FActorComponentTickFunction::ExecuteTickHelper), if you want the speed of the special effect to be consistent with character A, then you need to set PSC->SpawnedParticle->Rename(nullptr, Actor);its OwnerPrivateto character A.
  This will cause that when the character A Destroy, the Component on it will also be destroyed, which will trigger FPSCPool::Acquirethe check in:

check(!RetElem.PSC->IsPendingKill());

  Possible solutions are:

  1. If it is ManualReleasethe method, you can directly Rename, but you need to ReleaseToPoolreturn to the current World in Rename before
  2. If it is AutoReleasethe method, then you need to modify the frame in other ways.

3.2 Reset

  Whenever you design a recycling and reuse mechanism, you cannot avoid the need for Reset. but

  The summary is that if you need to use a pool, don't do anything with the return value PSC.

Let me complain again, when modifying the Owner, there is no such function as SetOwner, but Rename is used. . It feels weird. Maybe I don’t want you to be able to SetOwner.

Personal knowledge: https://www.zhihu.com/people/gaoy-88

Guess you like

Origin blog.csdn.net/Bob__yuan/article/details/119079027