2 # (c) Copyright 2013 TJ <hacker@iam.tj>
3 # (c) Copyright Eddie Berrisford-Lynch <dev@fun2be.me>
4 # Licensed on the terms of the GNU General Public License version 3 (see COPYING)
6 # Demonstration of using Python 3.x, PyGame 1.92 and SDL to create a simple 2D
7 # platform scrolling game
9 import sys, os, time, random, pygame
11 # Use conditionals in the code to print useful information to the console using the form:
12 # if DEBUG: print("Debug: some_var=%d, another_var=%s" % (some_var, another_var))
18 """ Represents an audio output. Can represent a user-created SDL channel or the single
19 Music channel (which can load/play Ogg, MP3 and other audio media). The Music channel
20 can be primed with a playlist which will loop.
21 Many methods have dual functionality depending on whether the instance object represents
22 the Music channel or a Sound channel, since those PyGame Classes have some differences in
25 # use integers to control volume which avoids float rounding error issues
29 def __init__(self, volume=1.0, channel_id=0):
31 volume: an SDL/PyGame floating-point with range 0.00 to 1.00. It is storied internally as an
32 integer with range 0 to 100 to avoid rounding errors when stepping the volume.
33 channel_id: -1 == create the single Music channel. 0,1,2,3... one of several Sound channels.
35 self.channel == None indicates this is the Music channel.
37 self.volume = int(volume*100)
41 self.channel_id = channel_id
42 self.channel = None # flag to show this uses pygame.mixer.music
43 elif channel_id < pygame.mixer.get_num_channels():
44 self.channel_id = channel_id
45 self.channel = pygame.mixer.Channel(self.channel_id)
47 def set_volume(self, new_level):
48 """ Ensure the new level is within legal range and alter the mixer level """
49 if new_level >= Channel.MIN and new_level <= Channel.MAX:
50 self.volume = new_level
51 if self.channel == None:
52 pygame.mixer.music.set_volume(self.volume/100)
54 self.channel.set_volume(self.volume/100)
55 if DEBUG: print("Channel.set_volume(", self.volume, ")")
58 """ Increase volume by a single step """
59 if self.volume + self.volume_step <= Channel.MAX:
60 self.set_volume(self.volume + self.volume_step)
62 def volume_down(self):
63 """ Decrease volume by a single step """
64 if self.volume - self.volume_step >= Channel.MIN:
65 self.set_volume(self.volume - self.volume_step)
70 def queue(self, sound):
71 """ Sound channels can queue one Sound in addition to the currently playing Sound """
72 if self.channel != None:
73 self.channel.queue(sound)
76 """ Retrieve the queued Sound (if any) """
77 return self.channel.get_queue() if self.channel != None else None
79 def fadeout(self, time):
80 """ Fade out the sound nicely
83 if self.channel == None:
84 pygame.mixer.music.fadeout(time)
86 self.channel.fadeout(time)
88 def play(self, sound, loops=0, maxtime=0, fade_ms=0):
89 """ Play a Music track or Sound
91 loops: number of repeats (after initial play - see pyGame API docs)
92 maxtime: seconds to play. 0 == to the end
93 fade_ms: fade-out time in milliseconds
95 if self.channel != None:
96 # allow 'sound' to be a path string or an object
97 sound = pygame.mixer.Sound(sound)
98 self.channel.play(sound, loops, maxtime, fade_ms)
100 pygame.mixer.music.load(sound)
101 pygame.mixer.music.play(loops)
104 """ Toggle the play/pause state of the channel """
107 if self.channel == None:
108 pygame.mixer.music.unpause()
110 self.channel.unpause()
113 if self.channel == None:
114 pygame.mixer.music.pause()
118 def playlist(self, path):
119 """ Create a playlist from a given file-system directory
120 path: absolute or relative path to a directory containing *only* valid media files (Ogg, MP3, etc.)
122 See pygame.mixer.music API docs
124 self.music_path = path
125 # build a list of the filenames in the directory (ignores sub-directories)
126 self.playlist = [ f for f in os.listdir(self.music_path) if os.path.isfile(os.path.join(self.music_path, f)) ]
127 random.shuffle(self.playlist) # randomise the play order
129 if DEBUG: print ("playlist=", self.playlist)
131 self.playlist_index = -1 # flag for 'start from the beginning of list'
132 self.playlist_next() # begin play-back
133 # generate an event when the current track ends
134 pygame.mixer.music.set_endevent(pygame.USEREVENT)
136 def playlist_next(self):
137 """ Begin playing the next track in the playlist. Loops when it reaches the end of the list. """
138 self.playlist_index = self.playlist_index + 1 if self.playlist_index < len(self.playlist)-1 else 0
139 if DEBUG: print("Channel.playlist_next() = [%d] %s" %(self.playlist_index, self.playlist[self.playlist_index] ))
141 pygame.mixer.music.load(os.path.join(self.music_path, self.playlist[self.playlist_index]))
142 pygame.mixer.music.play()
146 """ Container for all game control and configuration items """
148 soundtrack = Channel(channel_id=-1)
149 sound_effects = Channel(channel_id=0)
151 def __init__(self, width=0, height=0, title=""):
153 width: desired width of the game window
154 height: desired height of the game window
155 title: Window caption (title-bar)
157 self.config.update({'width':width, 'height':height})
158 self.config.update({'title':title})
159 self.config.update({'sound':1}) # default to playing sound
160 self.play = False # Flag that controls exit from main loop
161 self.resolution = width, height
165 class SeamlessBackground:
166 """ The game window has a scrolling background image that is seamless, which allows it to be tiled
167 so that as the viewport scrolls the image appears to be continuous in both directions.
169 The game window only scrolls left or right, which means that the background needs to be redrawn
170 at the left or right margins (depending on direction of scroll) or, as a one-off, the entire
171 background image needs drawing at the beginning of the game.
173 The seamless background image provided should be larger (wider) than the intended game window to
174 allow the wrapping of the image to work correctly.
180 def __init__(self, filename):
182 filename: absolute or relative path to seamless background image file (JPeG, PNG, etc.)
184 if os.path.exists(filename):
185 self.image_path = filename
186 self.image = pygame.image.load(self.image_path).convert()
187 self.width = self.image.get_size()[0]
189 if self.image == None:
190 print("Error: failed to load image", filename)
192 def draw(self, screen, x_pos, step):
193 """ Redraw damaged areas of the game screen.
194 x_pos: -1 == request the full screen be painted, not just the damaged left/right margins. Otherwise,
195 it is the current game-level's left-edge x coordinate.
196 step: the number of pixels, and direction, the screen has just scrolled (negative, 0, or positive)
198 if self.image != None:
199 # resolution[0] = width, [1] = height
200 resolution = screen.get_size()
201 # adjust x_pos to be x coord of damaged rectangle (left or right side of screen)
203 x_pos += resolution[0] - step
206 elif step == 0 and x_pos >= 0:
207 # nothing to do if not drawing the initial background
210 # calculate the offset into the background image such that the scrolled
211 # game window and the background image will match
213 # if drawing the background for the first time (x_pos==-1) use the entire image
214 # use modulo to get the offset within the image of the first damaged column
215 bk_x_start = x_pos % self.width if x_pos != -1 else 0
216 bk_rect = pygame.Rect(bk_x_start, 0, resolution[0] if x_pos == -1 else abs(step), resolution[1])
217 # When the image wraps there might not be 'step' columns of pixels available from the right side of
218 # the seamless image. Therefore use an intermediate 'patch' image to build the complete image to be drawn
220 patch = pygame.Surface((bk_rect.width, bk_rect.height))
221 patch.blit(self.image, (0, 0), bk_rect)
222 remainder = self.width - bk_x_start
223 if remainder < abs(step):
224 # need more pixel columns because the seamless image just wrapped around
225 patch.blit(self.image, (4,0), pygame.Rect(0, 0, remainder, resolution[1]))
227 # calculate the left coord in the game window where the new background should be drawn
228 scr_x_dest = resolution[0] - step if step > 0 else 0
229 screen.blit(patch, (scr_x_dest, 0))
231 print("No image to draw")
235 myGame = Game(900, 600)
236 flags = pygame.OPENGL & pygame.DOUBLEBUF
239 # main loop sleep (100 milliseconds)
242 pygame.display.set_caption("Demo platform scroller")
245 stepping = 8 # pixels scrolled each flip
246 background_colour = (20, 20, 64)
247 screen = pygame.display.set_mode(myGame.resolution, flags, depth)
248 resolution = screen.get_size()
249 screen.fill(background_colour)
252 background_image = SeamlessBackground(os.path.join("resources", "binary.jpg"))
253 background_image.draw(screen, -1, 0) # -1 flags initial background drawing
254 pygame.display.flip()
257 blip = pygame.Surface((abs(stepping), abs(stepping)), pygame.HWSURFACE)
258 blip_colour = (128, 128, 128)
259 blip.fill(blip_colour)
262 y = resolution[1] / 2
264 if myGame.config['sound'] == 1:
265 myGame.soundtrack.playlist(os.path.join(os.getcwd(), "resources", "Music"))
268 movement = 0 # represents number of pixels, and direction, of next scroll
270 myGame.play = True # start the game
273 if x_pos + movement >= 0:
278 # change colour randomly
279 d_red = random.randrange(-1,2) *8
280 d_green = random.randrange(-1,2) *8
281 d_blue = random.randrange(-1,2) *8
282 # build a modified colour tuple, ensuring all values are valid
283 blip_colour = tuple( b+d if b+d < 256 and b+d >=0 else b for b, d in zip(blip_colour, (d_red, d_green, d_blue)) )
284 blip.fill(blip_colour)
286 # change blip position up or down randomly by one step
287 dy = random.randrange(-1,2) * abs(stepping)
288 if((y + dy) < resolution[1] and (y + dy) >= 0):
289 # TODO: no longer need to redraw background colour if using a background image
290 # screen.fill(background_colour, pygame.Rect(resolution[0] - abs(stepping), y, abs(stepping), abs(stepping) ))
294 screen.scroll(-movement, 0)
295 background_image.draw(screen, x_pos, movement)
296 screen.blit(blip, (resolution[0] - abs(stepping), y))
297 pygame.display.flip()
299 # manage the frame-rate
303 for event in pygame.event.get():
304 if event.type == pygame.QUIT or (event.type == pygame.KEYUP and event.key == pygame.K_q):
306 if event.type == pygame.KEYUP:
310 elif k == pygame.K_7:
311 myGame.soundtrack.pause()
312 elif k == pygame.K_l:
313 auto_scroll = True if auto_scroll == False else False
315 if event.type == pygame.USEREVENT:
316 if DEBUG: print("pygame.USEREVENT received; calling playlist_next()")
317 myGame.soundtrack.playlist_next()
320 # reset if not auto-scrolling
323 # detect which keys are held down
324 keys_pressed = pygame.key.get_pressed()
325 if sum(keys_pressed):
326 if keys_pressed[pygame.K_6]:
328 myGame.soundtrack.volume_down()
329 elif keys_pressed[pygame.K_8]:
331 myGame.soundtrack.volume_up()
332 elif keys_pressed[pygame.K_d]:
335 elif keys_pressed[pygame.K_a]:
340 myGame.soundtrack.fadeout(5000)
342 pygame.display.quit()
345 if __name__ == '__main__':