Spawn particles on a ground in a circle

I would like to spawn a particle system on the ground in a circle, for example.
I took a fire from the content example.
I can spawn a the particle system in C++, no problem.
But now, I want to fill a circle on the ground. What is the best way to achieve that ? Since changing the box size of the particle system modify the apparence of the particle system.

Is it the best to spawn several particle systems on the ground to fill the space and therefore avoid modifying the particle system aspect ?

Txs for your suggestions.
D.

I’ve implemented a solution that spawn several emitters in the circle.
I keep them in an array to be able to destroy them when required.

Hey man, would you mind to share the code. I am not a programmer and I am trying to do something similar, so it would be of great help. Thank you.

This spell class is like a bullet or a rocket. When it touch the ground, I spawn the effect in a circle or ground actors, it depends on the spell configuration.

The OnImpact method is called with the HitResult as a parameter.
TouchedGround() is called from the server in case you have a client/server game.
WeaponConfig.Spell.SpellLife is the time for the spell. (effects or ground actors).
WeaponConfig.Spell.DistanceGapInRadius defines the gap between effects or ground actors. If 0, then only one is spawned immediatly. If not, then the gap is used to fill the circle and effects or ground actors are spawned using a timer.
For spell and ground actor, the algorithm is the same. Start from the center and spawn effects or actors using the gap. Keep the spawn objects within an array to be able to destroy then once the lifetime is reached.

