Adding Sound Effects

Introduction: Giving the Game a Voice

Our dodge game already has reusable obstacle spawning, five falling obstacles, collision detection, and a live score in the corner of the window. Everything it tells the player, however, it tells with pixels alone.

Sound changes that instantly. A short ding when a point is scored and a blunt thud when the player is hit make the same events feel far more satisfying, and they reach the player even when their eyes are busy watching the falling squares. In this final lesson we add exactly those two effects.

The pattern we will use is short and reusable: initialize the mixer, load each sound once during setup, and call .play() at the exact place in the code where the matching event happens.

How Pygame Handles Audio

All audio in Pygame is handled by the pygame.mixer module. Before any sound can be loaded or played, the mixer must open an audio device. pygame.init() tries to do this for us along with everything else, but that attempt fails silently if the audio system is not ready yet — which can happen in a browser. Calling pygame.mixer.init() explicitly right after pygame.init() makes the setup deliberate and keeps the rest of the program predictable:

Python
    pygame.init()
    pygame.mixer.init()

For short effects like ours, the right tool is pygame.mixer.Sound. It loads an entire audio file into memory so that playback can start instantly, with no disk access at the moment of the event. (Long background music is handled differently, by pygame.mixer.music, which streams the file instead of loading all of it.)

The supplied assets use browser-compatible WAV encoding. PCM WAV is commonly used for short effects because it is simple and broadly supported, although WAV itself is a container format and is not inherently uncompressed.

One browser-specific quirk is worth knowing: most browsers block audio until the user has interacted with the page. If you hear nothing at first, click inside the game window or press a key, and the sounds will start coming through.

Loading Sound Files Before the Loop

Both effects are loaded once during setup, next to the other one-time preparations:

Python
    font = pygame.font.SysFont(None, 36)

    point_sound = pygame.mixer.Sound("assets/point.wav")
    hit_sound = pygame.mixer.Sound("assets/hit.wav")

The paths are relative to main.py, so the assets folder must stay beside the program; if the file cannot be found, Pygame raises an error and the game never starts.

Loading before the loop matters for the same reason it mattered for the font object. Reading and decoding an audio file is slow compared to a single frame, and doing it sixty times per second would stutter the game for no benefit. Each Sound object can be replayed as many times as we like.

Playing a Sound

Once a sound is loaded, playing it is a single method call:

Python
    point_sound.play()

play() hands the audio to the mixer and returns immediately, without waiting for the sound to finish. That non-blocking behavior is essential in a game loop: the frame carries on drawing and updating while the effect plays in the background. Calling play() again before the previous playback ends simply layers a second copy on top, which is exactly what we want when two obstacles are dodged at nearly the same moment.

Rewarding a Successful Dodge

The point sound belongs wherever a point is awarded — right beside the score += 1 line inside the recycling block:

Python
        for obstacle in obstacles:
            obstacle.y += obstacle_speed
            if obstacle.y > HEIGHT:
                obstacle.x = random.randint(
                    0, WIDTH - OBSTACLE_SIZE
                )
                obstacle.y = random.randint(-100, 0)
                score += 1
                point_sound.play()

Because both lines share the same if block, the rule stays perfectly simple: one dodged obstacle, one point, one ding. Moving point_sound.play() out of the if would fire the effect every frame for every obstacle, producing a continuous buzz instead of a reward.

Signaling a Collision Once

The hit sound goes into the collision loop, next to the code that ends the run:

Python
        for obstacle in obstacles:
            if player.colliderect(obstacle):
                hit_sound.play()
                running = False
                break

The break is the interesting addition. Without it, the loop would keep checking the remaining obstacles even after the game has been declared over. If the player happened to be overlapping two obstacles in the same frame — quite possible when squares land side by side — the thud would play twice, one on top of the other, and sound wrong.

break exits the collision loop immediately after the first detected hit, guaranteeing exactly one sound per crash. Note that it leaves only the for loop, not the while loop: the game still finishes drawing the current frame, and only then does while running see the False flag and stop.

The Complete Program

Python
import asyncio
import random
import pygame

WIDTH, HEIGHT = 800, 600
FPS = 60
OBSTACLE_SIZE = 40
DARK_BLUE = (20, 28, 48)
GREEN = (87, 215, 170)
RED = (220, 50, 50)
WHITE = (255, 255, 255)

def spawn_obstacles(count):
    new_obstacles = []
    for _ in range(count):
        x = random.randint(0, WIDTH - OBSTACLE_SIZE)
        y = random.randint(-HEIGHT, 0)
        new_obstacles.append(
            pygame.Rect(x, y, OBSTACLE_SIZE, OBSTACLE_SIZE)
        )
    return new_obstacles

async def main():
    pygame.init()
    pygame.mixer.init()
    screen = pygame.display.set_mode((WIDTH, HEIGHT))
    pygame.display.set_caption("Dodge Game")
    clock = pygame.time.Clock()
    font = pygame.font.SysFont(None, 36)

    point_sound = pygame.mixer.Sound("assets/point.wav")
    hit_sound = pygame.mixer.Sound("assets/hit.wav")

    player = pygame.Rect(375, 540, 50, 40)
    player_speed = 5

    obstacles = spawn_obstacles(5)
    obstacle_speed = 4
    score = 0

    running = True
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False

        keys = pygame.key.get_pressed()
        if keys[pygame.K_LEFT]:
            player.x -= player_speed
        if keys[pygame.K_RIGHT]:
            player.x += player_speed
        if keys[pygame.K_UP]:
            player.y -= player_speed
        if keys[pygame.K_DOWN]:
            player.y += player_speed
        player.clamp_ip(screen.get_rect())

        for obstacle in obstacles:
            obstacle.y += obstacle_speed
            if obstacle.y > HEIGHT:
                obstacle.x = random.randint(
                    0, WIDTH - OBSTACLE_SIZE
                )
                obstacle.y = random.randint(-100, 0)
                score += 1
                point_sound.play()

        for obstacle in obstacles:
            if player.colliderect(obstacle):
                hit_sound.play()
                running = False
                break

        screen.fill(DARK_BLUE)
        pygame.draw.rect(screen, GREEN, player)
        for obstacle in obstacles:
            pygame.draw.rect(screen, RED, obstacle)
        score_text = font.render("Score: " + str(score), True, WHITE)
        screen.blit(score_text, (10, 10))
        pygame.display.flip()
        clock.tick(FPS)
        await asyncio.sleep(0)

    pygame.quit()

asyncio.run(main())

Conclusion and Next Steps

The audio pattern is pleasingly simple: initialize the mixer, load each sound once before the loop, and call .play() where its game event occurs. The point sound shares the trigger with the score increment, while the hit sound plays exactly once thanks to the break that stops collision processing.

Our finished dodge game now brings everything together: a helper function that creates obstacles, a list and loops that move and recycle them, collision detection that ends the run, a rendered score that rewards survival, and sound effects that respond to both success and failure. From here you could add a game-over screen, gradually increase obstacle_speed as the score climbs, or spawn more obstacles over time — every one of those ideas builds on the same patterns you have practiced in this course.

Sign up

Join the 1M+ learners on CodeSignal

Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal