How-To: Easily Implement Sprite Sheets In Your PyGames
Working with sprites is almost a given with any implementation of the pygame library, for python3. There are several ways you can technically load and work with sprites, almost certainly one ends up not wanting to manage all those art asset files, that might clutter up a game development process. This is why the concept of sprite sheets are used in game design in the first place, along with other benefits.
Now what do you do? You are aware of the pygame.sprite.Sprite class, which is in itself either used to create ‘x’ number of sprites drawn upon the display surface, or it if subclassed, given greater flexibility for a game object, like the concept of a Player(pygame.sprite.Sprite) class. Having this base knowledge, then you need to able to manage such sprites as the Player, Enemies, etc images and animations as the game progresses. This is where using the concept of a sprite sheet comes into play.
Lets set the stage, by using an example in which you can download the full source for, located here: PyGames-Mars github repo.
What you see in the above video is a sample pygame instance called PyGames-Mars. In it you see the ‘player’, ROVI the Mar Rover, moving around as native martians, the dancing sprites, dance about. Each of these derive from the pygame.sprite.Sprite class, from the pygame lib in python3.
How did we get to all that movement and animation?
I — Getting Started:
1. First you need a base for the game to build from. You can find the different instances of PyGames-Mars’ located at: github repo. The 03_Animation instance, which is the instance shown in the video above, is a clone of 02_PopupMenu, except with the needed elements to enable animation added.
2. Next, navigate to the root folder of the 03 game instance, and click to open up the player.py file. In this file you will see that is indeed a subclass of the class pygame.sprite.Sprite.
class Player(pygame.sprite.Sprite):
def __init__(self,pos,groups,obstacle_sprites):
super().__init__(groups)
3. Moving along, you will notice the __init__(…), and contained within it is a self.sprite_sheet variable call. This call sets its value to: ss.SpriteSheet(…).
# Player Animtations & Animation Frames:
self.sprite_sheet = ss.SpriteSheet(loc=GRAPHICS+PLAYER,file=PLAYERMAP)
self.animation_images = self.sprite_sheet.sprites()
4. Further, if you look at the entire __init__(…), from that point forward there are several variables of the Player class that are used to build out the animation of the Player sprite in the game. For example the self.animation_images = self.sprite_sheet.sprites() is another important aspect, as well as the selfimage = self.animation_images[DOWN+IDLE][0].
def __init__(self,pos,groups,obstacle_sprites):
super().__init__(groups)
# Player Animtations & Animation Frames:
self.sprite_sheet = ss.SpriteSheet(loc=GRAPHICS+PLAYER,file=PLAYERMAP)
self.animation_images = self.sprite_sheet.sprites()
self.frame_index = 0
self.animation_speed = self.sprite_sheet.animation_speed()
self.image = self.animation_images[DOWN+IDLE][0]
self.rect = self.image.get_rect(topleft = pos)
self.direction = pygame.math.Vector2(self.rect.x,self.rect.y)
self.speed = 2
self.obstacle_sprites = obstacle_sprites
self.last_sample = INIT_LASTSAMPLE
self.nav_direction = NavDirection.South
self.status = DOWN
self.lifebar = 100
# Minerals Found dict:
self.mine_cords=dict()
5. Now that you are in the Player class, you see there is a custom class called SpriteSheet which is imported as ss. This is where the magic takes place for generating the different animation images used in the movement of the Player sprite, from a single sprite sheet image file.
II — SpriteSheet class breakdown:
1. The SpriteSheet class is found in the root of the 03 game instance, in the python file named spritesheet.py. In this class you will see its __init__(…) and how it builds out.
# Sprite Sheet Class:
class SpriteSheet(object):
def __init__(self,loc,file) -> None:
self.__loc__ = loc
self.__file__ = file
self.__title__ = ''
self.__sprite_sheet__ = ''
self.__animation_speed__ = 0
self.__dims__ = (0,0)
self.__keys__ = list()
self.__suffkeys__ = list()
self.__vmlines__ = dict()
self.__subby__ = dict()
self.__init_map__()
self.__sprite_sheet_image__ = self.__init_spritesheet_image()
self.__sprites__ = self.__generate_sprites__()
def __init_spritesheet_image(self) -> pygame.Surface:
return pygame.image.load(self.__loc__+self.sprite_sheet()).convert_alpha()
def __init_map__(self):
# Init Variable setup:
ssmap = self.read_spritesheet_map(self.__loc__+self.__file__)
ssmapd = json.load(ssmap).get(SPRITESHEETMAP)
# Operate on JSON file properties:
self.title(ssmapd.get(TITLE))
self.sprite_sheet(ssmapd.get(SPRITE_SHEET))
self.animation_speed(ssmapd.get(ANIMATION_SPEED))
self.dimensions(ssmapd.get(DIMS))
self.keys(ssmapd.get(KEYS))
self.suffkeys(ssmapd.get(SUFF_KEYS))
self.valuemap_lines(ssmapd.get(VALUES_MAP))
for x in self.valuemap_lines():
self.__subby__[x]=SpriteDetails.new(self.valuemap_lines()[x].get(SPRITE_DETAILS))
def __generate_sprites__(self):
ret = dict()
for k in self.__subby__:
ta = list()
sd = self.__subby__[k]
r = sd.row()
c = sd.count()
s = sd.start()
i = 0
while i < c:
tup = (
(s)+(self.dimensions()[0]*i),
(self.dimensions()[1]*r)
)+self.dimensions()
ta.append(
self.__sprite_sheet_image__.subsurface(tup)
)
i += 1
ret[k]=ta
return ret
def sprites(self) -> dict:
return self.__sprites__
def title(self,s=None) -> str:
if s != None:
self.__title__ = s
return self.__title__
def sprite_sheet(self,s=None) -> str:
if s != None:
self.__sprite_sheet__ = s
return self.__sprite_sheet__
def animation_speed(self,s=None) -> float:
if s != None:
self.__animation_speed__ = s
return self.__animation_speed__
def dimensions(self,s=None) -> tuple:
if s != None:
self.__dims__ = s
return tuple(self.__dims__)
def keys(self,s=None) -> list:
if s != None:
self.__keys__ = s
return self.__keys__
def suffkeys(self,s=None) -> list:
if s != None:
self.__suffkeys__ = s
return self.__suffkeys__
def valuemap_lines(self,s=None) -> dict:
if s != None:
self.__vmlines__ = s
return self.__vmlines__
def read_spritesheet_map(self,file):
f = io.FileIO(file,'r+')
return f
2. From the above you can see that two variables are presented to the __init__(…) of the SpriteSheet class. The variables ‘loc’ and ‘file’, represent the location and sprite sheet json configuration file to load. You can see a call in the __init__(…) for __init_map(), __init_spritesheet_image(), and finally, just below that the next line for __generate_sprites__().
self.__init_map__()
self.__sprite_sheet_image__ = self.__init_spritesheet_image()
self.__sprites__ = self.__generate_sprites__()
Within the context of these three lines of code, is the main operation for loading a targeted sprite sheet image file, and how it is to be split for individual sprite animation frame usage.
3. The __init_map__(self) code using the location and file values passed into the initialization fo the SpriteSheet class instance, to load a specific schema of JSON, from the target sprite sheet JSON file.
{
"SpriteSheetMap":{
"Title": "Rover Sprite Sheet, 64 pixels",
"Sprite_Sheet": "rover.png",
"Dimensions": [64,64],
"Animation_Speed": 0.13,
"Value_Keys": ["down","left","right","up"],
"Suffix_Keys":["_idle"],
"Values_Map":{
"down_idle":{
"sprite_details":{
"row": 0,
"count": 1,
"start": 0
}
},
"down":{
"sprite_details":{
"row": 1,
"count": 4,
"start": 0
}
},
"left_idle":{
"sprite_details":{
"row": 2,
"count": 1,
"start": 0
}
},
"left":{
"sprite_details":{
"row": 2,
"count": 4,
"start": 64
}
},
"right_idle":{
"sprite_details":{
"row": 3,
"count": 1,
"start": 0
}
},
"right":{
"sprite_details":{
"row": 3,
"count": 4,
"start": 64
}
},
"up_idle":{
"sprite_details":{
"row": 4,
"count": 1,
"start": 256
}
},
"up":{
"sprite_details":{
"row": 4,
"count": 4,
"start": 0
}
}
},
"Notes": "For the Values_Map, each entry gives 0 based row, count of sprites in row, start='topleft x' of first sprite."
}
}
The python code in __init_map__(self) uses the json python lib load the contents of the json file into dict() objects. This is broken down and schema managed through the use of constants like TITLE, SPRITE_SHEET, etc.
You will notice that, for example, the SPRITE_SHEET schema value is what ties the JSON sprite sheet configuration file to the desired sprite sheet image file that will be loaded and sub-surfaced to generate sprite frame images for use in game.
Finally you will also notice, within the __init_map__(self), the self.valuemap_lines(…) call, and specifically the use of another custom class contained within the spritesheet.py file called SpriteDetails. This class was created to help contain a subset of information, loaded about each sprite frame type break down, from the JSON file being consumed.
4. The __init_spritesheet_image(self) call is now a simple one to make, because of the work performed in the __init_map__(self) function. Now that you have the location and sprite sheet image file to load, that is what the single line of code in __init_spritesheet_image(self) does.
return pygame.image.load(self.__loc__+self.sprite_sheet()).convert_alpha()
An import side note to make about the python code contained within the init_spritesheet_image call is the .convert_alpha() call. This is important step, when performing a pygame.image.load(…) call, relating to overall performance for working with sprite images.
5. Finally you arrive at the __generate_sprites__(self) function call. Contained within the scope of this bound function, you find a dict() object being created. This operates off of a SpriteSheet class instanct variable called __subby__. This contains the elements from the JSON sprite sheet configuration file that tells the python code how to sub-surface the loaded image, and further how to classify those sub-surfaced images.
def __generate_sprites__(self):
ret = dict()
for k in self.__subby__:
ta = list()
sd = self.__subby__[k]
r = sd.row()
c = sd.count()
s = sd.start()
i = 0
while i < c:
tup = (
(s)+(self.dimensions()[0]*i),
(self.dimensions()[1]*r)
)+self.dimensions()
ta.append(
self.__sprite_sheet_image__.subsurface(tup)
)
i += 1
ret[k]=ta
return ret
There is a specific line of code in the above image that uses the sprite dimensions, row, count and start position that allows for correct sub-surfaces to be generated from the loaded image. This is the call to the self.__sprite_sheet_image__.subsurface(tup) that creates the sprite image frame orders, for the sprite classifications desired. For example the contents of rover.json file, SpriteSheetMap.Values_Map.down_idle & *.down:
"down_idle":{
"sprite_details":{
"row": 0,
"count": 1,
"start": 0
}
},
"down":{
"sprite_details":{
"row": 1,
"count": 4,
"start": 0
}
}
When you look back at the Player class __init__(…) code, and see now the sprite_sheet and animation_images, you can understand now how the use of an expected json formatted file is used with the custom SpriteSheet class found in the spritesheet.py file, which can easily be used in almost any python3/pygame project you may have.
III — Enemy sprite class breakdown:
1. Now that you have a good understanding of the SpriteSheet class for building out the Player sprite class of the PyGame-Mars 03 game instance, you have what is needed to breakdown the animation achieved for each of the “enemy” alien natives of Mars.
In order for you to finalize your understanding of the animation, and to see how the SpriteSheet class is implemented for the ‘alien’ sprites, open up the enemy.py python file found in the root of the project.
import pygame
import spritesheet as ss
from settings import *
class Enemy(pygame.sprite.Sprite):
def __init__(self,pos,groups,obstacle_sprites,enemy_type):
super().__init__(groups)
self.obstacle_sprites=obstacle_sprites
# Enemy Animation & SpriteSheet
# NOTE: builds out from settings.GRAPHICS + settings.ENEMIES constant folder|string values
# Further passing in enemy_type, assumes that naming convention employed for naming JSON(ie: EXT)
# files. For example, resolves to:
# GRAPHICS+ENEMIES+enemy_type+EXT
# -
# GRAPHICS='./graphics'
# ENEMIES='/enemies/'
# enemy_type='alientblob_blue'
# EXT='.json'
# -
# './graphics/enemies/alienblob_blue.json'
# -
# For loading SpriteSheet()
self.sprite_sheet = ss.SpriteSheet(loc=GRAPHICS+ENEMIES,file=enemy_type+EXT)
self.enemy_type = enemy_type
self.animation_images = self.sprite_sheet.sprites()
self.frame_index = 0
self.animation_speed = self.sprite_sheet.animation_speed()
self.animation_framelen = 0
self.image = self.animation_images[DOWN][0]
self.rect = self.image.get_rect(topleft = pos)
self.status = DOWN
self.vector = pygame.math.Vector2(self.rect.x,self.rect.y)
def animate(self):
animation = self.animation_images[self.status]
self.animation_framelen = len(animation)
# loop over the frame index
self.frame_index += self.animation_speed
if self.frame_index >= len(animation):
self.frame_index = 0
animation = animation[int(self.frame_index)]
# set the image
self.image = animation
def update(self):
self.animate()
What you see in the above code snippet is the __init__(…), animate(…) and update(…) bound functions of the Enemy(pygame.sprite.Sprite) class. The init, similar to the Player class, calls for the generation of the SpriteSheet class instance, so the <enemy_type> (ie: alienblob_yellow.json) can correctly be loaded and have the correct sprite(s) generate for use to animate the specific enemy sprite.
The animate(self) function, when called, uses the status of the enemy to pick what classification fo sprite frames to load. (ie: “down”), while operating within the parameters of the rate of change for the enemy sprite, as to which frame index of what image will be loaded, when the animate(self) function is called upon.
The final function of update(self) calls upon the animate(self), when the game loop / level.run(…) — calls the visible_sprites .draw(…) and .update(…) functions. Thus updating the frames, based on the animation_speed value.
{
"SpriteSheetMap":{
"Title": "Alien Blob Yellow Sprite Sheet, 64 pixels",
"Sprite_Sheet": "alienblob_yellow.png",
"Animation_Speed": 0.17,
"Dimensions": [64,64],
"Value_Keys": ["down"],
"Suffix_Keys":[],
"Values_Map":{
"down":{
"sprite_details":{
"row": 0,
"count": 8,
"start": 0
}
}
},
"Notes": "Alien Blob Yellow for PyGames-Mars tutorial game instances."
}
}
IV — Conclusion
In this article we covered how you can use the concept of a sprite sheet to help manage your sprite assets, and use them to easily animate different types of sprites, while taking advantage of proving performance enhancement based concepts for image loading and sub-surface generation.
You should be able to easily download the full source of the entire game, and all its instances at the github repo: PyGames-Mars
For the custom SpriteSheet python class and example usage, use the PyGames-Mars/03_Animation instance. Finally you can easily see how to use the SpriteSheet class, and json configuration file concepts by using debugging the test_spritesheet_map.py file found in the root of the 03_Animation project.
I hope you have found this helpful! Thank you for your time!
J. Brandon George
twitter: @PyFryDay
email: darth.data410@gmail.com
github: DarthData410
GitHub Project Source: https://github.com/DarthData410/PyGames-Mars