Mechanic - Audio

In the Game Design lesson, we identified adding audio feedback would enhance the interactivity of our game. Below are the identified events that we could add sound effects to:

Event

Sound Effect

Shooting laser

Positive

Shooting asteroid

Positive

Saving astronaut

Positive

Ship collides with asteroid

Negative

Shooting astronaut

Negative

It is also common for computer games to have a backing track being played throughout the entire game.

In this lesson we will implement all the sound effects and add a backing track.

Planning

Mechanisms

First we will need to work out what mechanisms are available for playing sounds. As always, our first place to look is the GameFrame Documentation. Once your on the documentation page, perform a search for sound (ctrl + F or command + F) to find all the sound related GameFrame features.

You will find:

  • Sounds folder → holds all the sound files. Check your sounds folder you will notice that you have been provided with all the required sound effect, along with some extra ones.

  • load_sounds() which is a Rooms Method → we will use this to add our sounds.

  • PyGame sound methods is something new. This is the first time that we use methods from the Pygame layer of our stack. From the Pygame methods, we can find methods for using sounds that we have loaded.

So there are two steps to playing sounds in GameFrame.

  1. Load the sound → the sound is an attribute of the Room/Level object, therefore it must happen within the Room __ init__()

  2. Play the sound → this can be used by any object with the Room/Level, including RoomObjects, but they need to access the attribute of the Room they are in.

We now have our mechanisms, so let’s plan the code.

IPO tables

Background Music

The first question we need to resolve, is what Room/Level does the sound reside in? Normally background music starts playing as soon as the game has loaded, therefore, our music should be loaded and played from into the WelcomeScreen Room/Level.

play background music IPO

Sound Effects

The sound effects are a bit more complicated. They all occur in the GamePlay Room/Level, but they are triggered by events tied to various RoomObjects within that room. Therefore, we need to split the loading and the playing over different objects:

  • loading will occur in the GamePlay class

  • play() call will occur wherever the relevant event is handled

sound effects IPO

Now we know what we have to, lets start coding.

Coding

Rooms/WelcomeScreen.py

We’ll start with the background music.

Open Rooms/WelcomeScreen.py and then add or adjust the highlighted code below:

 1from GameFrame import Level
 2from Objects.Title import Title
 3
 4class WelcomeScreen(Level):
 5    """
 6    Intial screen for the game
 7    """
 8    def __init__(self, screen, joysticks):
 9        Level.__init__(self, screen, joysticks)
10        
11        # set background image
12        self.set_background_image("Background.png")
13        
14        # add title object
15        self.add_room_object(Title(self, 240, 200))
16        
17        # load sounds
18        self.bg_music = self.load_sound("Music.mp3")
19        
20        # play background music
21        self.bg_music.play(loops=1)

Let’s investigate this code:

  • line 18: loads the "Music.mp3" sound file and saves it as bg_music

  • line 21: starts playing the music. Note loops=1 means the music will restart once it reaches the end.

Save Rooms/WelcomeScreen.py and the run MainController.py to test if the music plays.

Rooms/GamePlay.py

Now we need to load all the sound effects that will be used in the GamePlay room.

Open Rooms/GamePlay.py and the add or adjust the highlighted code below:

 1from GameFrame import Level, Globals
 2from Objects.Ship import Ship
 3from Objects.Zork import Zork
 4from Objects.Hud import Score, Lives
 5
 6class GamePlay(Level):
 7    def __init__(self, screen, joysticks):
 8        Level.__init__(self, screen, joysticks)
 9        
10        # set background image
11        self.set_background_image("Background.png")
12        
13        # add objects
14        self.add_room_object(Ship(self, 25, 50))
15        self.add_room_object(Zork(self,1120, 50))
16        
17        # add HUD items
18        self.score = Score(self, 
19                           Globals.SCREEN_WIDTH/2 - 20, 20, 
20                           str(Globals.SCORE))
21        self.add_room_object(self.score)
22        self.lives = Lives(self, Globals.SCREEN_WIDTH - 150, 20)
23        self.add_room_object(self.lives)
24        
25        # load sound files
26        self.shoot_laser = self.load_sound("Laser_shot.ogg")
27        self.asteroid_shot = self.load_sound("Asteroid_shot.wav")
28        self.astronaut_saved = self.load_sound("Astronaut_saved.ogg")
29        self.asteroid_collision = self.load_sound("Ship_damage.ogg")
30        self.astronaut_shot = self.load_sound("Astronaut_hit.ogg")

