Fireworks
A simple pixel graphics firework effect, using CPU or GPU particles depending on if you’re running on mobile or not.
Intro
As part of a “congratulations” screen in a game, I wrote this fireworks effect to use as an overlay. The repository at Codeberg wraps the useful code up in an example project for easier demonstration.
I’ve also used a static version of my random helper library for picking the various random parameters, but that’s easy enough to replace with your own calls.
The Fireworks Scene
There are two scenes in use; the first is this controlling CanvasLayer, used so you can decide where your fireworks sit in your z-levels, and to launch fireworks by instantiating new ones while celebrating.
extends CanvasLayer
signal reset
signal launch
@onready var fuse = $Timer
@onready var rocket = preload("res://fireworks/rocket.tscn")
var celebrating : bool = false
var screen : Vector2
func _ready():
# connect our signals
connect("reset", _on_fireworks_reset)
connect("launch", _on_fireworks_launch)
fuse.connect("timeout", _on_fuse_timeout)
# try to use all the screen for the launch and explosion
screen = get_viewport().get_visible_rect().size
func _on_fireworks_reset():
celebrating = false
fuse.stop()
func _on_fireworks_launch():
celebrating = true
fuse.start(0.5)
func _on_fuse_timeout():
# launch a firework
var _rocket = rocket.instantiate()
_rocket.init(
# pick a starting position just off the bottom screen in the middle
Vector2(screen.x / 2.0, screen.y - 60.0),
# aim somewhere at the top of the screen
Random.get_vector2(Vector2.ZERO, Vector2(screen.x, screen.y / 4.0))
)
add_child(_rocket)
_rocket.set_owner(self)
# and start the next countdown
var _delay = Random.get_float(0.5, 3.5)
fuse.start(_delay)
That’s the controller - send it the relevant signal (eg. $Fireworks.emit_signal("launch")
as done in the demo project) and it will merrily start firing off rockets every few seconds.
The Rocket Scene
The other scene is the firework itself, which is pretty much two sets of a pair of particle emitters; one for the smoke trail as the firework is launched, and one for the bang when the rocket explodes.
The “duplication” of Smoke
/Boom
nodes comes about from picking either GPUParticle2D
nodes for use on desktop machines, or the simpler CPUParticle2D
for use on mobile devices. (So make your own life simpler, and chop one set out if your game doesn’t target both kinds of platforms.)
extends Node2D
var use_gpu : bool = OS.get_name() not in ["Android", "iOS", "Web"]
@onready var particles = $GPU if use_gpu else $CPU
@onready var smoke = particles.get_node("Smoke")
@onready var boom = particles.get_node("Boom")
var target : Vector2
var speed : float
var explode_margin : float
var exploding : bool
var boom_color : Color :
set(value):
boom_color = value
# there's a slightly different syntax between the GPU/CPU particle nodes
if use_gpu:
boom.process_material.color = boom_color
else:
boom.color = boom_color
func init(_position : Vector2, _target : Vector2) -> void:
# starting position of the smoke
position = _position
# position where the firework explodes
target = _target
# vary how fast the firework (smoke) will go
speed = Random.get_float(0.7, 2.0)
# and how close to the target that the firework (smoke) needs to get before exploding
explode_margin = Random.get_float(100, 200)
func _ready():
# pick a random but light color to contrast against our darker background
boom_color = Random.get_color().lightened(0.25)
# and let the length of the explosion vary too
boom.lifetime = Random.get_float(3, 6)
# show the right GPU/CPU particle nodes
particles.visible = true
func _process(delta):
# if we're exploding, we're done here
if exploding:
return
# else our rocket moves to the target as soon as it's been added to the scene tree
position += ((target - position) / speed) * delta
# before exploding if it's close enough to the target
if (target - position).length() < explode_margin:
explode()
func explode():
# stop any extra processing now
exploding = true
# smoke off
smoke.emitting = false
# firework go bang
boom.emitting = true
# wait for the explosion to finish
await boom.finished
# and then tidy ourselves up
queue_free()
We’ve set our rocket up with the bare minimum of two positions (a launch site and a target) just after we instantiated it. We can take that opportunity to fill in a couple of other blanks (how fast it goes, and how far away from the target we’ll be before exploding), or you could pass them along at the same time, depending on how syncronised you want your fireworks to be.
Note that the explode_margin
should be bigger than zero, mainly because the fireworks’ positions will never reach their targets (floating points) and they’ll hang in the sky, but also because it looks aesthetically better as it randomly affects the Smoke
lifetime.
Our rocket will launch as soon as its added to the scene tree, so the last stage of setting up is in _ready()
, where we set the explosion colour and length, and then make sure the right pair of particle nodes is visible.
Finally, when we explode, we wait for the one-shot Boom
particle node to stop emitting its pixels and then remove the node.
Particle Nodes
Those are the controlling scenes and scripts, but the real magic is in the particle nodes themselves and there’s plenty of variables you can use to tweak for your own fireworks.
- For the smoke, use scale and colour ramps to create the look of the trail getting smaller and darker.
- For the bang, give the particles a bit of spin and use less gravity to make them “hang in the air” after the explosion.
Of course, the easiest way is to play with the parameters until you’re either happy with the effect, or you realise you’ve got the rest of the actual game to finish…