Timing is not necessarily a subject closely associated with sound effects or music in a game, but we’re going to be using both extensively in this chapter while making a very interesting game. The chapter project is called The Oil Spill Game. While studying timing and new gameplay concepts, you will learn how to load and play sound files.
You will learn how to:
Load and play a sound using the Pygame mixer
Use a back buffer to better control drawing
Create a fast-paced arcade-style game called The Oil Spill Game
The Oil Spill Game is shown in Figure 10.1. In this game, the player has to clean up an oil spill using a water cannon that fires a high-pressure stream of water at the contaminated areas to clean them up. At least, that’s the theory! In reality, we just use the mouse cursor to click on an oil splotch to clean it up! This game uses color alpha channel manipulation to erase the oil splotches and is a good exercise in user input as well as program logic.
The audio system we’re going to use is included with Pygame in the pygame.mixer
module. We will learn how to create an audio clip by loading an audio file and then playing the clip in game. Pygame offers some advanced features to control the channels for audio playback, control the mixing of sounds, and even the ability to generate sounds. Pygame does not always initialize the audio mixer, so it’s best to initialize it on our own just to be sure our program doesn’t crash upon trying to load and play a file. This only needs to be called once:
pygame.mixer.init()
We use the pygame.mixer.Sound()
class to load and manage audio objects. Pygame supports two audio file formats: uncompressed WAV and OGG audio files. OGG is a good alternative to MP3 swith no licensing issues. If you wanted to use MP3 in a commercial game, you would have to license the MP3 technology in order to use it. OGG offers similar quality and compression without the licensing! As a result, OGG is a good choice for longer-running music tracks in your game. Of course, you can use OGG for regular short-duration sound effects too. It just tends to be more common to use WAV for shorter audio files and OGG for the longer ones. That is entirely up to you, though. Since WAV files must be uncompressed, you cannot load any WAV file created with a special codec. Since they are uncompressed, the file sizes will tend to be on the large size if they are more than a few seconds in length. Because of the size issue, OGG is recommended for audio clips longer than a few seconds.
audio_clip = pygame.mixer.Sound("audio_file.wav")
If you have audio files in some format (like WAV or MP3) that you want to convert to OGG in order to use the format, you will need to convert the files with an audio converter. One good example is free software called Audacity which has advanced audio editing features as well. You can download it from http://audacity.sourceforge.net.
The pygame.mixer.Sound()
constructor returns a Sound
object. Among the several methods in this class are play()
and stop()
. These are easy enough to use, but we’re not going to use Sound
for playback, just for loading and storing the audio data. For playing sounds, we’re going to use pygame.mixer.Channel
. This is a class that offers more versatility than Sound
for playback. The Pygame audio mixer handles channels internally, so what we do is request an available channel.
channel = pygame.mixer.find_channel()
This will request an unused channel and return it so we can use it. If there are no available channels, then the mixer returns None
. Because this may be a problem, if you want to override that default behavior, then pass True
to find_channel()
in order to force it to return the lowest-priority channel available.
channel = pygame.mixer.find_channel(True)
Once we have a channel, we can play a Sound
object by using the Channel.play()
method:
channel.play(sound_clip)
There are additional features in both the Sound
and Channel
classes that you can find in the Pygame documentation online at www.pygame.org/docs/ref/mixer.html.
The Oil Spill Game is an interesting experiment in color manipulation with the use of alpha to cause the oil splotches to disappear as the user “wipes” them with the mouse cursor. This game could be called “stain remover” just as well, and the colors could be changed to any theme for variations on the theme of “oil cleanup.”
The oil splotches are indeed sprites, each created as a root pygame.sprite.Sprite
, and stored in a sprite group with pygame.sprite.Group
. A custom class inherits from MySprite
to add some new features required for the game, but they are otherwise pygame.sprite.Sprite
-based MySprite
objects. But there is no source artwork—the circles are drawn onto the sprite image at load time.
Timing is important in The Oil Spill Game. Once every second, a new oil sprite is added to a group that is drawn at a random location on the screen. Each oil sprite has a random radius so that after a while the screen does indeed begin to look like oil is dripping all over it. The trick is to add a new oil sprite only once per second, using timing. There are a few ways to do this, but perhaps the easiest way is with a millisecond timer. First, we create a root time:
last_time = 0
Then, this root time value is updated whenever the one-second interval is reached using pygame.time.get_ticks()
:
ticks = pygame.time.get_ticks() if ticks > last_time + 1000: add_oil() last_time = ticks
The key is saving the current ticks value in last_time
when a timing even occurs. If the ticks value is not saved in last_time
, then it will only happen once and then never again.
When the mouse cursor moves around on the screen, circle-based collision detection is used via pygame.sprite.collide_circle_ratio()
to determine when the mouse cursor is over an oil splotch sprite. Figure 10.2 shows the gameplay when the cursor is over such a sprite.
The oil sprites have a custom image that is created in memory (not loaded from a bitmap file). That image has a dark circle drawn on it with a random radius to represent a single oil splotch.
image = pygame.Surface((oil.radius,oil.radius)).convert_alpha() image.fill((255,255,255,0)) oil.fadelevel = random.randint(50,150) oil_color = 10,10,20,oil.fadelevel r2 = oil.radius//2 pygame.draw.circle(image, oil_color, (r2,r2), r2, 0) oil.set_image(image)
By clicking the mouse on an oil splotch, that has the effect of “cleaning” it because the dark circle fades out as it is being cleaned and disappears. This is done by changing the alpha channel color component of the sprite until it is completely invisible. When alpha reaches zero, then the sprite is removed from the group.
Identifying an oil sprite in the group could be done with a group collision method in pygame.sprite
, but I wanted to get a little more control over how collision response works, so this game iterates the group of oil sprites manually and calls pygame.sprite.collide_circle_ratio(0.5)
. This makes the collision radius half of the normal value to increase the game’s challenge a little bit.
for oil in oil_group: if pygame.sprite.collide_circle_ratio(0.5)(cursor, oil): oil_hit = oil
Although it is not necessary, to make the game a little more fun, clicking the mouse cursor on the screen causes it to appear to be cleaning the background as well. This helps the player get a feel for the gameplay before any oil splotches even show up. Two colors are defined: darktan
for the normal background, and tan
for the cleaned spots:
darktan = 190,190,110,255 tan = 210,210,130,255
Note that a fourth color component (alpha) is added to these colors. This is required when working with 32-bit color. If you don’t specify an alpha channel when creating the color, then it is not available later.
When the user clicks the mouse button anywhere on the screen, a tan circle is drawn to help the player get a feel for what they need to do in the game. It also makes the game more fun.
b1,b2,b3 = pygame.mouse.get_pressed() mx,my = pygame.mouse.get_pos() pos = (mx+30,my+30) if b1 > 0: pygame.draw.circle(backbuffer, tan, pos, 30, 0)
The source code for The Oil Spill Game is not long at all with 147 lines (including blanks and comments), but the source code is rather packed and efficient, so we’ll list the entire source code here for easy reference. It is an interesting game. I think there’s potential here by designing some gameplay challenges, adding scoring, etc.
# Oil Spill Game # Chapter 10 import sys, time, random, math, pygame from pygame.locals import * from MyLibrary import * darktan = 190,190,110,255 tan = 210,210,130,255 class OilSprite(MySprite): def __init__(self): MySprite.__init__(self) self.radius = random.randint(0,60) + 30 #radius 30 to 90 play_sound(new_oil) def update(self, timing, rate=30): MySprite.update(self, timing, rate) def fade(self): r2 = self.radius//2 color = self.image.get_at((r2,r2)) if color.a > 5: color.a -= 5 pygame.draw.circle(self.image, color, (r2,r2), r2, 0) else: oil_group.remove(self) play_sound(clean_oil) #this function initializes the game def game_init(): global screen, backbuffer, font, timer, oil_group, cursor, cursor_group pygame.init() screen = pygame.display.set_mode((800,600)) pygame.display.set_caption("Oil Spill Game") font = pygame.font.Font(None, 36) pygame.mouse.set_visible(False) timer = pygame.time.Clock() #create a drawing surface backbuffer = pygame.Surface((800,600)) backbuffer.fill(darktan) #create oil list oil_group = pygame.sprite.Group() #create cursor sprite cursor = MySprite() cursor.radius = 60 image = pygame.Surface((60,60)).convert_alpha() image.fill((255,255,255,0)) pygame.draw.circle(image, (80,80,220,70), (30,30), 30, 0) pygame.draw.circle(image, (80,80,250,255), (30,30), 30, 4) cursor.set_image(image) cursor_group = pygame.sprite.GroupSingle() cursor_group.add(cursor) #this function initializes the audio system def audio_init(): global new_oil, clean_oil #initialize the audio mixer pygame.mixer.init() #not always called by pygame.init() #load sound files new_oil = pygame.mixer.Sound("new_oil.wav") clean_oil = pygame.mixer.Sound("clean_oil.wav") def play_sound(sound): channel = pygame.mixer.find_channel(True) channel.set_volume(0.5) channel.play(sound) def add_oil(): global oil_group, new_oil oil = OilSprite() image = pygame.Surface((oil.radius,oil.radius)).convert_alpha() image.fill((255,255,255,0)) oil.fadelevel = random.randint(50,150) oil_color = 10,10,20,oil.fadelevel r2 = oil.radius//2 pygame.draw.circle(image, oil_color, (r2,r2), r2, 0) oil.set_image(image) oil.X = random.randint(0,760) oil.Y = random.randint(0,560) oil_group.add(oil) #main program begins game_init() audio_init() game_over = False last_time = 0 #repeating loop while True: timer.tick(30) ticks = pygame.time.get_ticks() for event in pygame.event.get(): if event.type == QUIT: sys.exit() keys = pygame.key.get_pressed() if keys[K_ESCAPE]: sys.exit() #get mouse input b1,b2,b3 = pygame.mouse.get_pressed() mx,my = pygame.mouse.get_pos() pos = (mx+30,my+30) if b1 > 0: pygame.draw.circle(backbuffer, tan, pos, 30, 0) #collision test oil_hit = None for oil in oil_group: if pygame.sprite.collide_circle_ratio(0.5)(cursor, oil): oil_hit = oil if b1 > 0: oil_hit.fade() break #add new oil sprite once per second if ticks > last_time + 1000: add_oil() last_time = ticks #draw backbuffer screen.blit(backbuffer, (0,0)) #draw oil oil_group.update(ticks) oil_group.draw(screen) #draw cursor cursor.position = (mx,my) cursor_group.update(ticks) cursor_group.draw(screen) if oil_hit: print_text(font, 0, 0, "OIL SPLOTCH - CLEAN IT!") else: print_text(font, 0, 0, "CLEAN") pygame.display.update()
This chapter covered the audio system in Pygame via pygame.mixer
and showed how to load and play a sound file in the context of gameplay events. An interesting game called The Oil Spill Game was used as a backdrop for an audio demonstration program, but it ended up having more interesting color and sprite manipulation code with audio something of an afterthought. Overall, though, there were some valuable concepts and useful source code presented.
3.16.75.165