Unpacking the new code:

  • line 26: loads the "Laser_shot.ogg" sound file and saves it as shoot_laser

  • line 27: loads the "Asteroid_shot.wav" sound file and saves it as asteroid_shot

  • line 28: loads the "Astronaut_saved.ogg" sound file and saves it as astronaut_saved

  • line 29: loads the "Ship_damage.ogg" sound file and saves it as asteroid_collision

  • line 30: loads the "Astronaut_hit.ogg" sound file and saves it as astronaut_shot

Save and close Rooms/GamePlay.py.

We now need to insert the sound effects into their respective event handlers.

Objects/Laser.py

Open Objects/Laser.py and the add or adjust the highlighted code below:

39    # --- Event handlers
40    def handle_collision(self, other, other_type):
41        """
42        Handles laser collisions with other registered objects
43        """
44        if other_type == "Asteroid":
45            self.room.asteroid_shot.play()
46            self.room.delete_object(other)
47            self.room.score.update_score(5)
48        elif other_type == "Astronaut":
49            self.room.astronaut_shot.play()
50            self.room.delete_object(other)
51            self.room.score.update_score(-10)

Looking at the code:

  • line 45: plays the asteroid_shot sound when a laser collides with an asteroid.

    • since the asteroid_shot is an attribute of the GamePlay Room the laser needs to access an attribute of another object.

    • all RoomObjects have an attribute called room which identifies which room it belongs to.

    • self.room.asteroid_shot.play() should be read as:

      • play the asteroid_shot sound

      • the belongs to the room

      • this asteroid is in

    • line 46: plays the astronaut_shot sound when a laser collides with an astronaut.

Save and close Rooms/Laser.py.

Objects/Ship.py

Open Objects/Ship.py and the add or adjust the highlighted code below:

52    def shoot_laser(self):
53        """
54        Shoots a laser from the ship
55        """
56        if self.can_shoot:
57            new_laser = Laser(self.room, 
58                            self.x + self.width, 
59                            self.y + self.height/2 - 4)
60            self.room.add_room_object(new_laser)
61            self.can_shoot = False
62            self.set_timer(10,self.reset_shot)
63            self.room.shoot_laser.play()

Looking at the code:

  • line 63: plays the shoot_laser sound when the space bar is pressed

Save and close Objects/Ship.py

Objects/Asteroid.py

Open Objects/Asteroid.py and the add or adjust the highlighted code below:

53    def handle_collision(self, other, other_type):
54        """
55        Handles the collision events for the Asteroid
56        """
57        
58        if other_type == "Ship":
59            self.room.delete_object(self)
60            self.room.asteroid_collision.play()
61            Globals.LIVES -= 1
62            if Globals.LIVES > 0:
63                self.room.lives.update_image()
64            else:
65                self.room.running = False

Looking at the code:

  • line 60: plays the asteroid_collision sound when asteroid collides with the ship

Save and close Objects/Asteroid.py

Objects/Astronaut.py

Open Objects/Astronaut.py and the add or adjust the highlighted code below:

31    # --- Event Handlers
32    def handle_collision(self, other, other_type):
33        """
34        Handles the collision event for Astronaut objects
35        """
36        # ship collision
37        if other_type == "Ship":
38            self.room.astronaut_saved.play()
39            self.room.delete_object(self)
40            self.room.score.update_score(50)

Looking at the code:

  • line 60: plays the astronaut_saved sound when astronaut collides with the ship

Save and close Objects/Astronaut.py

Testing

Now it is time to test that the sounds effects are working.

In testing, use a testing table to make sure that you check all five of the sound effects:

Event

Expected Sound

Actual Sound

Resolution

Shooting laser

Laser_shot

Shooting asteroid

Asteroid_shot

Saving astronaut

Astronaut_saved

Ship collides with asteroid

Ship_damage

Shooting astronaut

Astronaut_hit

Adjusting volume

After testing the game, you may want to adjust the mix of the sound. If you are using the sound file provided you will find the background music is too loud and overpowers the sound effect. Fortunately, we can adjust the volume of individual sounds by using the Pygame .volume() method.

Rooms/WelcomeScreen.py

Open Rooms/WelcomeScreen.py and then add or adjust the highlighted code below:

 1from GameFrame import Level
 2from Objects.Title import Title
 3
 4class WelcomeScreen(Level):
 5    """
 6    Intial screen for the game
 7    """
 8    def __init__(self, screen, joysticks):
 9        Level.__init__(self, screen, joysticks)
