Projectile Optimization

Projectile Optimization and FPS Improvement in Unreal Engine

I’ve been struggling with this issue for quite some time — the FPS drop that happens as soon as the number of projectiles increases beyond 100. I’m sure many of you have faced the same issue, so I wanted to share the approach while developing my Turret Plugin Mortar Pro Turret Creator Plugin

There may be more efficient solutions out there, but this is what worked for me. I’d love to hear thoughts or suggestions for improvement .


Problem: Having Separate Actors as Projectiles

Setting up projectiles as actors is often the quickest and easiest approach.
A player or AI spawns a projectile actor with configured speed, collision, and damage. On hit, it spawns Niagara effects and applies damage and this was the same approach I followed initially for my plugin

However, having hundreds of ticking actors quickly becomes a bottleneck — especially when aiming for massive projectile counts. Each actor ticking independently adds up fast.

	AActor* ProjectileObj = nullptr;

	FActorSpawnParameters ActorSpawnParams;
	ActorSpawnParams.Owner = OwnerActor;
	ActorSpawnParams.Instigator = OwnerActor->GetInstigator();
	ActorSpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;


	ProjectileObj = GetWorld()->SpawnActor<AActor>(SpawnClass,SpawnLocation,SpawnRotation, ActorSpawnParams);

Optimization 1: Disable Individual Tick

The first optimization was simple — disable tick on individual projectile actors.
This prevents hundreds of tick calls per frame. I


Optimization 2: Aggregate Projectile Movement Tick

The ProjectileMovementComponent is powerful, but when hundreds of them tick simultaneously it will affect the performance which it did in my plugin.

To fix this:

  • I created an Aggregate Manager (could be a subsystem).
  • All projectile movement updates were processed in a single tick loop inside this manager.
//This will tick all the actor components registered
void AggregateSubSystem::ExecuteTick(ETickingGroup TickGroup, float DeltaTime, ELevelTick TickType, ENamedThreads::Type CurrentThread, const FGraphEventRef& MyCompletionGraphEvent)
{

	for (FActorComponentTickFunction* Func : TickableComponents)
	{
		Func->ExecuteTick(DeltaTime, TickType, CurrentThread, MyCompletionGraphEvent);
	}

	// Cleanup after ticking all components
	//Like for actors that are dead we want to remove the tick
	for (FActorComponentTickFunction* Func : CleanupQueue)
	{
		TickableComponents.Remove(Func);
	}
	CleanupQueue.Empty();
} 

Optimization 3: Use Significance Manager

Epic provides the Significance Manager to prioritize objects relative to others (usually the player).

In my plugin, MortarProPlugin, I used this to dynamically adjust tick rates based on distance from the player.
Far-away projectiles update less frequently


Optimization 4: No Separate Actors (Manager-Based System)

This was the major improvement — about a 16 - 20% FPS improvement.

Instead of spawning individual actors:

  • I created a manager class (can be an actor or subsystem) that stores all projectile data in arrays of structs.
    Example data per projectile:
    • CurrentPosition
    • CurrentVelocity
    • Lifetime
  • The manager loops through and updates all projectiles in its tick.
    Sample snippet
void BulletHellManager::Tick(float DeltaTime)
{

       //Code omitted

	Super::Tick(DeltaTime);

	for (int32& ActiveIndex : ProjectilesIndex)
	{


		//  Get Updated Rotation,Velocity and Position
		FVector OldPosition = ProjectileInstance[ActiveIndex].Position;
		ProjectileInstance.Rotation = GetUpdatedRotation(ProjectileInstance[ActiveIndex], DeltaTime);
		ProjectileInstance.Velocity = GetUpdatedVelocity(ProjectileInstance[ActiveIndex], DeltaTime);
		ProjectileInstance.Position = GetUpdatedPosition(ProjectileInstance[ActiveIndex], DeltaTime);



		FHitResult Hit;
		if (DoLineTrace(Hit, ProjectileInstance[ActiveIndex]))
		{
			ExplosionIndex.Add(ActiveIndex);
		}

	}

	UpdatePositions(ProjectileInstance,ActiveIndex);
	
	//Report Collision to Blueprint
	BPExplosion(ProjectileInstance, ExplosionIndex);


}

Handling Collision

In the manager class now each projectile performs a simple line trace instead of relying on complex per-actor collision.
This keeps the logic lightweight and fast.

FX Handling

Spawning a Niagara effect per projectile or having per-projectile Niagara components is expensive.
Instead:

  • I Used a global Niagara system.
  • and Feed projectile hit data via Niagara Data Channels to trigger effects efficiently.

Static Mesh Rendering

Using multiple static meshes per projectile adds rendering overhead.
Instead of adding static meshes per projectile, which I initially did in my plugin, I used Instanced Static Mesh (ISM) components. I avoided Hierarchical ISM (HISM) due to warnings about instability in dynamic updates (as mentioned by Epic). Instanced Static Mesh Component in Unreal Engine | 虚幻引擎 5.6 文档 | Epic Developer Community


Bonus Tips

Further improvements can be explored, like Async Line Traces and Parallel For Loops for projectile updates.

For now, I kept it simple as I found a post related to thread safety while doing line trace in a thread.

Line Trace

Would like to thank Unreal Engine forums and Discord channel for helping me to get to this solution.

1 Like