+++ /dev/null
-#!/usr/bin/python3
-# (c) Copyright 2013 TJ <hacker@iam.tj>
-# (c) Copyright Eddie Berrisford-Lynch <dev@fun2be.me>
-# Licensed on the terms of the GNU General Public License version 3 (see COPYING)
-#
-# Demonstration of using Python 3.x, PyGame 1.92 and SDL to create a simple 2D
-# platform scrolling game
-
-import sys, os, time, random, pygame
-
-# Use conditionals in the code to print useful information to the console using the form:
-# if DEBUG: print("Debug: some_var=%d, another_var=%s" % (some_var, another_var))
-DEBUG = True
-
-pygame.init()
-
-class Channel:
- """ Represents an audio output. Can represent a user-created SDL channel or the single
- Music channel (which can load/play Ogg, MP3 and other audio media). The Music channel
- can be primed with a playlist which will loop.
- Many methods have dual functionality depending on whether the instance object represents
- the Music channel or a Sound channel, since those PyGame Classes have some differences in
- functionality.
- """
- # use integers to control volume which avoids float rounding error issues
- MAX = 100
- MIN = 0
- volume_step = 5
- def __init__(self, volume=1.0, channel_id=0):
- """ Constructor.
- volume: an SDL/PyGame floating-point with range 0.00 to 1.00. It is storied internally as an
- integer with range 0 to 100 to avoid rounding errors when stepping the volume.
- channel_id: -1 == create the single Music channel. 0,1,2,3... one of several Sound channels.
-
- self.channel == None indicates this is the Music channel.
- """
- self.volume = int(volume*100)
- self.channel_id = 0
- self.paused = False
- if channel_id == -1:
- self.channel_id = channel_id
- self.channel = None # flag to show this uses pygame.mixer.music
- elif channel_id < pygame.mixer.get_num_channels():
- self.channel_id = channel_id
- self.channel = pygame.mixer.Channel(self.channel_id)
-
- def set_volume(self, new_level):
- """ Ensure the new level is within legal range and alter the mixer level """
- if new_level >= Channel.MIN and new_level <= Channel.MAX:
- self.volume = new_level
- if self.channel == None:
- pygame.mixer.music.set_volume(self.volume/100)
- else:
- self.channel.set_volume(self.volume/100)
- if DEBUG: print("Channel.set_volume(", self.volume, ")")
-
- def volume_up(self):
- """ Increase volume by a single step """
- if self.volume + self.volume_step <= Channel.MAX:
- self.set_volume(self.volume + self.volume_step)
-
- def volume_down(self):
- """ Decrease volume by a single step """
- if self.volume - self.volume_step >= Channel.MIN:
- self.set_volume(self.volume - self.volume_step)
-
- def get_volume(self):
- return self.volume
-
- def queue(self, sound):
- """ Sound channels can queue one Sound in addition to the currently playing Sound """
- if self.channel != None:
- self.channel.queue(sound)
-
- def get_queue(self):
- """ Retrieve the queued Sound (if any) """
- return self.channel.get_queue() if self.channel != None else None
-
- def fadeout(self, time):
- """ Fade out the sound nicely
- time: milliseconds
- """
- if self.channel == None:
- pygame.mixer.music.fadeout(time)
- else:
- self.channel.fadeout(time)
-
- def play(self, sound, loops=0, maxtime=0, fade_ms=0):
- """ Play a Music track or Sound
- sound:
- loops: number of repeats (after initial play - see pyGame API docs)
- maxtime: seconds to play. 0 == to the end
- fade_ms: fade-out time in milliseconds
- """
- if self.channel != None:
- # allow 'sound' to be a path string or an object
- sound = pygame.mixer.Sound(sound)
- self.channel.play(sound, loops, maxtime, fade_ms)
- else:
- pygame.mixer.music.load(sound)
- pygame.mixer.music.play(loops)
-
- def pause(self):
- """ Toggle the play/pause state of the channel """
- if self.paused:
- self.paused = False
- if self.channel == None:
- pygame.mixer.music.unpause()
- else:
- self.channel.unpause()
- else:
- self.paused = True
- if self.channel == None:
- pygame.mixer.music.pause()
- else:
- self.channel.pause()
-
- def playlist(self, path):
- """ Create a playlist from a given file-system directory
- path: absolute or relative path to a directory containing *only* valid media files (Ogg, MP3, etc.)
-
- See pygame.mixer.music API docs
- """
- self.music_path = path
- # build a list of the filenames in the directory (ignores sub-directories)
- self.playlist = [ f for f in os.listdir(self.music_path) if os.path.isfile(os.path.join(self.music_path, f)) ]
- random.shuffle(self.playlist) # randomise the play order
-
- if DEBUG: print ("playlist=", self.playlist)
-
- self.playlist_index = -1 # flag for 'start from the beginning of list'
- self.playlist_next() # begin play-back
- # generate an event when the current track ends
- pygame.mixer.music.set_endevent(pygame.USEREVENT)
-
- def playlist_next(self):
- """ Begin playing the next track in the playlist. Loops when it reaches the end of the list. """
- self.playlist_index = self.playlist_index + 1 if self.playlist_index < len(self.playlist)-1 else 0
- if DEBUG: print("Channel.playlist_next() = [%d] %s" %(self.playlist_index, self.playlist[self.playlist_index] ))
-
- pygame.mixer.music.load(os.path.join(self.music_path, self.playlist[self.playlist_index]))
- pygame.mixer.music.play()
-
-
-class Game:
- """ Container for all game control and configuration items """
- config = dict()
- soundtrack = Channel(channel_id=-1)
- sound_effects = Channel(channel_id=0)
-
- def __init__(self, width=0, height=0, title=""):
- """ Constructor.
- width: desired width of the game window
- height: desired height of the game window
- title: Window caption (title-bar)
- """
- self.config.update({'width':width, 'height':height})
- self.config.update({'title':title})
- self.config.update({'sound':1}) # default to playing sound
- self.play = False # Flag that controls exit from main loop
- self.resolution = width, height
- self.debug = DEBUG
-
-
-class SeamlessBackground:
- """ The game window has a scrolling background image that is seamless, which allows it to be tiled
- so that as the viewport scrolls the image appears to be continuous in both directions.
-
- The game window only scrolls left or right, which means that the background needs to be redrawn
- at the left or right margins (depending on direction of scroll) or, as a one-off, the entire
- background image needs drawing at the beginning of the game.
-
- The seamless background image provided should be larger (wider) than the intended game window to
- allow the wrapping of the image to work correctly.
- """
- image_path = None
- image = None
- width = None
-
- def __init__(self, filename):
- """ Constructor.
- filename: absolute or relative path to seamless background image file (JPeG, PNG, etc.)
- """
- if os.path.exists(filename):
- self.image_path = filename
- self.image = pygame.image.load(self.image_path).convert()
- self.width = self.image.get_size()[0]
-
- if self.image == None:
- print("Error: failed to load image", filename)
-
- def draw(self, screen, x_pos, step):
- """ Redraw damaged areas of the game screen.
- x_pos: -1 == request the full screen be painted, not just the damaged left/right margins. Otherwise,
- it is the current game-level's left-edge x coordinate.
- step: the number of pixels, and direction, the screen has just scrolled (negative, 0, or positive)
- """
- if self.image != None:
- # resolution[0] = width, [1] = height
- resolution = screen.get_size()
- # adjust x_pos to be x coord of damaged rectangle (left or right side of screen)
- if step > 0:
- x_pos += resolution[0] - step
- elif step < 0:
- x_pos -= step
- elif step == 0 and x_pos >= 0:
- # nothing to do if not drawing the initial background
- return
-
- # calculate the offset into the background image such that the scrolled
- # game window and the background image will match
-
- # if drawing the background for the first time (x_pos==-1) use the entire image
- # use modulo to get the offset within the image of the first damaged column
- bk_x_start = x_pos % self.width if x_pos != -1 else 0
- bk_rect = pygame.Rect(bk_x_start, 0, resolution[0] if x_pos == -1 else abs(step), resolution[1])
- # When the image wraps there might not be 'step' columns of pixels available from the right side of
- # the seamless image. Therefore use an intermediate 'patch' image to build the complete image to be drawn
- # to the game window
- patch = pygame.Surface((bk_rect.width, bk_rect.height))
- patch.blit(self.image, (0, 0), bk_rect)
- remainder = self.width - bk_x_start
- if remainder < abs(step):
- # need more pixel columns because the seamless image just wrapped around
- patch.blit(self.image, (4,0), pygame.Rect(0, 0, remainder, resolution[1]))
-
- # calculate the left coord in the game window where the new background should be drawn
- scr_x_dest = resolution[0] - step if step > 0 else 0
- screen.blit(patch, (scr_x_dest, 0))
- else:
- print("No image to draw")
-
-
-def main():
- myGame = Game(900, 600)
- flags = pygame.OPENGL & pygame.DOUBLEBUF
- depth = 0
-
- # main loop sleep (100 milliseconds)
- delay = 0.01
-
- pygame.display.set_caption("Demo platform scroller")
-
- # background colour
- stepping = 8 # pixels scrolled each flip
- background_colour = (20, 20, 64)
- screen = pygame.display.set_mode(myGame.resolution, flags, depth)
- resolution = screen.get_size()
- screen.fill(background_colour)
-
- # background image
- background_image = SeamlessBackground(os.path.join("resources", "binary.jpg"))
- background_image.draw(screen, -1, 0) # -1 flags initial background drawing
- pygame.display.flip()
-
- # scrolling object
- blip = pygame.Surface((abs(stepping), abs(stepping)), pygame.HWSURFACE)
- blip_colour = (128, 128, 128)
- blip.fill(blip_colour)
-
- # starting position
- y = resolution[1] / 2
-
- if myGame.config['sound'] == 1:
- myGame.soundtrack.playlist(os.path.join(os.getcwd(), "resources", "Music"))
-
- auto_scroll = False
- movement = 0 # represents number of pixels, and direction, of next scroll
- x_pos = 0
- myGame.play = True # start the game
- while (myGame.play):
- # update game state
- if x_pos + movement >= 0:
- x_pos += movement
- else:
- movement = 0
-
- # change colour randomly
- d_red = random.randrange(-1,2) *8
- d_green = random.randrange(-1,2) *8
- d_blue = random.randrange(-1,2) *8
- # build a modified colour tuple, ensuring all values are valid
- 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)) )
- blip.fill(blip_colour)
-
- # change blip position up or down randomly by one step
- dy = random.randrange(-1,2) * abs(stepping)
- if((y + dy) < resolution[1] and (y + dy) >= 0):
- # TODO: no longer need to redraw background colour if using a background image
- # screen.fill(background_colour, pygame.Rect(resolution[0] - abs(stepping), y, abs(stepping), abs(stepping) ))
- y += dy
-
- # redraw the display
- screen.scroll(-movement, 0)
- background_image.draw(screen, x_pos, movement)
- screen.blit(blip, (resolution[0] - abs(stepping), y))
- pygame.display.flip()
-
- # manage the frame-rate
- time.sleep(delay)
-
- # process events
- for event in pygame.event.get():
- if event.type == pygame.QUIT or (event.type == pygame.KEYUP and event.key == pygame.K_q):
- myGame.play = False
- if event.type == pygame.KEYUP:
- k = event.key
- if k == pygame.K_p:
- time.sleep(10)
- elif k == pygame.K_7:
- myGame.soundtrack.pause()
- elif k == pygame.K_l:
- auto_scroll = True if auto_scroll == False else False
-
- if event.type == pygame.USEREVENT:
- if DEBUG: print("pygame.USEREVENT received; calling playlist_next()")
- myGame.soundtrack.playlist_next()
-
- if not auto_scroll:
- # reset if not auto-scrolling
- movement = 0
-
- # detect which keys are held down
- keys_pressed = pygame.key.get_pressed()
- if sum(keys_pressed):
- if keys_pressed[pygame.K_6]:
- # volume down
- myGame.soundtrack.volume_down()
- elif keys_pressed[pygame.K_8]:
- # volume up
- myGame.soundtrack.volume_up()
- elif keys_pressed[pygame.K_d]:
- # move right
- movement = stepping
- elif keys_pressed[pygame.K_a]:
- # move left
- movement = -stepping
-
- # end of game
- myGame.soundtrack.fadeout(5000)
- time.sleep(5)
- pygame.display.quit()
- exit(0)
-
-if __name__ == '__main__':
- main()