10        
11        # set background image
12        self.set_background_image("Background.png")
13        
14        # add title object
15        self.add_room_object(Title(self, 240, 200))
16        
17        # load sounds
18        self.bg_music = self.load_sound("Music.mp3")
19        
20        # play background music
21        self.bg_music.set_volume(0.1)
22        self.bg_music.play(loops=1)

Save and close Rooms/WelcemeScreen.py and test that the volume levels are now correct using MainController.py

Commit and Push

We have finished and tested another section of code so we should make a Git commit.

To do this:

  1. In GitHub Desktop go to the bottom left-hand box and write into the summary Added lasers.

  2. Click on Commit to main

  3. Click on Push origin

Now the work from this lesson is committed and synced with the online repo.

Completed File States

Below are all the files we used in this lesson in their finished state. Use this to check if your code is correct.

Rooms/WelcomeScreen.py

 1from GameFrame import Level
 2from Objects.Title import Title
 3
 4class WelcomeScreen(Level):
 5    """
 6    Intial screen for the game
 7    """
 8    def __init__(self, screen, joysticks):
 9        Level.__init__(self, screen, joysticks)
10        
11        # set background image
12        self.set_background_image("Background.png")
13        
14        # add title object
15        self.add_room_object(Title(self, 240, 200))
16        
17        # load sounds
18        self.bg_music = self.load_sound("Music.mp3")
19        
20        # play background music
21        self.bg_music.set_volume(0.1)
22        self.bg_music.play(loops=1)

Rooms/GamePlay.py

 1from GameFrame import Level, Globals
 2from Objects.Ship import Ship
 3from Objects.Zork import Zork
 4from Objects.Hud import Score, Lives
 5
 6class GamePlay(Level):
 7    def __init__(self, screen, joysticks):
 8        Level.__init__(self, screen, joysticks)
 9        
10        # set background image
11        self.set_background_image("Background.png")
12        
13        # add objects
14        self.add_room_object(Ship(self, 25, 50))
15        self.add_room_object(Zork(self,1120, 50))
16        
17        # add HUD items
18        self.score = Score(self, 
19                           Globals.SCREEN_WIDTH/2 - 20, 20, 
20                           str(Globals.SCORE))
21        self.add_room_object(self.score)
22        self.lives = Lives(self, Globals.SCREEN_WIDTH - 150, 20)
23        self.add_room_object(self.lives)
24        
25        # load sound files
26        self.shoot_laser = self.load_sound("Laser_shot.ogg")
27        self.asteroid_shot = self.load_sound("Asteroid_shot.wav")
28        self.astronaut_saved = self.load_sound("Astronaut_saved.ogg")
29        self.asteroid_collision = self.load_sound("Ship_damage.ogg")
30        self.astronaut_shot = self.load_sound("Astronaut_hit.ogg")

Objects/Laser.py

 1from GameFrame import RoomObject, Globals
 2
 3class Laser(RoomObject):
 4    """
 5    Class for the lasers shot by the Ship
 6    """
 7    
 8    def __init__(self, room, x, y):
 9        """
10        Inistialise the laser
11        """
12        # include attributes and methods from RoomObject
13        RoomObject.__init__(self, room, x, y)
14        
15        # set image
16        image = self.load_image("Laser.png")
17        self.set_image(image, 33, 9)
18        
19        # set movement
20        self.set_direction(0, 20)
21        
22        # handle events
23        self.register_collision_object("Asteroid")
24        self.register_collision_object("Astronaut")
25        
26    def step(self):
27        """
28        Determine what happens to the laser on each tick of the game clock
29        """
30        self.outside_of_room()
31        
32    def outside_of_room(self):
33        """
34        removes laser if it has exited the room
35        """
36        if self.x > Globals.SCREEN_WIDTH:
37            self.room.delete_object(self)
38            
39    # --- Event handlers
40    def handle_collision(self, other, other_type):
41        """
42        Handles laser collisions with other registered objects
43        """
44        if other_type == "Asteroid":
45            self.room.asteroid_shot.play()
46            self.room.delete_object(other)
47            self.room.score.update_score(5)
48        elif other_type == "Astronaut":
49            self.room.astronaut_shot.play()
50            self.room.delete_object(other)
51            self.room.score.update_score(-10)

Objects/Ship.py

 1from GameFrame import RoomObject, Globals
 2from Objects.Laser import Laser
 3import pygame
 4
 5class Ship(RoomObject):
 6    """
 7    A class for the player's avitar (the Ship)
 8    """
 9    
