When using particles in Godot, it is a common problem to unintentionally introduce lag spikes in your game due to how shaders are compiled in the engine. In this article, I'm going to cover the solution that I have come up with to work around this issue in a programmatic and isolated way.
First, let's wrap our heads around what is causing this problem. As mentioned above, the simple answer is: shader compilation. But, to better explain what is going on, let's break it down a bit further.
When you have an object in the game that uses a Particles2D node - the first time it is instanced in the scene and a particle is emitted - the material for that Particles2D node has to be compiled. Once it has been compiled, any subsequent instances of those particles will not have to be recompiled and will not cause any additional lag spikes.
The technical reason behind this is beyond my understanding at the moment, but from what I've gathered is: shader caching doesn't exist in OpenGL, so we need to make a cache ourselves. So, now that we understand the problem, let's take a stab at solving it.
Note: It has been mentioned that Vulkan has proper shader caching and will likely resolve this problem entirely. Vulkan is scheduled for the 3.2 release of Godot, so hopefully this solution won't be required for much longer, but until then this will be a simple and easy way to provide more control of when this compilation occurs.
Save Particle Materials To Files
Before we begin building the actual cache scene, we need to save all
ParticlesMaterial instances to files. You are likely creating these in the Particles2D node, but by default they are stored within the scene itself. So, we just need to save these to their own files. You can do so by selecting the
ParticlesMaterial in the Inspector to view the resource details inside of the UI and then selecting the "Save As..." option to save it to the file system.
For the purposes of this article, we're going to save two material files
SecondParticles.material. We will be saving these files to a new
res://Materials/ directory, but you can save them wherever you would like.
Create A Cache Scene
Once we have our material files created, we can begin creating our scene that will act as our cache. Let's create a new scene and name it
ParticlesCache and inside of it let's create a single
Next, we'll need to add a script to the scene so we can programmatically load in our materials.
extends CanvasLayer var FirstParticlesMaterial = preload("res://Materials/FirstParticles.material") var SecondParticlesMaterial = preload("res://Materials/SecondParticles.material") var materials = [ FirstParticlesMaterial, SecondParticlesMaterial, ] func _ready(): for material in materials: var particles_instance = Particles2D.new() particles_instance.set_process_material(material) particles_instance.set_one_shot(true) particles_instance.set_emitting(true) self.add_child(particles_instance)
This script preloads our material files and then adds them to a local
materials array. Once they are in the array, we can loop over them and then programmatically create new
Particles2D nodes and add them to our scene.
For each of these
Particles2D nodes, we are going to assign the process material, ensure the particles are only fired once, emit the particles, and then add it to our
ParticlesCache scene as children to our root
CanvasLayer node. Depending on the size of your particles, you may also want to set their position to ensure that they are displayed outside of the viewport.
AutoLoad The Scene
The final step is to add these newly created scene to our list of autoload scenes. Open up your project settings, select the "AutoLoad" tab, and then open the
ParticlesCache.tscn file and click the "Add" button.
Now this scene will be added when the game is started and will persist throughout the lifetime of the execution. This is what keeps the particle material files compiled and loaded in memory to prevent further compilation.
I hope you have found this quick tip helpful. Depending on the device - these lag spikes caused by shader compilation can be extremely obtrusive. For example, my 2011 MacBook Pro will completely hang for 3 seconds when a particle is first emitted while my 2014 MacBook Pro only experiences a small stutter.
Since there is no safe assumption on what devices our games will be running on - we should always aim for the least common denominator and treat that as our target. If we can solve this solution for them, then we've solved it for everyone.
If you have found ways to improve upon this, please share them in the comments below.