Fireworks

Header image for Fireworks

A simple pixel graphics firework effect, using CPU or GPU particles depending on if you’re running on mobile or not.

6 minute reading time of 1384 words (inc code) Code available at codeberg.org and the last commit was

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…


Reply via email