10    def __init__(self, room, x, y):
11        """
12        Initialise the Ship object
13        """
14        RoomObject.__init__(self, room, x, y)
15        
16        # set image
17        image = self.load_image("Ship.png")
18        self.set_image(image,100,100)
19        
20        # register events
21        self.handle_key_events = True
22        
23        self.can_shoot = True
24        
25    def key_pressed(self, key):
26        """
27        Respond to keypress up and down
28        """
29        
30        if key[pygame.K_w]:
31            self.y_speed = -10
32        elif key[pygame.K_s]:
33            self.y_speed = 10
34        if key[pygame.K_SPACE]:
35            self.shoot_laser()
36            
37    def keep_in_room(self):
38        """
39        Keeps the ship inside the room
40        """
41        if self.y < 0:
42            self.y = 0
43        elif self.y + self.height> Globals.SCREEN_HEIGHT:
44            self.y = Globals.SCREEN_HEIGHT - self.height
45            
46    def step(self):
47        """
48        Determine what happens to the Ship on each click of the game clock
49        """
50        self.keep_in_room()
51        
52    def shoot_laser(self):
53        """
54        Shoots a laser from the ship
55        """
56        if self.can_shoot:
57            new_laser = Laser(self.room, 
58                            self.x + self.width, 
59                            self.y + self.height/2 - 4)
60            self.room.add_room_object(new_laser)
61            self.can_shoot = False
62            self.set_timer(10,self.reset_shot)
63            self.room.shoot_laser.play()
64            
65    def reset_shot(self):
66        """
67        Allows ship to shoot again
68        """
69        self.can_shoot = True

Objects/Asteroid.py

 1from GameFrame import RoomObject, Globals
 2import random
 3
 4class Asteroid(RoomObject):
 5    """
 6    A class for Zorks danerous obstacles
 7    """
 8    
 9    def __init__(self, room, x, y):
10        """
11        Initialise the Asteroid object
12        """
13        # include attributes and methods from RoomObject
14        RoomObject.__init__(self,room, x, y)
15        
16        # set image
17        image = self.load_image("asteroid.png")
18        self.set_image(image,50,49)
19        
20        # set travel direction
21        angle = random.randint(135,225)
22        self.set_direction(angle, 10)
23        
24        # register events
25        self.register_collision_object("Ship")
26        
27    def step(self):
28        """
29        Determines what happens to the asteroid on each tick of the game clock
30        """
31        self.keep_in_room()
32        self.outside_of_room()
33        
34    def keep_in_room(self):
35        """
36        Keeps the asteroid inside the top and bottom room limits
37        """
38        if self.y < 0:
39            self.y = 0
40            self.y_speed *= -1
41        elif self.y > Globals.SCREEN_HEIGHT - self.height:
42            self.y = Globals.SCREEN_HEIGHT - self.height
43            self.y_speed *= -1
44            
45    def outside_of_room(self):
46        """
47        removes asteroid that have exited the room
48        """
49        if self.x + self.width < 0:
50            print("asteroid deleted")
51            self.room.delete_object(self)
52            
53    def handle_collision(self, other, other_type):
54        """
55        Handles the collision events for the Asteroid
56        """
57        
58        if other_type == "Ship":
59            self.room.delete_object(self)
60            self.room.asteroid_collision.play()
61            Globals.LIVES -= 1
62            if Globals.LIVES > 0:
63                self.room.lives.update_image()
64            else:
65                self.room.running = False

Objects/Astronaut.py

 1from GameFrame import RoomObject
 2
 3class Astronaut(RoomObject):
 4    """
 5    Class for the astronauts escaping from Zork
 6    """
 7    
 8    def __init__(self,room,x,y):
 9        """
10        Initialise the astronaut instance
11        """
12        # include attirbutes and method from RoomObject
13        RoomObject.__init__(self,room,x,y)
14        
15        # set image
16        image = self.load_image("Astronaut.png")
17        self.set_image(image,50,49)
18        
19        # set travel direction
20        self.set_direction(180, 5)
21        
22        # handle events
23        self.register_collision_object("Ship")
24        
25    def step(self):
26        """
27        Determines what happend to the astronaut on each tick of the game clock
28        """
29        self.outside_of_room()
30        
31    # --- Event Handlers
32    def handle_collision(self, other, other_type):
33        """
34        Handles the collision event for Astronaut objects
35        """
36        # ship collision
37        if other_type == "Ship":
38            self.room.astronaut_saved.play()
39            self.room.delete_object(self)
40            self.room.score.update_score(50)
41            
42    def outside_of_room(self):
43        """
44        removes astronauts that have exited the room
45        """
46        if self.x + self.width < 0:
47            self.room.delete_object(self)