Audio and Particles

Module 6 ยท Lesson 2 ยท ~30 min ยท Godot 4.x

Sound is 50% of the perceived quality of a game. Silent games feel like prototypes even when they're finished. Particles are the second most effective polish tool per hour of effort. This lesson covers both, in the "minimum viable polish" dose.

Audio basics

Godot has three audio player node types:

Import sounds: drop .wav or .ogg files into your project's assets/audio/ folder. Godot imports them automatically. Use .ogg for music (compressed) and .wav for short SFX (low latency).

# scripts/sfx.gd โ€” autoload "SFX"
extends Node

var players: Array[AudioStreamPlayer] = []
const POOL_SIZE := 8

func _ready() -> void:
    for i in POOL_SIZE:
        var p := AudioStreamPlayer.new()
        add_child(p)
        players.append(p)

func play(stream: AudioStream, volume_db: float = 0.0, pitch: float = 1.0) -> void:
    for p in players:
        if not p.playing:
            p.stream = stream
            p.volume_db = volume_db
            p.pitch_scale = pitch
            p.play()
            return
    push_warning("SFX pool exhausted")

Pool pattern โ€” avoids creating a node per sound and handles overlapping sounds gracefully.

# Playing a sound
const CARD_TAP := preload("res://audio/card_tap.wav")
SFX.play(CARD_TAP)

# With a random pitch for variety
SFX.play(CARD_TAP, 0.0, randf_range(0.9, 1.1))

Audio buses

Go Audio โ†’ Open Audio Bus Editor (bottom panel). Create buses: Master, Music, SFX, UI. Route your AudioStreamPlayers to the appropriate bus (in their Inspector, set the Bus property). Now players can adjust volume per category in settings.

AudioServer.set_bus_volume_db(AudioServer.get_bus_index("Music"), linear_to_db(0.5))
# 0.5 = -6 dB

Where to get sounds

Music: this is where paying a few dollars is worth it. A single ~2-minute loop from itch.io ($5โ€“15) elevates everything. Or Kevin MacLeod's incompetech.com has CC-BY royalty-free tracks if you need truly free.

Diegetic audio budget

For Lexicon Duel you need:

That's ~12 files. Not much. But each needs to be curated โ€” a single bad SFX drags the whole game down.

Particles in Godot 4

Use GPUParticles2D for game-world effects. CPUParticles2D if you're worried about mobile GPU (generally unnecessary on modern phones).

Basic setup: add a GPUParticles2D node, assign a ParticleProcessMaterial to its Process Material property, assign a texture to the Texture property (or leave null for dots), hit Emit. Tweak:

# Spawn a one-shot burst
const IMPACT := preload("res://scenes/impact_particles.tscn")

func spawn_impact(at: Vector2) -> void:
    var p := IMPACT.instantiate()
    add_child(p)
    p.global_position = at
    p.emitting = true
    # auto-free after its lifetime
    await get_tree().create_timer(1.0).timeout
    p.queue_free()

Effects to build

  1. Card play burst โ€” when a word is submitted, each played card explodes into a burst of glowing particles that fly toward the enemy.
  2. Hit impact โ€” when damage lands, 15-20 particles spray out from the target.
  3. Shield break โ€” when block is consumed, crystal-shard particles fly outward.
  4. Level up โ€” background of concentric expanding rings when the player completes a duel.

Make the first, use it everywhere. Each variation is a duplicate of the base scene with one property tweaked.

Music: volume and fade

A sudden silent-to-loud music cut feels amateur. Always fade in:

func play_music(stream: AudioStream) -> void:
    if $Music.stream == stream and $Music.playing:
        return
    var t := create_tween()
    t.tween_property($Music, "volume_db", -60, 0.4)  # fade out
    t.tween_callback(func():
        $Music.stream = stream
        $Music.play()
    )
    t.tween_property($Music, "volume_db", 0, 0.8)  # fade in

Do this now

  1. Grab 5-8 SFX from Freesound (CC0). Save to assets/audio/.
  2. Set up the SFX autoload. Wire card-tap and word-submit sounds.
  3. Build one impact_particles.tscn. Spawn it on enemy hit.
  4. Duel again. The difference between before and after is your "aha" moment for juice.