Source code for serge.visual

"""Classes to handle sprites and other visual items"""

import pygame
import os
import copy
import re

pygame.font.init()

import common
import serialize
import registry

class BadSprite(Exception): """The sprite was not loaded"""
class InvalidCell(Exception): """The sprite cell number was out of range"""
class BadScale(Exception): """An invalid scaling factor was used"""
class InvalidJustification(Exception): """The justification was not recognized"""
class NotAllFilesFound(Exception): """Didn't find all the files when looking for a multi cell from files"""
class BadFont(Exception): """The font was not loaded"""
class InvalidNameList(Exception): """The name list did not match the sprite given"""


log = common.getLogger('Visual')

[docs]class Store(registry.GeneralStore): """Stores sprites""" def _registerItem(self, name, path, w=1, h=1, framerate=0, running=False, rectangular=True, angle=0.0, zoom=1.0, loop=True, one_direction=False, convert_alpha=False): """Register a sprite""" # # Watch for special case h = -1 ... this is a multi cell if h == -1: return self.registerFromFiles(name, path, w, framerate, running, rectangular, angle, zoom, loop, one_direction, convert_alpha) # # Reality heighteck if zoom <= 0.0: raise BadScale('The zoom factor for sprite %s was not >= 0' % name) # # Special case of no filename - we want to specify this layer if path != '': # # Load the image and work out the dimensions based on the number of cells # wide and high try: image = pygame.image.load(self._resolveFilename(path)) except Exception, err: raise BadSprite('Failed to load sprite from "%s": %s' % (path, err)) # s = self._registerImage(name, image, w, h, framerate, running, rectangular, angle, zoom, loop, one_direction, convert_alpha) else: s = self.items[name] = None # # Remember the settings used to create the sprite self.raw_items.append([name, path, w, h, framerate, running, rectangular, angle, zoom, loop, one_direction, convert_alpha]) return s def _registerImage(self, name, image, w, h, framerate, running, rectangular, angle, zoom, loop, one_direction, convert_alpha): """Register an image""" # if zoom != 1.0: image = pygame.transform.smoothscale(image, (int(image.get_width()*zoom), int(image.get_height()*zoom))) if angle != 0.0: image = pygame.transform.rotate(image, angle) # width = image.get_width()/w height = image.get_height()/h # # Now load as a sprite sheet s = Sprite() # try: s.setImage(image, (width, height), framerate, running, convert_alpha=convert_alpha) except Exception, err: raise BadSprite('Failed to create sprite %s: %s' % (name, err)) # # Set properties s.framerate = framerate s.running = running s.rectangular = rectangular s.loop = loop s.one_direction = one_direction s.name = name s.convert_alpha = convert_alpha # self.items[name] = s # return s
[docs] def registerFromFiles(self, name, path, number, framerate=0, running=False, rectangular=True, angle=0.0, zoom=1.0, start=1, loop=True, one_direction=False, convert_alpha=False): """Register a multi cell sprite from a number of files The path should be a string with a single numerical substitution. We will pass the numbers 1..number to this substitution to find the names of the files. """ # # Generate a composite image - load the first one to find the size try: image = pygame.image.load(self._resolveFilename(path % start)) except Exception, err: raise BadSprite('Failed to load multi cell sprite from "%s": %s' % (path, err)) # # Get size width = image.get_width() height = image.get_height() # # Now create a canvas to load all the files onto canvas = pygame.Surface((number*width, height), pygame.SRCALPHA, 32) for i in range(1, number+1): try: image = pygame.image.load(self._resolveFilename(path % (i+start-1))) except Exception, err: raise NotAllFilesFound('Failed to load multi cell sprite from "%s": %s' % (path, err)) canvas.blit(image, ((i-1)*width, 0)) # # Ok, now create as if normal s = self._registerImage(name, canvas, number, 1, framerate, running, rectangular, angle, zoom, loop, one_direction, convert_alpha) # # Special case of h = -1 to trigger logic in the registerItem method when we are recovering from # a serialize self.raw_items.append([name, path, number, -1, framerate, running, rectangular, angle, zoom, loop, one_direction, convert_alpha]) # return s
[docs] def registerMultipleItems(self, names, path, w, h=1, rectangular=True, angle=0.0, zoom=1.0, one_direction=False, convert_alpha=False): """Register a number of sprites from a single image The image must be a horizontal row of sprites and you must provide a list of names the same size as the row of sprites. Each other sprites will be created. """ # # Reality check on the names if len(names) != w*h: raise InvalidNameList('Name list length, %d, does not match sprite width, %d, (%s)' % ( len(names), w, names)) if len(names) != len(set(names)): raise InvalidNameList('Name list contains duplcate names (%s)' % names) # # Split into cells using a temporary sprite temp = self._registerItem('__TEMP__', path, w, h) # # Now get the individual images out for idx, name in enumerate(names): self._registerImage(name, temp.cells[idx], 1, 1, 0, False, rectangular, angle, zoom, False, one_direction, convert_alpha) # Clean up self.removeItem('__TEMP__')
[docs] def registerItemsFromPattern(self, pattern, prefix='', w=1, h=1, framerate=0, running=False, rectangular=True, angle=0.0, zoom=1.0, loop=True, one_direction=False, convert_alpha=False): """Register all items matching a certain regular expression The items will be registered as the filename with the extension dropped off. You can optionally specify a prefix to be used to put in front of the registered name. """ log.info('Registering sprites from pattern "%s"' % pattern) items = [item for item in os.listdir(self._resolveFilename('')) if re.match(pattern, item)] names = [] for item in items: name = '%s%s' % (prefix, os.path.splitext(item)[0]) names.append(name) log.debug('Found sprite "%s" - registered as "%s"' % (item, name)) self.registerItem(name, item, w, h, framerate, running, rectangular, angle, zoom, loop, one_direction, convert_alpha) return names
Register = Store() # Legacy name Sprites = Register
[docs]class Drawing(object): """Represents something to draw on the screen""" def __init__(self): """Initialise the drawing""" self._alpha = 1.0 self.width = 0.0 self.height = 0.0 self.zoom = 1.0 self.angle = 0.0 self.horizontal_flip = False self.vertical_flip = False self.convert_alpha = False
[docs] def setScale(self, scale): """Set the scaling to a certain factor""" self.scaleBy(scale/self.zoom)
[docs] def scaleBy(self, factor): """Scale the image by a factor""" raise NotImplementedError('scaleBy not implemented on %s' % self)
[docs] def setAngle(self, angle): """Set the rotation to a certain angle""" raise NotImplementedError('setAngle not implemented on %s' % self)
[docs] def getAngle(self): """Return the current angle""" return self.angle
[docs] def rotateBy(self, angle): """Rotate by a certain amount""" self.setAngle(self.getAngle()+angle)
[docs] def flipHorizontal(self): """Flip the drawing horizontally""" raise NotImplementedError('flipHorizontal not implemented on %s' % self)
[docs] def setHorizontalFlip(self, flip): """Set the horizontal flip state""" if self.horizontal_flip != flip: self.flipHorizontal()
[docs] def flipVertical(self): """Flip the drawing vertically""" raise NotImplementedError('flipVertical not implemented on %s' % self)
[docs] def setVerticalFlip(self, flip): """Set the vertical flip state""" if self.vertical_flip != flip: self.flipVertical()
### Rendering ###
[docs] def renderTo(self, milliseconds, surface, (x, y)): """Render to a surface""" raise NotImplementedError('renderTo not implemented on %s' % self)
[docs] def setAlpha(self, alpha): """Set the overall alpha""" raise NotImplementedError('setAlpha not implemented on %s' % self)
def _setAlphaForSurface(self, surface, alpha): """Set the alpha for a surface""" # # (Not quite sure about all of this!) # Try to set the alpha and then if this fails drop back to slow method surface.set_alpha(alpha*255) if surface.get_at((0,0))[3] == alpha*255: return # size = surface.get_size() for y in xrange(size[1]): for x in xrange(size[0]): r,g,b,a = surface.get_at((x,y)) surface.set_at((x,y),(r,g,b,int(a*alpha))) return surface @property def alpha(self): return self._alpha @alpha.setter def alpha(self, alpha): self.setAlpha(alpha) ### Copying ###
[docs] def getCopy(self): """Return a copy""" return copy.copy(self)
[docs] def setSize(self, width, height): """Set the size of the drawing directly""" raise NotImplementedError('setSize not implemented on %s' % self)
[docs]class SurfaceDrawing(Drawing): """A visual object that renders to a surface. You can create an instance of this class and then write to its surface or use this as a base class for your own class that will write the surface. """ def __init__(self, width, height, pixel_format=pygame.SRCALPHA): """Initialise the surface""" super(SurfaceDrawing, self).__init__() self.width = width self.height = height self.pixel_format = pixel_format self.clearSurface()
[docs] def getSurface(self): """Return our surface""" return self.surface
[docs] def clearSurface(self): """Clear the surface""" return self.setSurface(pygame.Surface((self.width, self.height), self.pixel_format, 32))
[docs] def setSurface(self, surface): """Update our surface""" self.raw_image = self.surface = surface self._updateDimensions() return self.surface
[docs] def renderTo(self, milliseconds, surface, (x, y)): """Render to a surface""" surface.blit(self.getSurface(), (x, y))
[docs] def scaleBy(self, factor): """Scale the image by a factor""" if factor <= 0: raise BadScale('Scaling factor must be >= 0') self.zoom *= factor img = self.raw_image self.surface = pygame.transform.smoothscale(img, (int(img.get_width()*self.zoom), int(img.get_height()*self.zoom))) self._updateDimensions()
[docs] def setAngle(self, angle): """Change the angle - returning the amount by which the sprite has shifted""" self.angle = angle self.surface = pygame.transform.rotate(self.raw_image, self.angle) self._updateDimensions()
def _updateDimensions(self): """Update the dimensions of the drawing""" self.width = self.surface.get_width() self.height = self.surface.get_height() self.cx = self.width/2 self.cy = self.height/2 self.radius = max(self.width, self.height)/2
[docs] def setSize(self, width, height): """Set the size of the drawing directly""" img = self.raw_image self.surface = pygame.transform.smoothscale(img, int(width), int(height)) self._updateDimensions()
[docs]class Sprite(Drawing): """An object that gets drawn on the screen""" # # This is the function used to rotate the sprite. You can change this to "rotate" # to get non-filtered rotations smooth_rotate = lambda self, img, angle, scale : pygame.transform.rotozoom(img, angle, scale) base_rotate = lambda self, img, angle, scale : pygame.transform.rotate(img, angle) # rotate = smooth_rotate # Set the following to True to allow caching of rotations of a sprite to the given number of degrees cache_rotations = False cache_granularity = 1.0
[docs] def getCopy(self): """Return a copy of this sprite""" new = self.__class__() new.setImage(self.raw_image, (self.width, self.height), self.framerate, self.running, self.loop, self.one_direction, self.convert_alpha) return new
[docs] def setAlpha(self, alpha): """Set the overall alpha""" self.setCells() for idx, cell in enumerate(self.cells): self._setAlphaForSurface(cell, alpha) self._alpha = alpha
[docs] def setImage(self, image, (width, height), framerate=0, running=False, loop=True, one_direction=False, convert_alpha=False): """Set the image of this sprite""" # # Store cells of the image for speed - use of the cache is determined by the # cache_rotations property self._cell_cache = {} # # Store raw image self.raw_image = image if not convert_alpha else image.convert_alpha() # self.width = self.raw_width = width self.height = self.raw_height = height self.cx = self.raw_cx = width/2 self.cy = self.raw_cy = height/2 # self.framerate = framerate self.frame_time = 0 if framerate == 0 else 1000.0/framerate self.running = running self.loop = loop self.one_direction = one_direction self.last_time = 0 self.direction = 1 self.zoom = 1.0 self.angle = 0.0 self.fixed_size = None # self.setCells() self.current_cell = 0 self._alpha = 1.0
### Cells ###
[docs] def setCells(self): """Create the cells for the animation of this sprite""" self.cells = [] # # Have we cached this set of cells if self.cache_rotations: granular_angle = (self.angle//self.cache_granularity)*self.cache_granularity try: self.cells = self._cell_cache[granular_angle] except KeyError: # Oh, well - not in the cache after all pass # # Make cells if not self.cells: rows = self.raw_image.get_height()/self.raw_height cols = self.raw_image.get_width()/self.raw_width # raw_image = self.raw_image.copy() for row in range(rows): for col in range(cols): r = pygame.Rect(col*self.raw_width, row*self.raw_height, self.raw_width, self.raw_height) img = raw_image.subsurface(r) if self.horizontal_flip or self.vertical_flip: img = pygame.transform.flip(img, self.horizontal_flip, self.vertical_flip) if self.zoom != 1.0: img = pygame.transform.smoothscale(img, (int(img.get_width()*self.zoom), int(img.get_height()*self.zoom))) elif self.fixed_size: img = pygame.transform.smoothscale(img, self.fixed_size) if self.angle != 0.0: img = self.rotate(img, self.angle, 1.0) self.cells.append(img) # if self.cells: self.width = self.cells[0].get_width() self.height = self.cells[0].get_height() self.cx = self.width/2 self.cy = self.height/2 self.radius = max(self.width, self.height)/2 # if self.cache_rotations: self._cell_cache[granular_angle] = self.cells
[docs] def setSize(self, width, height): """Set the size of the drawing directly""" self.fixed_size = (int(width), int(height)) self.zoom = 1.0 self.setCells()
[docs] def setCell(self, number): """Set the current cell number""" if 0 <= number < len(self.cells): self.current_cell = number else: raise InvalidCell('Cell number %d is out of range (%d) for this sprite (%s)' % ( number, self.getNumberOfCells(), self))
[docs] def getCell(self): """Return the current cell number""" return self.current_cell
[docs] def getNumberOfCells(self): """Return the number of animation cells""" return len(self.cells)
### Deformations ###
[docs] def scaleBy(self, factor): """Scale the image by a factor""" if factor <= 0: raise BadScale('Scaling factor must be >= 0') self.zoom *= factor self.setCells()
[docs] def setAngle(self, angle): """Change the angle - returning the amount by which the sprite has shifted""" self.angle = angle self.setCells()
[docs] def flipHorizontal(self): """Flip the drawing horizontally""" self.horizontal_flip = not self.horizontal_flip self.setCells()
[docs] def flipVertical(self): """Flip the drawing vertically""" self.vertical_flip = not self.vertical_flip self.setCells()
### Rendering ###
[docs] def renderTo(self, milliseconds, surface, (x, y)): """Render to a surface""" # # Update current frame if self.framerate and self.running: self.last_time += milliseconds # # Are we moving on to another frame move_frames, remainder = divmod(self.last_time, self.frame_time) nframes = len(self.cells) # # Are we moving at all? if move_frames >= 1.0: self.last_time = remainder # # Move on in cells new_virtual_cell = self.current_cell + self.direction*int(move_frames) # # Map back to the real space self.current_cell, hit_end = self._mapVirtualToRealCell(new_virtual_cell) # # Watch for hitting the end if hit_end: if not self.loop: self.running = False elif not self.one_direction: self.direction *= -1 # # Draw to the surface surface.blit(self.cells[self.current_cell], (x, y))
def _mapVirtualToRealCell(self, n): """Map a virtual cell number to a real one Returns real_cell, hit_end """ nc = len(self.cells) hit_end = False if not self.loop: # # No looping - over the ends moves to the end if n <= 0: return 0, True elif n >= nc-1: return nc-1, True else: return n, False else: # # Looping if self.one_direction: # # One direction, going past the ends maps back to the begining. We hit the end if we have an odd number # of multiples of the number of cells rn = n % nc return rn, ((n // nc) % 2 == 1) else: # # Going in both directions, map onto twice the cell space (0 reflects back so # use an absolute). We hit the end if we have an odd number # of multiples of the number of cells rn = 0 if nc == 1 else n % (2*(nc-1)) if rn < 0: rn = abs(rn) hit_end = ((rn // nc) % 2 == 0) # # Watch for going beyond the end. if rn >= nc: return 2*nc-rn-2, True else: return rn, hit_end
[docs] def resetAnimation(self, running): """Reset the animation to the beginning""" self.setCell(0) self.direction = 1 self.running = running
[docs] def getSurface(self): """Return the current surface""" return self.cells[self.current_cell]
[docs]class Text(Drawing): """Some text to display""" def __init__(self, text, colour, font_name='DEFAULT', font_size=12, justify='center', fixed_char_width=None): """Initialise the text""" super(Text, self).__init__() # # Hack - see below self._actor_parent = None # self.colour = colour if len(colour) == 4 else (colour + (255,)) self.text = text self.font_size = font_size self.font_name = font_name self.angle = 0.0 self.font = Fonts.getFont(self.font_name, self.font_size) self.fixed_char_width = fixed_char_width # self.setText(text) self.justify = justify self.colour_key = (127, 127, 127)
[docs] def setJustify(self, justify): """Set the justification""" setting = justify.lower() if setting not in ('left', 'center'): raise InvalidJustification('Justification not recognized (%s)' % justify) self.justify = setting
[docs] def setText(self, text): """Set our text""" # # Break into lines and then render each one if text == '': text = ' ' lines = text.splitlines() # # Find out how wide to make our surface width = 0 for line in lines: if self.fixed_char_width: width = max(len(line) * self.fixed_char_width, width) else: width = max(self.font.size(line)[0], width) height = self.font.get_height() # # Now make a surface self.surface = self._getNewSurface((width, height*len(lines))) # # And write all our text for idx, line in enumerate(lines): self.surface.blit(self._getLineRender(line), (0, height*idx)) # if self.angle != 0: self.surface = pygame.transform.rotate(self.surface, self.angle) self.width = self.surface.get_width() self.height = self.surface.get_height() # # Hack to get the actor to spot that we updated our width if self._actor_parent: self._actor_parent.resizeTo(self.width, self.height) # self.text = text
def _getNewSurface(self, (width, height)): """Return a new surface""" if not self.convert_alpha: surface = pygame.Surface((width, height), pygame.SRCALPHA, 32) else: surface = pygame.Surface((width, height)) surface.fill(self.colour_key) surface.set_colorkey(self.colour_key) # return surface def _getLineRender(self, line): """Return a new surface with a line rendered""" if not self.fixed_char_width: return self.font.render(line, True, self.colour) else: surface = self._getNewSurface((len(line) * self.fixed_char_width, self.font.get_height())) for i, char in enumerate(line): char_surface = self.font.render(char, True, self.colour) surface.blit(char_surface, ((i + 0.5) * self.fixed_char_width - char_surface.get_width() / 2, 0)) return surface
[docs] def setColour(self, colour): """Set the colour""" self.colour = colour self.setText(self.text)
[docs] def setFontSize(self, font_size): """Set our font size""" self.font_size = int(font_size) self.font = Fonts.getFont(self.font_name, int(self.font_size)) self.setText(self.text)
[docs] def setAlpha(self, alpha): """Set our alpha""" self.setText(self.text) if self.convert_alpha: self.surface.set_alpha(alpha * 255) else: self._setAlphaForSurface(self.surface, alpha) self._alpha = alpha
[docs] def renderTo(self, milliseconds, surface, (x, y)): """Render to a surface""" if self.justify == 'left': surface.blit(self.surface, (x+self.width/2, y+self.height/2)) else: surface.blit(self.surface, (x, y))
[docs] def setAngle(self, angle): """Rotate our sprite by a certain angle""" self.angle = angle self.setText(self.text)
[docs] def scaleBy(self, scale): """Scale our sprite by a certain certain amount""" self.setFontSize(self.font_size*scale)
[docs] def convertAlpha(self, color_key=(127, 127, 127)): """Convert this text to an surface alpha""" self.convert_alpha = True self.setText(self.text)
### Fonts ###
[docs]class FontStore(registry.GeneralStore): """A store for fonts""" def __init__(self): """Initialise the font registry""" super(FontStore, self).__init__() # self._font_cache = {}
[docs] def registerItem(self, name, path): """Register a font""" # # Special case for DEFAULT - we allow multiple registrations of # this one if name == 'DEFAULT' and name in self.items: self.removeItem('DEFAULT') super(FontStore, self).registerItem(name, path)
def _registerItem(self, name, path): """Register the font""" # # Try to load a font - we are going to discard this but it is a good # check to see if we will be able to create a font later real_path = path try: _ = pygame.font.Font(real_path, 12) except Exception, err: # # Try with actual path real_path = self._resolveFilename(path) try: _ = pygame.font.Font(real_path, 12) except Exception, err: raise BadFont('Failed to load font from "%s": %s' % (path, err)) # # Remember the settings used to create the sound self.raw_items.append([name, real_path]) self.items[name] = real_path return real_path
[docs] def clearItems(self): """Clear the items Leaves the DEFAULT item in there if it is there. """ try: default = self.getItem('DEFAULT') except registry.UnknownItem: default = None super(FontStore, self).clearItems() if default: self.registerItem('DEFAULT', default)
[docs] def getFont(self, name, size): """Return a font This method implements a caching scheme. There seem to be problems on OSX repeatedly creating fonts and so we are caching here. This doesn't seem to be needed on other platforms but we use it anyway as it should give a slight performance bonus. """ try: return self._font_cache[name, size] except KeyError: return self._font_cache.setdefault( (name, size), pygame.font.Font(self.getItem(name), size) )
Fonts = FontStore() Fonts.registerItem('DEFAULT', pygame.font.get_default_font())