code

Quick and dirty spritesheet generator

2 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 output

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

As 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.