For filling the circle it is very simple.

  1. randomize the angle to avoid linear effect

  2. NumPoints is calculated and used to known how many objects I have to spawn

  3. DeltaPoints is used to spawn the number of objects during on call of the SpawnEffectActors()

  4. Once the number of object is reached, calculate the new number of object to spawn during the next call of SpawnEffectActors()

  5. Once the DistanceFromCenter is greather than the spell radius, stop the timer to stop spawning effects or ground actors

  6. Finally, the function DisableAndDestroy is in charge of deactivating and destroying the effects kept within the array and the ground actor also

    void AKSpell::OnImpact(const FHitResult& HitResult)
    {
    UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::OnImpact - Role=%s HitResult.ImpactPoint=%s HitResult.ImpactNormal=%s”), NETROLETOSTRING(Role), *HitResult.ImpactPoint.ToString(), *HitResult.ImpactNormal.ToString());
    if (Role == ROLE_Authority && !bTouchedGround)
    {
    TouchedGround(HitResult);
    // Since we have loop effects in the emitters
    // we cannot destroy the particles only when we are on the server
    // we need also to deactivate from the client
    // otherwise the effect still on the clients
    }
    }

    void AKSpell::TouchedGround(const FHitResult& Impact)
    {
    UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::TouchedGround - Role=%s”), NETROLETOSTRING(Role));
    if (ParticleComp)
    {
    ParticleComp->Deactivate();
    }
    // Keep track of the time this spell touch the ground
    TouchTime = GetWorld()->GetTimeSeconds();

    // effects and damage origin shouldn't be placed inside mesh at impact point
    NudgedImpactLocation = Impact.ImpactPoint + Impact.ImpactNormal * 10.0f;
    UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround  -  NudgedImpactLocation=%f"), *NudgedImpactLocation.ToString());
    
    // Apply the spell damage at impact
    if (WeaponConfig.Spell.SpellImpactDamage > 0 && WeaponConfig.Spell.SpellRadius > 0 && WeaponConfig.DamageType)
    {
    	UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround  -  apply damage at impact   World time=%f"), TouchTime);
    	UGameplayStatics::ApplyRadialDamage(this, WeaponConfig.Spell.SpellImpactDamage, NudgedImpactLocation, WeaponConfig.Spell.SpellRadius, WeaponConfig.DamageType, TArray<AActor*>(), this, MyController.Get(), WeaponConfig.FullDamage);
    }
    // Apply alterations at impact
    if (WeaponConfig.Alterations.Num() > 0 && WeaponConfig.Spell.SpellRadius > 0 && WeaponConfig.DamageType)
    {
    	UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround  -  apply alterations at impact   World time=%f"), TouchTime);
    	UKAttributeHelper::ApplyRadialAlterations(this, SpellGuid, WeaponConfig.Alterations, NudgedImpactLocation, WeaponConfig.Spell.SpellRadius, WeaponConfig.DamageType, TArray<AActor*>(), this, MyController.Get());
    
    }
    // Start the timer to destroy at the life time end
    if (WeaponConfig.Spell.SpellLife > 0)
    {
    	// Start the timer to disable and destroy after the spell life time
    	UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround  -  launch timer to DisableAndDestroy after %f seconds"), WeaponConfig.Spell.SpellLife);
    	FTimerHandle TempHandle2;
    	GetWorldTimerManager().SetTimer(TempHandle2, this, &AKSpell::DisableAndDestroy, WeaponConfig.Spell.SpellLife, false);
    }
    else
    {
    	// if no life, we can disable and destroy
    	DisableAndDestroy();
    }
    
    // Start the timer to apply recurring spell damage
    if (WeaponConfig.Spell.SpellLife > 0 && WeaponConfig.Spell.SpellRecurringDamageInterval > 0 &&
    	WeaponConfig.Spell.SpellRecurringDamage > 0 && WeaponConfig.Spell.SpellRadius > 0 && WeaponConfig.DamageType)
    {
    	UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround  -  launch timer to apply recurring damage   World time=%f"), TouchTime);
    	FTimerHandle TempHandle;
    	GetWorldTimerManager().SetTimer(TempHandle, this, &AKSpell::ApplyRecurringDamage, WeaponConfig.Spell.SpellRecurringDamageInterval, false);
    }
    
    // Do we have the spell effect ?
    if (GroundTemplate)
    {
    	// If we have a distance gap defined we will need to spawn several actors
    	if (WeaponConfig.Spell.DistanceGapInRadius > 0.0f)
    	{
    		// Initialize 
    		DistanceFromCenter = 0.0f;
    		NumPoints = 1;
    		// Call a loop timer function to spawn the effect actors
    		GetWorldTimerManager().SetTimer(SpawnEffectActors_Handle, FTimerDelegate::CreateUObject(this, &AKSpell::SpawnEffectActors, Impact), 0.2f, true);
    	}
    	else // Otherwise, we have only one effect to spawn
    	{
    		FTransform const SpawnTransform(FRotator(0, 0, 0), NudgedImpactLocation);
    		AKWeaponFireGroundEffect* EffectActor = GetWorld()->SpawnActorDeferred<AKWeaponFireGroundEffect>(GroundTemplate, SpawnTransform);
    		if (EffectActor)
    		{
    			EffectActor->SurfaceHit = Impact;
    			// Set its life time
    			EffectActor->GroundEffectLifeTime = WeaponConfig.Spell.SpellLife;
    			// Set its color based on the damage type
    			EffectActor->SetColor(GetWeaponConfigDamageTypeColor(WeaponConfig));
    			UGameplayStatics::FinishSpawningActor(EffectActor, SpawnTransform);
    			// Add the actor to the array
    			EffectActors.Add(EffectActor);
    		}
    	}
    }
    else
    {
    	UE_LOG(LogKSGMWeapon, Warning, TEXT("AKSpell::TouchedGround  -  GroundTemplate is not set."));
    }
    
    // Do we have a ground actor ?
    // For an actor only spawned on the server side
    // never on the client.
    if (Role == ROLE_Authority && GroundActor)
    {
    	// If we have a distance gap defined we will need to spawn several actors
    	if (WeaponConfig.Spell.DistanceGapInRadius > 0.0f)
    	{
    		// Initialize 
    		GDistanceFromCenter = 0.0f;
    		GNumPoints = 1;
    		// Call a loop timer function to spawn the ground actors
    		GetWorldTimerManager().SetTimer(SpawnGroundActors_Handle, FTimerDelegate::CreateUObject(this, &AKSpell::SpawnGroundActors, Impact), 0.2f, true);
    	}
    	else // Otherwise, we have only one actor to spawn
    	{
    		// We apply a scale to fill the area since we have only one actor
    		FTransform const SpawnTransform(FRotator(0, 0, 0), NudgedImpactLocation, FVector(WeaponConfig.Spell.SpellRadius / GroundActorBoundSize, WeaponConfig.Spell.SpellRadius / GroundActorBoundSize, 1.0f));
    		UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround  -  GetActorLocation=%s"), *SpawnTransform.GetLocation().ToString());
    		UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround  -  GetActorScale=%s"), *SpawnTransform.GetScale3D().ToString());
    		AKGroundActor* GActor = GetWorld()->SpawnActorDeferred<AKGroundActor>(GroundActor, SpawnTransform);
    		if (GActor)
    		{
    			UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::TouchedGround  -  Ground Actor spawned=%p"), GActor);
    			// Do we have deferred collision
    			if (GActor->DeferredCollisionTime > 0.f)
    			{
    				GActor->SetActorEnableCollision(false);
    				SpawnedGroundActors.Add(GActor);
    			}
    			UGameplayStatics::FinishSpawningActor(GActor, SpawnTransform);
    			// Add the actor to the array
    			GroundActors.Add(GActor);
    			// Do we have collisions to sets back
    			if (SpawnedGroundActors.Num() > 0)
    			{
    				// Start the timer to enable back the collision
    				FTimerHandle TempHandle2;
    				GetWorldTimerManager().SetTimer(TempHandle2, this, &AKSpell::SetBackCollision2GroundActors, GActor->DeferredCollisionTime, false);
    			}
    
    		}
    	}
    }
    
    bTouchedGround = true;
    

    }

    void AKSpell::SpawnEffectActors(const FHitResult Impact)
    {
    FVector Distance(DistanceFromCenter, 0.0f, 0.0f);
    int32 count = EffectActors.Num();
    int32 DeltaPoints = NumPoints - count;
    UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::SpawnEffectActors - Role=%s DistanceFromCenter=%f count=%i NumPoints=%i DeltaPoints=%i”), NETROLETOSTRING(Role), DistanceFromCenter, count, NumPoints, DeltaPoints);

    for (int32 i = 0; i < DeltaPoints; ++i)
    {
    	float Percent = (float)i / (float)DeltaPoints;
    	float Angle = FMath::Lerp(0.0f, 360.0f, Percent);
    	float AngleDeg = FRotator::ClampAxis(Angle * 180.f / PI);
    	// Randomize the angle to avoid linear effects
    	Angle = FMath::RandRange(Angle - (360.0f / DeltaPoints / 4), Angle + (360.0f / DeltaPoints / 4));
    	FVector VertexDir = (i > 0) ? Distance.RotateAngleAxis(Angle, FVector(0.f, 0.f, 1.f)) : FVector(DistanceFromCenter,0.f,0.f);
    	//VertexDir.Normalize();
    	FVector FinalImpactLocation = NudgedImpactLocation + VertexDir;
    
    	// Set the rotation to zero
    	FTransform const SpawnTransform(FRotator(0,0,0), FinalImpactLocation);
    	AKWeaponFireGroundEffect* EffectActor = GetWorld()->SpawnActorDeferred<AKWeaponFireGroundEffect>(GroundTemplate, SpawnTransform);
    	if (EffectActor)
    	{
    		EffectActor->SurfaceHit = Impact;
    		// Set its life time
    		EffectActor->GroundEffectLifeTime = WeaponConfig.Spell.SpellLife;
    		// Set its color based on the damage type
    		EffectActor->SetColor(GetWeaponConfigDamageTypeColor(WeaponConfig));
    		UGameplayStatics::FinishSpawningActor(EffectActor, SpawnTransform);
    		// Add the actor to the array
    		count = EffectActors.Add(EffectActor) + 1;
    
    		// Check if we have to increase the distance from the center
    		// and calculate the new number of points to reach
    		if (count >= NumPoints)
    		{
    			DistanceFromCenter += WeaponConfig.Spell.DistanceGapInRadius;
    			UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::SpawnEffectActors  -  DistanceFromCenter=%f   WeaponConfig.Spell.DistanceGapInRadius=%f"), DistanceFromCenter, WeaponConfig.Spell.DistanceGapInRadius);
    			NumPoints += (int32)(DistanceFromCenter / WeaponConfig.Spell.DistanceGapInRadius) * 4;
    		}
    	}
    }
    // Do we reach the end of the circle
    if (DistanceFromCenter >= WeaponConfig.Spell.SpellRadius)
    {
    	GetWorldTimerManager().ClearTimer(SpawnEffectActors_Handle);
    	UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::SpawnEffectActors  -  Reach radius  DistanceFromCenter=%f   WeaponConfig.Spell.SpellRadius=%f   Count=%i"), DistanceFromCenter, WeaponConfig.Spell.SpellRadius, EffectActors.Num());
    }
    

    }

    void AKSpell::SpawnGroundActors(const FHitResult Impact)
    {
    FVector Distance(GDistanceFromCenter, 0.0f, 0.0f);
    int32 count = EffectActors.Num();
    int32 DeltaPoints = GNumPoints - count;
    UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::SpawnGroundActors - Role=%s DistanceFromCenter=%f count=%i NumPoints=%i DeltaPoints=%i”), NETROLETOSTRING(Role), GDistanceFromCenter, count, GNumPoints, DeltaPoints);

    float DeferredCollisionTime = 0.0f;
    for (int32 i = 0; i < DeltaPoints; ++i)
    {
    	float Percent = (float)i / (float)DeltaPoints;
    	float Angle = FMath::Lerp(0.0f, 360.0f, Percent);
    	float AngleDeg = FRotator::ClampAxis(Angle * 180.f / PI);
    	// Randomize the angle to avoid linear effects
    	Angle = FMath::RandRange(Angle - (360.0f / DeltaPoints / 4), Angle + (360.0f / DeltaPoints / 4));
    	FVector VertexDir = (i > 0) ? Distance.RotateAngleAxis(Angle, FVector(0.f, 0.f, 1.f)) : FVector(GDistanceFromCenter, 0.f, 0.f);
    	FVector FinalImpactLocation = NudgedImpactLocation + VertexDir;
    
    	// Set the rotation to zero
    	// Don't modify the scale since we have several actors to fill the spell area
    	FTransform const SpawnTransform(FRotator(0, 0, 0), FinalImpactLocation);
    	AKGroundActor* GActor = GetWorld()->SpawnActorDeferred<AKGroundActor>(GroundActor, SpawnTransform);
    	if (GActor)
    	{
    		// Do we have deferred collision
    		if ((DeferredCollisionTime = GActor->DeferredCollisionTime) > 0.f)
    		{ 
    			GActor->SetActorCollision(false);
    			SpawnedGroundActors.Add(GActor);
    		}
    
    		UGameplayStatics::FinishSpawningActor(GActor, SpawnTransform);
    		// Add the actor to the array
    		count = GroundActors.Add(GActor) + 1;
    
    		// Check if we have to increase the distance from the center
    		// and calculate the new number of points to reach
    		if (count >= GNumPoints)
    		{
    			GDistanceFromCenter += WeaponConfig.Spell.DistanceGapInRadius;
    			UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::SpawnGroundActors  -  DistanceFromCenter=%f   WeaponConfig.Spell.DistanceGapInRadius=%f"), GDistanceFromCenter, WeaponConfig.Spell.DistanceGapInRadius);
    			GNumPoints += (int32)(GDistanceFromCenter / WeaponConfig.Spell.DistanceGapInRadius) * 4;
    		}
    	}
    }
    // After having spwaned ground actors in this cycle
    // Do we have collisions to sets back
    if (SpawnedGroundActors.Num() > 0)
    {
    	// Start the timer to enable back the collision
    	FTimerHandle TempHandle2;
    	GetWorldTimerManager().SetTimer(TempHandle2, this, &AKSpell::SetBackCollision2GroundActors, DeferredCollisionTime, false);
    }
    // Do we reach the end of the circle
    if (GDistanceFromCenter >= WeaponConfig.Spell.SpellRadius)
    {
    	GetWorldTimerManager().ClearTimer(SpawnGroundActors_Handle);
    	UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT("AKSpell::SpawnGroundActors  -  Reach radius  DistanceFromCenter=%f   WeaponConfig.Spell.SpellRadius=%f   Count=%i"), GDistanceFromCenter, WeaponConfig.Spell.SpellRadius, GroundActors.Num());
    }
    

    }

    void AKSpell::DisableAndDestroy()
    {
    UE_LOG(LogKSGMWeapon, VeryVerbose, TEXT(“AKSpell::DisableAndDestroy - Role=%s”), NETROLETOSTRING(Role));
    UAudioComponent* ProjAudioComp = FindComponentByClass();
    if (ProjAudioComp && ProjAudioComp->IsPlaying())
    {
    ProjAudioComp->FadeOut(0.1f, 0.f);
    }

    MovementComp->StopMovementImmediately();
    
    // give clients some time to show the effect in case no recurring damage
    // in case we had no recurring damages
    if (WeaponConfig.Spell.SpellLife > 0 && WeaponConfig.Spell.SpellRecurringDamageInterval > 0 &&
    	WeaponConfig.Spell.SpellRecurringDamage > 0 && WeaponConfig.Spell.SpellRadius > 0 && WeaponConfig.DamageType)
    	SetLifeSpan( 2.0f );
    
    // We can deactivate and destroy all the created effects
    int32 count = EffectActors.Num();
    for (int32 idx = 0; idx < count; ++idx)
    {
    	AKWeaponFireGroundEffect* EffectActor = EffectActors[idx];
    	if (EffectActor)
    		EffectActor->DeactivateAndDestroy();
    }
    EffectActors.Reset();
    
    // and destroy the ground actors
    count = GroundActors.Num();
    for (int32 idx = 0; idx < count; ++idx)
    {
    	AActor* GActor = GroundActors[idx];
    	UE_LOG(LogKSGMWeapon, Verbose, TEXT("AKSpell::DisableAndDestroy  -  Destroy GActor=%p"), GActor);
    	if (GActor)
    		GActor->Destroy();
    }
    GroundActors.Reset();
    

    }

Was these explanations helpfull for you ?