The bullets themselves are also objects in the game world. How did you optimize Luna Abyss to retain its high-speed combat while hundreds of bullets are onscreen?Reynolds:

To ensure our bullets still performed well with so many around, we had to target two main areas. The first thing we needed to approach was the bullets themselves. With so many spawning and moving at once, the CPU takes a significant load. We further broke this down into two parts: ensuring we could spawn many tens or even hundreds of bullets in a frame without any noticeable frame drop, and ensuring we could move hundreds of bullets every frame. 

We’d noticed that attacks spawning many bullets at once would cause a frame drop, since spawning actors is quite an expensive process. When we’re asking for many of these at the same time, it’s a lot for the CPU to handle. We therefore opted to make pools of bullets, so we’d create all the bullets needed for combat at the beginning of the game, and simply teleport them in and activate them whenever an attack needed them. Once they collide with anything, we can just turn them off and return them to the pool, ready to be teleported in again when needed. 

We ensured we had performant array handling to decide which bullets to grab, and then we had efficient spawning. This had an overwhelming benefit for performance, as we no longer needed to create or destroy bullets, they were always around ready for use whenever we needed them. This method meant we needed to ensure their memory cost was as small as possible though, as they now always existed, so we kept a close eye on this and kept the bullets as simple as possible.

With the spawning addressed, we focused on movement. This is also a big load on the CPU, as the bullets need to check for collisions every frame. We therefore kept the bullet movement as simple as possible. When more complex behavior is required, we turn it off on the bullets as soon as those behaviors are no longer affecting the flight. We also ensured that all bullet logic was performed natively in C++ as this gives some performance gain over Blueprints script.

Now that our bullets were as efficient as we could make them, we needed to minimize CPU usage elsewhere so that these bullets wouldn’t cause frame drops in levels with many moving parts. For this, we used Unreal’s Profiling Tools to monitor the CPU times both in and out of combat: stepping through the levels to see what’s taking up time and reducing this time as much as possible without affecting gameplay elsewhere. We would do this out of combat first to reduce the background CPU usage, then focus on combat specifically to ensure that the combat systems were not struggling due to anything specific to that level, such as larger spaces causing bullets to survive longer, meaning we had more of them moving than intended. We could then address these issues on a case-by-case basis, usually modifying the combat systems to ensure they were efficient regardless of how our environments were designed.

Did the team use Niagara to generate its bullets? 

Reynolds:  We initially used Niagara particles for our bullets, but after some iteration, we eventually moved to static meshes. With so many bullets on screen at once, their visuals were kept simple and so they were not using much of the power that Niagara offers. Both as a result of this, and the fact that some aspects of Niagara’s particles are calculated on the CPU which we wanted to free up to handle our bullet behavior, we decided that meshes would give us better performance without a drop in visual quality.

Corr: In the early days of the project, we were exclusively using Cascade to handle all of our particles as no one on the team was familiar with Niagara. As the project progressed ,we started exploring Niagara and quickly learned how much more powerful and flexible it is over Cascade and we now exclusively use Niagara for all of our particles. There are way too many benefits of using Niagara to go over all of them, but the ease in which systems can be modified via Blueprints is easily one of my favorite things about it.

Source: Unreal Engine Blog