# -*- coding: utf-8 -*- import pygame, math, sys from pygame.locals import * LOADGIF = True try: import Image except: print "Loading of gifs will be disabled" LOADGIF = False SPRITE_DEFAULT_POSITION = [ -100, -100 ] SPRITE_DEFAULT_COLOR = ( 0, 255, 0 ) #-----------------------------------------------------------smoothrot # http://echochamber.me/viewtopic.php?f=11&t=33537 def smoothrot(surf, angle, scale = 3): """Perform a smoother rotation through enlarging and shrinking""" w = surf.get_width( ) h = surf.get_height( ) if w > h: s = w else: s = h surf2 = pygame.transform.rotozoom(surf, angle, scale) d = int(round((surf2.get_width() - s * scale) / 2.)) surf2 = pygame.transform.chop(surf2, (0, 0, d, d)) surf2 = pygame.transform.chop(surf2, (s * scale, s * scale, 2*d, 2*d)) surf2 = pygame.transform.scale(surf2, (s, s)) c = surf.get_colorkey() if c: surf2.set_colorkey(c) return surf2 #-------------------------------------------------------end smoothrot #-----------------------------------------------------------load_image # Code for loading GIF with PIL from: # http://choosetheforce.blogspot.com/2008/04/third-pygame-hulk-want-animation.html # http://choosetheforce.blogspot.com/2008/04/seventh-pygame-die-fluctuating-border.html # Framing and adjustments by Alex Kuhl def to_triplets(pil_palette): return [ pil_palette[ i : i+3 ] for i in range( 0, len( pil_palette ), 3 ) ] def load_image_animated( name ): images = [] frametime = 0 if name.endswith( ".gif" ): if LOADGIF: pil_image = Image.open( name ) frametime = pil_image.info[ 'duration' ] palette = to_triplets( pil_image.getpalette( ) ) try: while True: ( x0, y0, x1, y1 ) = ( 0, 0 ) + pil_image.size if len( pil_image.tile ) > 0: ( x0, y0, x1, y1 ) = pil_image.tile[ 0 ][ 1 ] image = pygame.image.fromstring( pil_image.tostring( ), pil_image.size, 'P' ) image.set_palette( palette ) image.set_colorkey( pil_image.info[ 'transparency' ] ) new_image = pygame.Surface( pil_image.size, pygame.SRCALPHA ) new_image.blit( image, ( x0, y0 ), ( x0, y0, x1 - x0, y1 - y0 ) ) images.append( new_image ) pil_image.seek( pil_image.tell() + 1 ) except EOFError: pass else: print "Loading of animated gifs is disabled. Please install PIL." sys.exit( ) else: print "Loading of non-gif animations is not current supported." sys.exit( ) return images, frametime def load_image( name, colorkey=None ): try: image = pygame.image.load( name ) except pygame.error, message: print 'Cannot load image:', name raise SystemExit, message if colorkey is not None: if colorkey is -1: colorkey = image.get_at( (0,0) ) image.set_colorkey( colorkey, RLEACCEL ) image = image.convert( ) else: image = image.convert_alpha( ) return image, image.get_rect( ) #-------------------------------------------------------end load_image #-----------------------------------------------------------normalize def normalize( vector ): a = vector[ 0 ] b = vector[ 1 ] length = math.sqrt( vector[ 0 ]**2 + vector[ 1 ]**2 ) if length != 0: return [ a/length, b/length ] return [ 0, 0 ] #-------------------------------------------------------end normalize #-----------------------------------------------------------EasySprite class EasySprite( pygame.sprite.Sprite ): def __init__( self, pPos ): pygame.sprite.Sprite.__init__( self ) self.pos = list( pPos ) self.rect = Rect( self.pos[ 0 ], self.pos[ 1 ], 50,50 ) self.hitmask = None self.image = None self.original = None self.velocity = 0 self.velangle = 0 # angle of velocity direction --> these two are tied self.veldir = [ 0, 0 ] # x and y components of velocity direction -/ self.rotangle = 0 def blit( self, screen ): screen.blit( self.image, self.rect ) # set the hitmask for pixel perfect collision detection # must be called *after* child class sets the image def set_hitmask( self ): self.hitmask = pygame.surfarray.array_alpha( self.image ) def set_position( self, pPos ): self.pos = list( pPos ) self.rect.topleft = self.pos def set_cposition( self, pPos ): self.rect.center = pPos self.pos = list( self.rect.topleft ) def get_position( self ): return self.pos def get_cposition( self ): return self.rect.center def get_left( self ): return self.rect.left def set_left( self, l ): self.rect.left = l self.reset_pos( ) def get_right( self ): return self.rect.right def set_right( self, r ): self.rect.right = r self.reset_pos( ) def get_top( self ): return self.rect.top def set_top( self, t ): self.rect.top = t self.reset_pos( ) def get_bottom( self ): return self.rect.bottom def set_bottom( self, b ): self.rect.bottom = b self.reset_pos( ) def reset_pos( self ): self.pos = list( self.rect.topleft ) def get_velocity( self ): return self.velocity def set_velocity( self, vel ): self.velocity = vel def get_velocity_dirX( self ): return self.veldir[ 0 ] def set_velocity_dirX( self, x ): self.veldir[ 0 ] = x self.veldir = normalize( self.veldir ) def get_velocity_dirY( self ): return self.veldir[ 1 ] def set_velocity_dirY( self, y ): self.veldir[ 1 ] = y self.veldir = normalize( self.veldir ) def get_velocity_dir( self ): return self.veldir def get_velocity_angle( self ): return math.atan2( self.veldir[ 1 ], self.veldir[ 0 ] ) def set_velocity_dir( self, angle ): # angle in radians self.veldir[ 0 ] = math.cos( angle ) self.veldir[ 1 ] = math.sin( angle ) def set_velocity_dir_deg( self, angle ): # angle in degrees self.set_velocity_dir( math.radians( angle ) ) # ********** this is probably the easiest way to, at least initially, set the movement of a sprite def set_movement( self, v, a ): # angle in radians self.set_velocity( v ) self.set_velocity_dir( a ) def move_pixels( self, px, py ): self.pos[ 0 ] = self.pos[ 0 ] + px self.pos[ 1 ] = self.pos[ 1 ] + py self.rect.topleft = self.pos def move_seconds( self, sec ): self.pos[ 0 ] = self.pos[ 0 ] + sec*self.veldir[ 0 ]*self.velocity self.pos[ 1 ] = self.pos[ 1 ] + sec*self.veldir[ 1 ]*self.velocity self.rect.topleft = self.pos def move_ticks( self, ticks ): self.move_seconds( ticks/1000.0 ) def move( self, ticks ): # shortcut for move_ticks self.move_seconds( ticks/1000.0 ) def scale( self, res ): temp = self.rect.center if self.image.get_bitsize( ) >= 24: self.image = pygame.transform.smoothscale( self.image, res ) else: self.image = pygame.transform.scale( self.image, res ) self.rect = self.image.get_rect( ) self.rect.center = temp self.pos = list( self.rect.topleft ) self.set_hitmask( ) self.update_original( ) def rotate_rad( self, angle ): self.rotate_deg( math.degrees( angle ) ) def rotate_deg( self, angle ): self.rotangle = ( self.rotangle + angle ) % 360 center = self.rect.center #self.image = pygame.transform.rotate( self.original, self.rotangle ) self.image = smoothrot( self.original, -self.rotangle, 3 ) self.rect = self.image.get_rect( ) self.rect.center = center self.pos = list( self.rect.topleft ) self.set_hitmask( ) def update_image( self, surf ): p = self.rect.center self.image = surf self.rect = self.image.get_rect( ) self.rect.center = p self.pos = list( self.rect.topleft ) self.set_hitmask( ) self.update_original( ) # because rotations mess up the image, we need a copy of the original surface # any method that alters the original, such as the scale or color changing, # should call this after it completes the surface changes so that # EasySprite can properly rotate # EXCEPTION: if the sprite will never be rotated this can safely be ignored def update_original( self ): self.original = self.image.copy( ) #-------------------------------------------------------end EasySprite #------------------------------------------------------RectangleSprite class RectangleSprite( EasySprite ): def __init__( self, pWidth, pHeight, pPos = SPRITE_DEFAULT_POSITION, pColor = SPRITE_DEFAULT_COLOR, pThickness = 0 ): EasySprite.__init__( self, pPos ) self.color = pColor self.width = pWidth self.height = pHeight self.thick = pThickness self.update_draw( ) def draw( self ): pygame.draw.rect( self.image, self.color, self.rect, self.thick ) def update_draw( self ): # TODO: figure out why rectangle and circle need the temp pos holder # and can't just update self.rect.topleft to self.pos after setting rect pos = self.pos self.image = pygame.Surface( ( self.width, self.height ), pygame.SRCALPHA, 32 ).convert_alpha( ) self.rect = self.image.get_rect( ) self.draw( ) self.update_image( self.image ) self.pos = pos self.rect.topleft = self.pos def get_color( self ): return self.color def set_color( self, pColor ): self.color = pColor self.update_draw( ) def set_width( self, w ): self.width = w self.update_draw( ) def set_height( self, h ): self.height = h self.update_draw( ) #--------------------------------------------------end RectangleSprite #------------------------------------------------------CircleSprite class CircleSprite( EasySprite ): def __init__( self, pRadius, pPos = SPRITE_DEFAULT_POSITION, pColor = SPRITE_DEFAULT_COLOR , pThickness = 0): EasySprite.__init__( self, [ pPos[ 0 ]-pRadius, pPos[ 1 ]-pRadius ] ) self.radius = pRadius self.color = pColor self.thick = pThickness self.update_draw( ) def draw( self ): pygame.draw.circle( self.image, self.color, ( self.radius, self.radius ), self.radius, self.thick ) def update_draw( self ): pos = self.pos self.image = pygame.Surface( ( self.radius*2, self.radius*2 ), pygame.SRCALPHA, 32 ).convert_alpha( ) self.rect = self.image.get_rect( ) self.draw( ) self.update_image( self.image ) self.pos = pos self.rect.topleft = self.pos def get_color( self ): return self.color def set_color( self, pColor ): self.color = pColor self.draw( ) def get_radius( self ): return self.radius def set_radius( self, pRadius ): self.radius = pRadius self.update_draw( ) #---------------------------------------------------end CircleSprite #------------------------------------------------------ImageSprite # a class to wrap images into a Sprite class ImageSprite( EasySprite ): # the constructor # pos - where you want sprite (top left, use set_cposition to change center position after creation) # filename - the filename of the image # color - whether image uses colorkey # - gifs seem to require use of colorkey rather than alpha, use -1 for them def __init__( self, pFileName, pPos = SPRITE_DEFAULT_POSITION, pColorKey=None ): EasySprite.__init__( self, pPos ) self.colorkey = pColorKey self.filename = pFileName self.update_draw( ) def update_draw( self ): self.update_image( load_image( self.filename, self.colorkey )[ 0 ] ) def set_image( self, pFileName, pColorKey=False ): self.colorkey = pColorKey self.filename = pFileName self.update_draw( ) #--------------------------------------------------end ImageSprite class AnimationSprite( EasySprite ): # colorkey is -1 for animations because it is assumed a GIF, which for some reason # image transparency does not work with def __init__( self, pFileName, pPos = SPRITE_DEFAULT_POSITION ): EasySprite.__init__( self, pPos ) self.filename = pFileName self.currtime = 0 self.imglist = [] self.frametime = 100 self.__load_images( ) self.frame = 0 self.update_draw( ) def __load_images( self ): self.imglist, self.frametime = load_image_animated( self.filename ) def update_draw( self ): self.update_image( self.imglist[ self.frame ] ) def next_frame( self ): self.frame = ( self.frame + 1 ) % len( self.imglist ) self.update_draw( ) def update_frame( self, ticks ): self.currtime += ticks if self.currtime > self.frametime: self.next_frame( ) self.currtime = 0 #------------------------------------------------------TextSprite # a class to wrap text into a Sprite class TextSprite( EasySprite ): # the constructor def __init__( self, pMessage, pSize, pPos = SPRITE_DEFAULT_POSITION, pColor = SPRITE_DEFAULT_COLOR ): EasySprite.__init__( self, pPos ) self.text = pMessage self.size = pSize self.color = pColor self.pos = pPos self.font = pygame.font.Font( None, pSize ) self.update_draw( ) def update_draw( self ): self.update_image( self.font.render( self.text, True, self.color ) ) def set_text( self, pMessage ): self.text = pMessage self.update_draw( ) def set_color( self, pColor ): self.color = pColor self.update_draw( ) #--------------------------------------------------end TextSprite #------------------------------------------------------TextBoxSprite class TextRectException: def __init__(self, message = None): self.message = message def __str__(self): return self.message class TextBoxSprite( TextSprite ): # the constructor def __init__( self, pMessage, pSize, pRectSize, pPos = SPRITE_DEFAULT_POSITION, pColor = SPRITE_DEFAULT_COLOR, pJustification = 0 ): TextSprite.__init__( self, pMessage, pSize, pPos, pColor ) self.rect = pygame.Rect( pPos, pRectSize ) self.justification = pJustification self.update_draw( ) def update_draw( self ): try: surf = self.render_textrect( ) except TextRectException: surf = self.font.render( "Text too big for passed rect", True, self.color ) self.update_image( surf ) def set_text( self, pMessage ): self.text = pMessage self.update_draw( ) def set_color( self, pColor ): self.color = pColor self.update_draw( ) def render_textrect( self ): """ From http://www.pygame.org/pcr/text_rect/index.php Slight adjustments made by Alex Kuhl Returns a surface containing the passed text string, reformatted to fit within the given rect, word-wrapping as necessary. The text will be anti-aliased. Takes the following arguments: justification - 0 (default) left-justified 1 horizontally centered 2 right-justified Returns the following values: Success - a surface object with the text rendered onto it. Failure - raises a TextRectException if the text won't fit onto the surface. """ final_lines = [] requested_lines = self.text.splitlines() # Create a series of lines that will fit on the provided # rectangle. for requested_line in requested_lines: if self.font.size(requested_line)[0] > self.rect.width: words = requested_line.split(' ') # if any of our words are too long to fit, return. for word in words: if self.font.size(word)[0] >= self.rect.width: raise TextRectException, "The word " + word + " is too long to fit in the rect passed." # Start a new line accumulated_line = "" for word in words: test_line = accumulated_line + word + " " # Build the line while the words fit. if self.font.size(test_line)[0] < self.rect.width: accumulated_line = test_line else: final_lines.append(accumulated_line) accumulated_line = word + " " final_lines.append(accumulated_line) else: final_lines.append(requested_line) # Let's try to write the text out on the surface. surface = pygame.Surface( self.rect.size, pygame.SRCALPHA, 32 ).convert_alpha( ) accumulated_height = 0 for line in final_lines: if accumulated_height + self.font.size(line)[1] >= self.rect.height: raise TextRectException, "Once word-wrapped, the text string was too tall to fit in the rect." if line != "": tempsurface = self.font.render(line, True, self.color ) if self.justification == 0: surface.blit(tempsurface, (0, accumulated_height)) elif self.justification == 1: surface.blit(tempsurface, ((rect.width - tempsurface.get_width()) / 2, accumulated_height)) elif self.justification == 2: surface.blit(tempsurface, (rect.width - tempsurface.get_width(), accumulated_height)) else: raise TextRectException, "Invalid justification argument: " + str(justification) accumulated_height += self.font.size(line)[1] return surface #-----------------------------------------------------end TextSprite #================================================================ #**************************************************************** # Collision Handling Section * #**************************************************************** #================================================================ #-----------------------------------------------------------backout # useful functions that will move a sprite colliding with another sprite # or group until it is touching to the minimal extent or not at all (NC versions) def backout_sprite( spr, spr2 ): colfunc = spritescollide_pp # reverse direction and back away until no longer colliding spr.set_velocity_dirX( -spr.get_velocity_dirX( ) ) spr.set_velocity_dirY( -spr.get_velocity_dirY( ) ) temp = colfunc( spr, colgrp ) if temp: spr.move( 10 ) while colfunc( spr, spr2 ): spr.move( 10 ) # reverse direction again and move back so still colliding a little spr.set_velocity_dirX( -spr.get_velocity_dirX( ) ) spr.set_velocity_dirY( -spr.get_velocity_dirY( ) ) while not colfunc( spr, spr2 ): spr.move( 1 ) def backout_sprite_nc( spr, spr2 ): colfunc = spritescollide_pp # reverse direction and back away until no longer colliding spr.set_velocity_dirX( -spr.get_velocity_dirX( ) ) spr.set_velocity_dirY( -spr.get_velocity_dirY( ) ) temp = colfunc( spr, spr2 ) if temp: spr.move( 1 ) while colfunc( spr, spr2 ): spr.move( 1 ) # reverse direction again so velocity state is left same as passed in spr.set_velocity_dirX( -spr.get_velocity_dirX( ) ) spr.set_velocity_dirY( -spr.get_velocity_dirY( ) ) def backout_group( spr, colgrp ): colfunc = spritecollideany_pp # reverse direction and back away until no longer colliding spr.set_velocity_dirX( -spr.get_velocity_dirX( ) ) spr.set_velocity_dirY( -spr.get_velocity_dirY( ) ) temp = colfunc( spr, colgrp ) if temp: spr.move( 10 ) while colfunc( spr, colgrp ): spr.move( 10 ) # reverse direction again and move back so still colliding a little spr.set_velocity_dirX( -spr.get_velocity_dirX( ) ) spr.set_velocity_dirY( -spr.get_velocity_dirY( ) ) while not colfunc( spr, colgrp ): spr.move( 1 ) def backout_group_nc( spr, colgrp ): colfunc = spritecollideany_pp # reverse direction and back away until no longer colliding spr.set_velocity_dirX( -spr.get_velocity_dirX( ) ) spr.set_velocity_dirY( -spr.get_velocity_dirY( ) ) temp = colfunc( spr, colgrp ) if temp: spr.move( 1 ) while colfunc( spr, colgrp ): spr.move( 1 ) # reverse direction again so velocity state is left same as passed in spr.set_velocity_dirX( -spr.get_velocity_dirX( ) ) spr.set_velocity_dirY( -spr.get_velocity_dirY( ) ) #-------------------------------------------------------end backout #******************************************** # Pixel Perfect Collisions # from http://www.pygame.org/projects/9/207/ # Additions by Alex Kuhl # - combined nested IFs into single with AND # - adjusted the functions to test for self-collision #******************************************** def _pixelPerfectCollisionDetection(sp1,sp2): """ Internal method used for pixel perfect collision detection. """ rect1 = sp1.rect; rect2 = sp2.rect; rect = rect1.clip(rect2) hm1 = sp1.hitmask hm2 = sp2.hitmask x1 = rect.x-rect1.x y1 = rect.y-rect1.y x2 = rect.x-rect2.x y2 = rect.y-rect2.y for r in range(0,rect.height): for c in range(0,rect.width): if hm1[c+x1][r+y1] & hm2[c+x2][r+y2]: return 1 return 0 def spritescollide_pp( sp1, sp2, dokill=False ): spritecollide = sp1.rect.colliderect ppcollide = _pixelPerfectCollisionDetection if spritecollide( sp2.rect ) and ppcollide( sp1, sp2 ): if dokill: sp2.kill( ) return True return False def spritecollide_pp(sprite, group, dokill=False): """pygame.sprite.spritecollide_pp(sprite, group, dokill) -> list pixel perfect collision detection between sprite and group given a sprite and a group of sprites, this will return a list of all the sprites that intersect the given sprite. all sprites must have a "hitmap" value, which is a 2d array that contains a value larger than zero for all pixels that can collide. the "hitmap" 2d array can be set by using pygame.surfarray.array_colorkey() or pygame.surfarray.array_alpha(). all sprites must have a "rect" value, which is a rectangle of the sprite area.if the dokill argument is true, the sprites that do collide will be automatically removed from all groups.""" crashed = [] spritecollide = sprite.rect.colliderect ppcollide = _pixelPerfectCollisionDetection if dokill: for s in group.sprites(): if spritecollide(s.rect) and ppcollide(sprite,s) and s != sprite: s.kill() crashed.append(s) else: for s in group.sprites(): if spritecollide(s.rect) and ppcollide(sprite,s) and s != sprite: crashed.append(s) return crashed def groupcollide_pp(groupa, groupb, dokilla, dokillb): """pygame.sprite.groupcollide_pp(groupa, groupb, dokilla, dokillb) -> dict collision detection between group and group by using pixel perfect collision detection given two groups, this will find the intersections between all sprites in each group. it returns a dictionary of all sprites in the first group that collide. the value for each item in the dictionary is a list of the sprites in the second group it collides with. the two dokill arguments control if the sprites from either group will be automatically removed from all groups.""" crashed = {} SC = spritecollide_pp if dokilla: for s in groupa.sprites(): c = SC(s, groupb, dokillb) if c: crashed[s] = c s.kill() else: for s in groupa.sprites(): c = SC(s, groupb, dokillb) if c: crashed[s] = c return crashed def spritecollideany_pp(sprite, group): """pygame.sprite.spritecollideany_pp(sprite, group) -> sprite finds any sprites that collide by using pixel perfect collision detection given a sprite and a group of sprites, this will return return any single sprite that collides with with the given sprite. If there are no collisions this returns None. if you don't need all the features of the spritecollide function, this function will be a bit quicker. all sprites must have a "hitmap" value, which is a 2d array that contains a value larger than zero for all pixels that can collide. the "hitmap" 2d array can be set by using pygame.surfarray.array_colorkey() or pygame.surfarray.array_alpha(). all sprites must have a "rect" value, which is a rectangle of the sprite area. """ spritecollide = pygame.sprite.rect.colliderect ppcollide = _pixelPerfectCollisionDetection for s in group.sprites(): if spritecollide(s.rect)and ppcollide(sprite,s) and s != sprite: return s return None