The Fancy New Particles - GGG Dev Log #5
Strap in folks, this one's a doozy. Today I'd like to talk about a feature I was stubborn about adding despite it perhaps maybe not being the most efficient use of our time; the cool new visual effects we worked on that allows broken obstacle pieces to be properly carried away by the conveyors rather than just sit there in an immersion-breaking way.
I'm actually also going to be putting this in the Dev Sandbox category of the blog, because I feel like getting quite technical. I also don't recommend reading this post if seeing back-end game development clips might take you out of the immersion/experience? Might also just be boring.
Don't take this as best-practice advice. It's just my experiences solving some weird problems.
To properly get started, this is the stuff I'm talking about:
The way objects break into many little pieces that then get carried down the conveyor. For context, in the original demo objects just poofed into smoke and only a couple large pieces (mostly from the player dying) would bounce around the arena; but those pieces would just land on conveyors and sit there unmoving in a way that makes you go "yep, it's a videogame".
I imagine people with experience trying to optimize advanced particle effects are already seeing this and going "oh no", while those without much experience with game engines might be thinking "what's the big deal?", and highly experienced developers are probably also going "what's the big deal?". I definitely don't fall into the category of "highly experienced" so...
The big deal, as the "oh no" crowd might have guessed, is that 90% of this ordeal was all about figuring out how to optimize something like this to not destroy performance. I'm convinced just about anyone could figure out how to make this work, but making it work efficiently is a whole other story. This may not be a big open world game with complex systems, but it still has important optimization challenges when you need hundreds of dynamic things to be on screen at the same time without impacting performance for a wide audience of players.
Getting started let's first look at the point where particles like this are born. The process for creating the cracked pieces is the "lazy" way that doesn't actually dynamically slice the object. Doing that would have been an unnecessary level of wasted processing time, and curating the chunky pieces allows for slightly more interesting visuals on the inside and in what ways they are split up. I make these in Blender like all the other models.
This would be a good time to note the game engine being used is Unity, for some this may add to that "oh no" factor of the challenge ahead, but otherwise it is good context to know that the shenanigans involved are quite unique to the engine's tools and working with its limitations.
The next step is another simple one, import the model and start setting these up as the mesh visuals for a standard Unity particle system customized to your liking. Just gotta use an in-game model as reference to get all the positions right, it helps to set the particle's simulation speed down low so you can pause it the moment the particle spawns to check if the positions and rotations are correct.
This is actually where our first hurdle happens. When obstacles die and spawn the death object that these particles appear from, sometimes the object is rotated from physics interactions and... at least in Unity 2019 there's no option to make particles start with rotation matching their parent object. I solved this issue with a very simple script that mirrors the parent's rotation values to the particle system's 3D Start Rotation during the particle object's On Enable.
(Bonus note: Don't forget to pool objects like this that are being repeatedly reused.)
Once all that is done you have particles that make you happy when viewing frame-by-frame.
That then fall through the floor... okay okay, so I want the particles to collide satisfyingly with the surrounding area, just gotta set up some box colliders that only collide with particles and nothing else and set up the physics module, that's simple. But it only gets us to where the true challenge starts: actually moving these chonks along this conveyor.
In a previous dev log I mentioned the whole detail about videogame conveyors not being like real life ones, they aren't physically moving objects that truly pull items around using friction and gravity. Again in our case they are just moving textures with a collider that tells objects to move at that speed in a fancy way.
In reality, despite our system being efficient for objects, for something with the complex optimization-focused underbelly of a particle system simply feeding the particles the same speed data on collision has a surprising overhead that would have probably taken a lot of unnecessary dev time to get right for a pointless visual feature.
This is where our first shenanigan comes in, at this point I was already using Unity's Force Field system to push particles around for explosions and such so why not just add a few around the conveyors to apply the movement speed to the particles considering the particles are already doing the extra calculations related to their External Forces module anyway?
It works, but it took some thought to get just right as Force Fields apply their momentum in somewhat strange ways. For example, the amount of force to apply ended up needing to be divided by the length of the box applying the force on these conveyors.
Either way, lo and behold with some wacky math to translate conveyor speed to whatever odd logic Force Fields use it does indeed move the particles on the conveyors!
Except this requires turning off any collision options that apply friction to particles or else they stop on the conveyors, so now they're sliding everywhere all over the place in weird ways.
This is solved by adding a few more Force Fields that instead apply their Drag value to particles.
Okay now everything is working as intended, breaking apart, colliding, moving with conveyors, sliding to a stop outside the arena, and when the conveyor carries them away they... they...
MY FRAMERATES, THEY'RE GONE, OH NO.
Yeah, as you can imagine having long lasting complex colliding particles that are full model mesh pieces falling into the void secretly off screen isn't doing the game any favors. This ended up being the real real challenge of making this system work, having my cake and eating it too as I wanted something that looked cool without impacting performance on low end systems.
Setting up a stress test with none of the optimizations in place we get a nice chunky 7fps as everything flies around. Let's get started optimizing this.
First thing's first, obviously we need to purge any particles that aren't in view. Seems simple enough, but it turns out its hard to find methods to track that kind of thing without adding overhead that either cuts into your FPS gains or straight up makes performance worse. This is probably just a skill issue, a custom programmed modification of the particle system could probably more efficiently solve the problem of culling these old particles but from all my findings I need to improve my skills before I can do that within decent amounts of dev time.
Even worse, certain built in options are horrifyingly inefficient. Notably the Triggers module is a total mistake to try and use, setting that up in our stress test conditions nukes the framerates down to just 2fps. The overhead ruins any potential gains and makes it worse instead.
So what did I do if nothing was helping? I threw the particles against a wall really really hard.
I'm not even joking. That is a blunt description of what I'm doing to them, I throw them against a wall and they die to death really efficiently. Shenanigans.
In more detail, I'm doing that thing again where I make use of features that are already enabled. By default the Collision module has a Max Kill Speed function that deletes particles moving too fast when they hit colliders, so with no new overhead from that and External Forces already enabled I just used some Force Fields to throw them into some walls.
They say it isn't stupid if it works, but I think I'm also okay calling the yeet box stupid.
And what does culling the particles get us up to? Up from 7fps to 10fps! ...oh.
Well okay, it's a ~40% efficiency increase to our extreme-case stress test, that's still good. I could probably squeeze some more efficiency out of it by making the range of the yeet box smaller too.
Still, any frame drop is unacceptable, performance comes before graphics. Obviously I can add quality settings so players can lower the number of particles, and I did add a quality slider that lowers the max percentage of them that spawns, but it kinda stinks that that lowered max also applies to low-intensity moments when the system has enough juice to handle it all. If only it could be more dynamic.
... ... ...
So I added a system that dynamically changes the particle quality based on performance!
Using a customizable Minimum Target Framerate, the game can lower things such as how many of these fancy particles spawn as the framerates dip to avoid further framerate dipping.
It does a great job maintaining framerates when the system can't handle it, in our stress test setting that Minimum to 80% the game runs at an average of 48fps (Oh hey, 80% of 60fps.)
Obviously for now it means it basically turns off the fancy new particles when the game can't handle them all, so it'll look more plain in those instances.
But it is well worth it to save the game from major frame dips. Also it is worth noting all these stress tests were done in the Unity Editor, in full game builds performance is far better as you can see from the game trailers where there's plenty of these broken pieces flying around despite the stress of 60fps HD recording on my outdated PC.
The best part about dynamic quality systems like this is that when someone calls me out for being dumb and shows me a better culling method than throwing the particles at a wall, the visual fidelity increases for everyone. It's very scalable and I plan to improve it and want to add other features like Dynamic Resolution for instance. An important step is figuring out the more complex functions Unity has for tracking GPU and CPU times separately, for accuracy.
There are definitely a few other tricks I'd like to try, like lowering the lifetime of particles to return framerates back to normal faster during spikes or more methods of reducing particle quality in general. Also might be possible to lower quality of other assets?
I wont be surprised if I find more optimization tricks later, for instance even within adjusting how many particles spawn I found that you get a small efficiency boost disabling a particle emitter entirely even if it is spawning 0 particles, just having the particle system enabled has overhead.
This post got insanely wordy, holy moly. If you actually read the entire thing, I salute you, and I hope I didn't forget any important details in the story as I really should have wrote this months ago when it was still fresh in my memories from when I first went through this insanity.
Here's one last clip of it with that 80% setting. I suppose another incentive to improving this system would be getting good results WHILE recording, you have to trust me it looks better when just playing without recording at the same time.