*You can find the links to the previous parts at the bottom of this tutorial.
We see particle effects everywhere in video games. Particle effects are used to visually enhance games. You can use them to create blood splatter, running trails, weather, or any other effect. For 2D games in Godot, you can add particle effects in two ways: via the GPUParticles2D node and the CPUParticles2D node.
WHAT YOU WILL LEARN IN THIS PART:
- How to work with the GPUParticles2D node.
- How to change properties on particles.
The CPUParticles2D is CPU based, so all the particle effects are processed by your computer’s CPU. You use this if you do not have a GPU that supports hardware acceleration. The GPUParticles2D node is GPU based, so it uses hardware acceleration to process particle effects. When choosing between GPUParticles2D and CPUParticles2D, it’s important to consider the requirements and constraints of your project. If performance is critical and you have lots of particles, GPUParticles2D might be the better choice. However, if compatibility, access to particle data, or specific features are more important, then CPUParticles2D might be the way to go.
Since I have an okay graphics card, I will be using the GPUParticles2D node throughout this tutorial, but if you have an older device you can still use the CPUParticles2D node because it has the same functionalities as the GPUParticles2D node.
STEP 1: PLAYER RUNNING PARTICLES
In your project, underneath your Player scene, add a new Node2D node which will contain your Particle Effects. Rename it to “Effects”.
Add a GPUParticles2D node to your Effects layer, and rename it to “RunningParticles”. We will use this node to create a smoke effect from our player’s feet when they are running.
If you select this node and look in the Inspector panel, you will be met with some foreign properties! Please read the Godot documentation which gives you a great overview of all of these properties with examples!
Here’s a general overview of the types of properties you could encounter:
- Amount: The total number of particles that should be emitted.
- Emitting: Where we enable/disable the particle’s emission state.
- Process Material: A material that determines how particles will behave. You can use the ParticlesMaterial or a ShaderMaterial for more customized behavior.
- — Use ParticlesMaterial if you need a straightforward solution for common particle effects, and you want to be able to configure them easily without writing code.
- — Use ShaderMaterial if you need advanced customization or unique effects, or if you have performance considerations that require fine-tuned control over the particle system.
- Direction: The direction in which the particles will be emitted.
- Initial Velocity: The speed at which particles are emitted.
- Angular Velocity: The rotational speed of the particles.
- Spread: The angle in degrees that the particles will be spread on emission.
- Scale: A multiplier for the scale of the particle.
- Damping: How much the particles slow down over time.
- Gravity: The effect of gravity on particles.
- Texture: The texture used for each particle.
- Explosiveness: Determines how the particles are emitted over time. A value of 1 means all particles are emitted at the start, and a value of 0 means particles are emitted evenly over the lifetime.
- Lifetime: How long, in seconds, each particle will last.
- Preprocess: How much time the system should simulate before the particles are seen for the first time.
- Visibility Rect (Visibility AABB): Specifies the rectangular area in which the particles are visible. It’s used for optimization purposes so that off-screen particles are not unnecessarily processed.
- Randomness: The randomness factor applied to various particle properties like velocity, direction, etc.
- One Shot: If enabled, the particles will emit once and not loop.
- Speed Scale: How fast the particle system simulation runs. You can use this for slow-motion or fast-forward effects.
This seems like a lot, but don’t worry — by the end of this tutorial, it might make a bit more sense. In your Inspector Panel, we need to assign our node with a new Process Material. Let’s click on the resource next to our Process Material property and add a new “New ParticleProcessMaterial” resource. We’re using this resource because we don’t need to create custom shaders for our particle effects.
Click on your newly created resource, and you will see this menu arise:
We need to give our material a new texture. This is the actual texture that will show up when our particle is emitted. You can think of our ParticleProcessMaterial as the material that will define what the particle looks like (color, reflectivity, etc.). The texture assigned to our material is just image files that are used within the material.
Figure 14: Example of a texture and a material.
Let’s assign a new Texture to our Particle Effects resource. Click on the Texture resource and select the option “Quick Load”. You can also just drag the image that you want into the texture box.
We will assign it with a particle image that we have in our Assets folder. You’ll see that there are a few to choose from! We want to assign the texture “Assets/Particles/9/1_1.png”.
This will create a white smoke image. If you look at your Player in your Player scene, you will see that the smoke is emitting downwards on their body.
To fix this, we need to change our particle’s Gravity property. Gravity affects which direction the particle pulls. We want it to pull to our left. In your Inspector Panel, underneath Gravity, change your values to (x: -10, y: 0, z: 0). We’ll change this to dynamically changing in the code in a minute.
Also move your Effects node to be above your AnimatedSprite2D node, because we don’t want our effects to play in front of the player’s feet. You also want to drag the particle effect down to their feet — and not in the middle of their body.
Our particle effect is a bit too bright. I want it to look duller so that it seems like the dust is moving up from the floor when our player runs. Underneath Visibility > Modulate, change your color value to #ffffff64. This should dull down our particle’s material.
We also want to play a bit more steadily, so underneath Time > Speed Scale, increase the value to an amount between 3 and 5. This property changes how fast the particle system simulation runs. You can use this for slow-motion or fast-forward effects. This will make it x times faster. A value of 1.0 means the simulation runs at normal speed. Values greater than 1.0 make the simulation run faster, while values between 0 and 1 make it run slower.
By default, we don’t want the RunningParticles to play if our player isn’t moving. In the inspector panel, disable the Emitting property. We will enable this in the code itself.
### Player.gd
#older code
#animations
func player_animations():
#on left and we aren't hurt
if Input.is_action_pressed("ui_left") && Global.is_jumping == false:
$AnimatedSprite2D.flip_h = true
$AnimatedSprite2D.play("run")
$CollisionShape2D.position.x = 7
$ScoreRayCast.position.x = 5
$Effects/RunningParticles.emitting = true
#on right and we aren't hurt
if Input.is_action_pressed("ui_right") && Global.is_jumping == false:
$AnimatedSprite2D.flip_h = false
$AnimatedSprite2D.play("run")
$CollisionShape2D.position.x = -7
$ScoreRayCast.position.x = -5
$Effects/RunningParticles.emitting = true
#on idle if nothing is being pressed and we aren't hurt
if !Input.is_anything_pressed():
$AnimatedSprite2D.play("idle")
We also want our gravity’s direction to change dynamically depending on which direction our player is running in. We can change this by assigning a Vector3(x,y,z) object to our process_material.gravity property in the code.
This will change our gravity’s value on the x, y, and z value. We won’t have to use the z value since our game is a 2D game — and hence it doesn’t render the third (width) value on our particles.
Figure 15: Vecor3() Object Visualized
Previously we made our gravity a static value, but now we’ll make it dynamic. We’ll also need to change its position so that it can move behind our player’s body whenever the player changes direction.
### Player.gd
#older code
#animations
func player_animations():
#on left and we aren't hurt
if Input.is_action_pressed("ui_left") && Global.is_jumping == false:
$AnimatedSprite2D.flip_h = true
$AnimatedSprite2D.play("run")
$CollisionShape2D.position.x = 7
$ScoreRayCast.position.x = 5
$Effects/RunningParticles.emitting = true
$Effects/RunningParticles.process_material.gravity = Vector3(10,0,0)
$Effects/RunningParticles.position.x = 5
#on right and we aren't hurt
if Input.is_action_pressed("ui_right") && Global.is_jumping == false:
$AnimatedSprite2D.flip_h = false
$AnimatedSprite2D.play("run")
$CollisionShape2D.position.x = -7
$ScoreRayCast.position.x = -5
$Effects/RunningParticles.emitting = true
$Effects/RunningParticles.process_material.gravity = Vector3(-10,0,0)
$Effects/RunningParticles.position.x = -5
#on idle if nothing is being pressed and we aren't hurt
if !Input.is_anything_pressed():
$AnimatedSprite2D.play("idle")
$Effects/RunningParticles.emitting = false
You can also change the y value in your Vector3() to give the effect some pull at the end.
### Player.gd
#older code
#animations
func player_animations():
#on left and we aren't hurt
if Input.is_action_pressed("ui_left") && Global.is_jumping == false:
$AnimatedSprite2D.flip_h = true
$AnimatedSprite2D.play("run")
$CollisionShape2D.position.x = 7
$ScoreRayCast.position.x = 5
$Effects/RunningParticles.emitting = true
$Effects/RunningParticles.process_material.gravity = Vector3(10,-2,0)
$Effects/RunningParticles.position.x = 5
#on right and we aren't hurt
if Input.is_action_pressed("ui_right") && Global.is_jumping == false:
$AnimatedSprite2D.flip_h = false
$AnimatedSprite2D.play("run")
$CollisionShape2D.position.x = -7
$ScoreRayCast.position.x = -5
$Effects/RunningParticles.emitting = true
$Effects/RunningParticles.process_material.gravity = Vector3(-10,-2,0)
$Effects/RunningParticles.position.x = -5
We can then also emit this trail if our player jumps.
### Player.gd
#older code
#singular input captures
func _input(event):
#older code
#on jump
if event.is_action_pressed("ui_jump") and is_on_floor():
velocity.y = jump_height
$AnimatedSprite2D.play("jump")
#sfx
$Music/JumpSFX.play()
$Effects/RunningParticles.emitting = true
Your code should look like this.
Now if you run your scene, your trail particles should play if your player runs to the left or the right, as well as when they jump! You can play around with the values in your code to see what works for you. You can make the effect slower, and shorter as well.
If you want it to look like your player’s individual footsteps are making the trail instead of it emitting in one individual line, you can change the Emission Shape and Lifetime Randomness of your resource.
The shape is the shape in which the particles are emitted. By default it is set to point, so it will be emitted in a straight line. Let’s change our shape to be a sphere with a “5” point radius. This will emit our particles in a small circular shape.
Now, let’s also add some randomness to our emissions. The randomness factor is applied to various particle properties like velocity, direction, etc. We’ll make it a small value such as 0.1.
Now if you run your scene, your particle will be emitted in a more “random” manner — and it should almost look like footsteps making the dust for the trail effect.
STEP 2: BOX PARTICLES
We want our box to also have a trail when it is thrown. In the Box scene, add a new Effects layer with a GPUParticle2D node named “ThrowingParticle” added to it.
Assign a new ParticleProcessMaterial resource to it.
In the textures property, assign the “res://Assets/Particles/9/1_9.png” image to it.
Then, change its Modulate value to #ffffff1e.
Change its Speed Scale to “5”.
We want it to emit in a Box shape with (x: 5, y: 4) as its extents. This is the size that we want it to emit in.
Then, change its Gravity to be 0 on all x, y, and z values. We don’t want it to pull in any particular direction since our spawners are on both sides of our map.
Then, we also want to randomize our emissions scale. This is how large and small the emission can get. Change it to min (1), max (1.5).
Move the particle to be at the start of your box.
Now if you run your scene, your boxes should also have a trail!
STEP 3: BOMB PARTICLES
We want our bomb to also have a trail when it is spawned. In the Bombscene, add a new Effects layer with a GPUParticle2D node named “FireParticle” added to it.
Assign a new ParticleProcessMaterial resource to it.
In the texture’s property, assign the “res://Assets/Particles/7/1_14.png” image to it.
Change its Amount to “3”.
We want it to emit in a Sphere shape with a point 10 radius.
Then, change its Gravity to 0 on all x, y, and z values. We don’t want it to pull in any particular direction since our spawners are on both sides of our map.
Then, we want to change our explosiveness and speed scale. If the lifetime is 1 and there are 10 particles, it means a particle will be emitted every 0.1 seconds. The explosiveness parameter changes this, and forces particles to be emitted altogether. Change the speed to “10” and the explosiveness to “1”.
Move the particle to be at the top of your bomb.
Now if you run your scene, your bomb should also have a trail!
You can add many other particle effects, such as a weapon effect particle when your player attacks. Itch.io is a great place to get particle effects that will help you create these effects, but for our game, we’ll keep it simple with our mediocre effects! You can also find a bunch of particle effects for free with Kenney’s Particle Pack which you can download directly into your Godot project.
Remember that working with particles can be an iterative process, so experimenting and fine-tuning is key to achieving the desired effect. Once again I do recommend that you go and give the documentation a read because it really shows you how to use all of the properties that are available — and it even comes with clips!
If you’re too “lazy” to make your own effects, you can search for “Particle Effects” in your Assetlib workspace.
From here, you can download the BurstParticles2D Asset Pack into your project.
From there you can copy and paste the particle effects from the “…/MultiParticleExample1.tscn” scene.
You also need to enable the plugin (Project Settings > Plugins), which will enable you to create your own particle effects using the custom BurstParticleGroup2D node to layer multiple BurstParticles for more complicated effects.
Congratulations on adding your particle effects to your game! We now have a complete game. In the next part we’ll export our project, and we’ll conclude our project. Now would be a good time to save your project and make a backup of your project so that you can revert to this part if any game-breaking errors occur. Go back and revise what you’ve learned before you continue with the series, and once you’re ready, I’ll see you in the next part!
Next Part to the Tutorial Series
The tutorial series has 24 chapters. I’ll be posting all of the chapters in sectional daily parts over the next couple of weeks. You can find the updated list of the tutorial links for all 24 parts of this series on my GitBook. If you don’t see a link added to a part yet, then that means that it hasn’t been posted yet. Also, if there are any future updates to the series, my GitBook would be the place where you can keep up-to-date with everything!
Support the Series & Gain Early Access!
If you like this series and would like to support me, you could donate any amount to my KoFi shop or you could purchase the offline PDF that has the entire series in one on-the-go booklet!
The booklet gives you lifelong access to the full, offline version of the “Learn Godot 4 by Making a 2D Platformer” PDF booklet. This is a 451-page document that contains all the tutorials of this series in a sequenced format, plus you get dedicated help from me if you ever get stuck or need advice. This means you don’t have to wait for me to release the next part of the tutorial series on Dev.to or Medium. You can just move on and continue the tutorial at your own pace — anytime and anywhere!
This book will be updated continuously to fix newly discovered bugs, or to fix compatibility issues with newer versions of Godot 4.