code
Quick and dirty spritesheet generator
Sun, 06/05/2011 - 19:15 — beerman2 posts in a day - man, I'm on fire!
Before I get started on my next game project, I thought I should address my woeful lack of tool support, since it tends to lead to a certain amount of corner-cutting and inefficiency. First up, spritesheets!
Sheep Snaggers 2's engine code had pretty decent support for rendering from single texture spritesheets, but since packing all my sprites together and working out the co-ordinates by hand was frankly a massive pain in the arse it only really got used for a couple of animations. I ended up actively resisting the idea of adding new animations because I couldn't be bothered with the donkey-work.
Since automating the donkey-work is pretty much what computers are for I thought I should address that by writing something to build the spritesheets for me - so this afternoon I finally pulled my finger out and the result was Spritepacker.py
A brief google suggested that if I wanted it to be really efficient I'd be dealing with a variation on a classic NP Hard problem, which is a bit more than I want to tackle on a lazy Sunday afternoon. A bit more googling found this example for packing lightmaps which looks like a nice simple interpretation of the First Fit Descending algorithm listed on Wikipedia. I tried it out with the Sheep Snaggers sprites and it seemed to do a pretty good job, anyway.
SpritePacker.py
The SpritePacker class takes a folder of sprites and generates the following:
- texture.png : single texture with all sprites in the folder packed into it
- texmap.py : python file containing a dictionary which maps the sprite names to texture co-ordinates
Example texmap.py file using various Sheep Snaggers sprites:
texmap = { 'kisschicklogo' : [0, 0, 500, 300], 'ming' : [501, 0, 202, 162], 'ship' : [704, 0, 32, 32], 'alien' : [737, 0, 32, 32], 'alien_shadow' : [770, 0, 32, 32], 'fighter' : [803, 0, 32, 32], 'sheep' : [836, 0, 32, 32], 'star' : [869, 0, 20, 20], 'star2' : [890, 0, 20, 20], 'bullet' : [911, 0, 4, 4], 'beerman_logo' : [0, 301, 477, 305], }
Full source code of SpritePacker.py:
#Beercave Gamews Beerware license v1.0 #This code is free to use for anything you want. #If you find it useful, I'd appreciate it if you buy me a beer :) #See http://www.beercave.co.uk/content/beerware for details #quick and dirty spritesheet packer #based on http://www.blackpawn.com/texts/lightmaps/default.html import os, pygame, glob from operator import itemgetter class SpritePacker(): def __init__(self, **kwargs): #defaults for input/output folders and texture size self.folder = kwargs.get("folder", "input") self.outfolder = kwargs.get("outfolder", "output") self.texturew = kwargs.get("texturew", 1024) self.textureh = kwargs.get("textureh", 1024) def Pack(self): patterns = ["*.gif", "*.png", "*.bmp"] #check for files in folder print os.path.abspath(self.folder) result = [] for pattern in patterns: pattern = os.path.join(self.folder, pattern) result.extend(glob.glob(pattern)) if len(result) == 0: print "No files in input folder!" return #build a new texture print "processing %d files:" % len(result) texture = pygame.Surface((self.texturew, self.textureh), pygame.SRCALPHA, 32) NodeTree = Node(texture, 0, 0, self.texturew, self.textureh) #sort our sprites by size sprites = [] for filepath in result: head, tail = os.path.split(filepath) filename = tail spritename, ext = os.path.splitext(tail) surface = pygame.image.load(filepath) x,y = surface.get_rect().size size = x*y #area should work as a proxy for size unless we get some badly out of proportion sprites sprites.append([filepath, spritename, size]) sprites.sort(key = itemgetter(2), reverse=True) print sprites for filepath, spritename, size in sprites: surface = pygame.image.load(filepath) print "Inserting %s - %s" %(spritename, surface) NodeTree.Insert(surface, spritename) #save the finished texture to outfolder outfile = os.path.join(self.outfolder, "texture.png") mapfile = os.path.join(self.outfolder, "texmap.py") pygame.image.save(texture, outfile) print "texture written to %s" % outfile #now dump the list of nodes map = NodeTree.Dump(True) + "}" with open(mapfile, 'w') as f: f.write(map) print "texture map written to %s" % mapfile class Node(): def __init__(self, texture, x, y, w, h): print "Creating node %d, %d, %d, %d" %(x,y,w,h) self.texture = texture self.bottom = None self.right = None self.rect = [x,y,w,h] self.imagerect = [0,0,0,0] self.image = None def Insert(self, surface, name): if self.image != None: print "node at %s contains %s. Adding to child node" % (self.rect, self.image) result = self.AddChild(surface, name) if not result: print "Could not add %s!" %name return result x,y,myw,myh = self.rect rect = surface.get_rect() print "image size : (%d,%d)" % rect.size w,h = rect.size #add some padding around sprites h += 1 w += 1 if w > myw or h > myh: print "Node too small!" return False print "adding to node at %s" % self.rect self.texture.blit(surface, (x,y)) self.image = name self.imagerect = [x,y,w-1,h-1] print self.imagerect #generate ChildNodes dw = myw - w dh = myh - h if dw > dh: self.right = Node(self.texture, x+w, y, dw, myh) self.bottom = Node(self.texture, x, y+h, w, dh) else: self.right = Node(self.texture, x+w, y, dw, h) self.bottom = Node(self.texture, x, y+h, myw, dh) return True def AddChild(self, surface, name): if not self.right.Insert(surface, name): return self.bottom.Insert(surface, name) return True #dump structure #{"filename" : [x,y,w,h], #} def Dump(self, first = False): #print "Dumping node" if self.image == None: return "" result = "'%s' : %s,\n" % (self.image, self.imagerect) if first: result = "texmap = {\n%s" %result result += self.right.Dump() result += self.bottom.Dump() return result packer = SpritePacker() packer.Pack()
This code is released under the Beerware license. It's completely free to use, but if you like it, why not buy me a beer? :)
*All donations are covered by the Beercave Games Beerware Pledge.
Pygame input wrapper
Sun, 06/05/2011 - 13:58 — beermanAs originally discussed in Keeping Control, I built a fairly handy wrapper class for handling player input in Sheep Snaggers 2. Now that the game is finished, I'm happy enough with the wrapper code to release it into the wild in the hope that someone else might find it useful.
Basically it abstracts out the various keyboard/mouse/controller functions by mapping them to a set of named controls. Each control can map to more than one input format so the core game logic doesn't have to know what controller you're using, it just asks the input module if e.g. a 'FIRE' event happened this frame.
You can find the source code along with a simple example here, or in the sidebar under 'Other Stuff'. The code is released under the Beerware license, so if you like it, why not buy me a pint?
Input.py
This is a wrapper class to simplify control handling in pygame when using multiple controller methods, as developed for Sheep Snaggers 2 and originally described here.
Rather than your game logic having to deal with all the controllers directly, it allows you to define named controls which can map to multiple controllers at once.
Simple example:
input = Input() while running: input.Poll() ship.x += input.GetControl('THRUST') ship.y += input.GetControl('CLIMB') if input.GetControl('FIRE') ship.fire() if input.GetControl('QUIT'): running = False render()
Full source code:
#Beercave Gamews Beerware license v1.0 #This code is free to use for anything you want. #If you find it useful, I'd appreciate it if you buy me a beer :) #See http://www.beercave.co.uk/content/beerware for details import pygame from pygame.locals import * #wrapper class for all input methods #maps controllers to named controls, eg 'FIRE', 'JUMP' etc #keeps all that nasty control handling code out of the game classes #call GetControl(CONTROL_NAME) to check the control state in game #clamp value to a min/max range def clamp(sourceval, minval, maxval): return max(min(sourceval, maxval), minval) class Input(): def __init__(self, **kwargs): #mappings for keydowns, key states and axis positions #format is controlname : [list of [source, (arguments...)]] #multiple inputs can all map to the same control #key/button press events (1,0) #identifies if a key/button was pressed this frame #['keyevent'], (KEY_ID,)] where KEY_ID is the pygame key id #['buttonevent', (STICK_ID, BUTTON_ID)] #['mouseevent', (BUTTON_ID)] #stickevent tracks whether the joystick was moved in a given #direction this frame. Threshold parameter allows for #filtering out of small movements #['stickevent', (STICK_ID,AXIS_ID,DIRECTION,THRESHOLD)] #key/button states (1,0) #identifies if a key/button is currently pressed #['keystate'], (KEY_ID,)] #['buttonstate', (STICK_ID, BUTTON_ID)] #axis mappings #track a joystick axis #can also map keyboard controls to a virtual axis so #the game code doesn't need to know whether it's #dealing with keys or a controller #['keyaxis'], (+KEY_ID, -KEY_ID)] #+KEY_ID is mapped to the positive axis direction #-KEY_ID is mapped to the negative axis direction #['stickaxis', (STICK_ID, AXIS_ID,)] #use '-stickaxis' to invert axis values self.controlMap = { 'QUIT' : [['keyevent', (K_ESCAPE,)]], 'PAUSE' : [['keyevent', (K_p,)]], 'START' : [ ['keyevent', (K_SPACE,)], ['keyevent', (K_RETURN,)], ['buttonevent', (0,0)] ], 'TEST' : [['keyevent', (K_SPACE,)]], 'OPTIONS' : [['keyevent', (K_o,)]], 'SCREENSHOT' : [['keyevent', (K_RCTRL,)]], 'THRUST' : [ ['keyaxis', (K_RIGHT, K_LEFT)], ['stickaxis', (0,0)] ], 'FIRE' : [['keyevent', (K_LCTRL,)], ['keyevent', (K_z,)], ['mouseevent', (1,)], ['mouseevent', (2,)], ['mouseevent', (3,)], ['buttonevent', (0,0)] ], 'FIRE2' : [ ['keyevent', (K_LSHIFT,)], ['keyevent', (K_c,)], ['buttonevent', (0,1)] ], 'TURN' : [ ['keyevent', (K_x,)], ['buttonevent', (0,2)] ], 'CLIMB' : [ ['keyaxis', (K_UP, K_DOWN)], ['-stickaxis', (0,1,)] ], 'STICK1' : [ ['-stickaxis', (0,0,)] ], 'STICK2' : [ ['-stickaxis', (0,1,)] ], 'UP' : [ ['keyevent', (K_UP, )], ['stickevent', (0,1,-1,.5)] ], 'DOWN' : [ ['keyevent', (K_DOWN, )], ['stickevent', (0,1,1,.5)] ], 'RIGHT' : [ ['keyevent', (K_RIGHT, )], ['stickevent', (0,0,1,.5)] ], 'LEFT' : [ ['keyevent', (K_LEFT, )], ['stickevent', (0,0,-1,.5)] ], 'FLINGX' : [['mousedir', (0,)]], 'FLINGY' : [['mousedir', (1,)]] } self.Mouse = [0,0] #state for all keys self.keys = pygame.key.get_pressed() #did event happen this frame for all keys self.keyevents = [0] *323 #all key events this frame self.keyspressed = [] #all mouse events this frame self.mouseevents = [0] * 4 #button press events this frame self.buttonevents = [[0]*20] #state of all joysticks in the system self.stickStates = [] self.InitSticks() self.gesturetime = 0 self.gesturex = 0 self.gesturey = 0 #update the input states #your game engine should call this every frame #for mouse control to work pass time elapsed this frame as dt def Poll(self, dt=0): keyevents = pygame.event.get(pygame.KEYDOWN) mouseevents = pygame.event.get(pygame.MOUSEBUTTONDOWN) stickevents = pygame.event.get(pygame.JOYAXISMOTION) buttonevents = pygame.event.get(pygame.JOYBUTTONDOWN) #clear the events list from the previous frame self.keyevents = [0] *323 self.mouseevents = [0] * 4 sticks = self.stickCount for i in range(0,sticks): state = self.stickStates[i] axes = state['numaxes'] buttons = state['numbuttons'] state['axisevents'] = [0] * axes state['buttonevents'] = [0] * buttons state['axes'] = [0.] * axes #move all pressed keys to an array - will save #iterating over the list every time we check #for a keypress for event in keyevents: self.keyevents[event.key] = True #all keyevents this frame #exposing this is useful for situations where we want to know #all the keys that were pressed - ie for handling text input self.keyspressed = keyevents self.keys = pygame.key.get_pressed() for event in mouseevents: self.mouseevents[event.button] = True for event in stickevents: self.stickStates[event.joy]['axisevents'][event.axis] = event.value for event in buttonevents: self.stickStates[event.joy]['buttonevents'][event.button] = True self.CheckMouse(dt) self.CheckSticks() pygame.event.clear() #check the system for attached joysticks and initialise def InitSticks(self): sticks = pygame.joystick.get_count() self.stickCount = sticks self.stickStates = [] for i in range(0,sticks): state = {} state['stick'] = stick =pygame.joystick.Joystick(i) stick.init() state['name'] = stick.get_name() state['numbuttons'] = buttons = stick.get_numbuttons() state['numaxes'] = axes = stick.get_numaxes() state['buttonstates'] = [0] * buttons state['axes'] = [0.] * axes state['axisevents'] = [0] * axes state['buttonevents'] = [0] * buttons self.stickStates.append(state) #poll all joysticks for state def CheckSticks(self): sticks = self.stickCount for i in range(0,sticks): state = self.stickStates[i] stick = state['stick'] buttons = state['numbuttons'] axes = state['numaxes'] for k in range(0, axes): state['axes'][k] = stick.get_axis(k) #poll mouse for state def CheckMouse(self,dt): #basic mouse gestures relx,rely = pygame.mouse.get_rel() #clear the axes so we don't report a movement every frame self.Mouse = [0,0] #check we've moved a decent amount if relx*relx+rely*rely > 10: self.gesturetime += dt self.gesturex += relx self.gesturey += rely elif self.gesturetime > 0: #a gesture just ended #change the velocity self.Mouse = [self.gesturex, -self.gesturey] self.gesturetime = 0 self.gesturex = 0 self.gesturey = 0 return #call this to get the state of a defined control def GetControl(self, name): mappings = self.controlMap.get(name, None) result = 0. for map in mappings: source, args = map if source == 'keyevent': key, = args result += self.GetKeyEvent(key) if source == 'buttonevent': stick,button = args result += self.GetButtonEvent(stick, button) if source == 'mouseevent': button, = args result += self.GetMouseEvent(button) if (source == 'keystate'): key, = args result += self.GetKeyState(key) if (source == 'keyaxis'): key1,key2 = args result += self.GetKeyAxis(key1, key2) if (source == 'stickaxis'): stick, axis = args val = self.GetStickAxis(stick, axis) result += val if (source == '-stickaxis'): stick, axis = args val = -self.GetStickAxis(stick, axis) result += val if (source == 'stickevent'): stick, axis, direction, threshold = args result += self.GetStickEvent(stick, axis, direction, threshold) if (source == 'mousedir'): axis, = args #mouse axis overrules alternatives if in use return self.GetMouseAxis(axis) return clamp(result, -1, 1) #functions to handle different control mappings def GetKeyEvent(self, key): return self.keyevents[key] def GetButtonEvent(self, stick, button): if self.stickCount <= stick: return False return self.stickStates[stick]['buttonevents'][button] def GetStickEvent(self, stick, axis, direction, threshold): if self.stickCount <= stick: return False value = self.stickStates[stick]['axisevents'][axis] return value * direction > threshold def GetMouseEvent(self, button): return self.mouseevents[button] def GetKeyState(self, key): return self.keys[key] def GetKeyAxis(self, key1, key2): keys = self.keys val1 = 1. if self.keys[key1] else 0. val2 = -1. if self.keys[key2] else 0. return val1 + val2 def GetStickAxis(self, stick, axis): if self.stickCount <= stick: return 0. state = self.stickStates[stick] pos = state['axes'][axis] return pos def GetMouseAxis(self, axis): return self.Mouse[axis]/1000.
This code is released under the Beerware license. It's completely free to use, but if you like it, why not buy me a beer? :)
*All donations are covered by the Beercave Games Beerware Pledge.
Beerware
Loosely based on Poul-Henning Kamp's Beerware license, the Beercave Games Beerware license is basically a way of releasing things for free while encouraging people to buy me more beer.
While the original Beerware license just asks you to buy the author a beer if you ever meet, I figure the chances of ever actually bumping into anyone who's used any of my stuff are slim enough to significantly limit the amount of free beer it's likely to net me.
