I just wanted to put this out here and see what people think and potential improvements I could make. I've been learning spritesheets in pygame as I was really scared to use them as I thought they were complicated. I've made my own class which can load a single or multiple sprites. I want to use this in the new game I'm making. But, before that, I want to learn a few more important things, but this is a great start I think.
I have also provided a minimal reproducible example that you can use by copying and pasting everything in this post.
ONLY REQUIREMENTS: pygame-ce==2.5.2
SpriteSheet Class
import pygame as pg
class SpriteSheet:
def __init__(self, filePath):
self.sheet = pg.image.load(filePath)
def getSpriteImage(
self,
frame: int,
width: int,
height: int,
scaleFactor: int = 1,
transparent: bool = True,
row: int = 0,
) -> pg.Surface:
# Creating a surface with giving dimensions
image = pg.Surface((width, height)).convert_alpha()
# Setting x coord for a selected frame
x = frame * width
# Setting y coord for a selected row
y = row * height
# Drawing the selected frame onto a surface
image.blit(self.sheet, (0, 0), (x, y, width, height))
# Rescaling the sprite
image = pg.transform.scale(image, (width * scaleFactor, height * scaleFactor))
# Removing the background of the sprite
if transparent:
image.set_colorkey((0, 0, 0))
return image
def getSpriteImages(
self, spriteSheetMetadata: str, scaleFactor: int = 1, transparent: bool = True
) -> dict:
# Returning sprite dict
sprites = {}
# Setting dimensions of all sprites on that spritesheet
sprite_width = spriteSheetMetadata["spriteMetadata"]["spriteWidth"]
sprite_height = spriteSheetMetadata["spriteMetadata"]["spriteHeight"]
# Row index to get multiple rows if multiple rows exist in the same spritesheet
rowIndex = 0
# Load sprites from each row
for spriteName, numFrames in spriteSheetMetadata["rows"].items():
spriteFrames = []
# Looping over each frame and getting the sprite
for frame in range(numFrames):
try:
sprite = self.getSpriteImage(
frame,
sprite_width,
sprite_height,
scaleFactor=scaleFactor,
transparent=transparent,
row=rowIndex,
)
spriteFrames.append(sprite)
# Debug for loaded success
print(f"Loaded sprite: {spriteName}, Frame {frame}")
except Exception as e:
# Debug for error occurd during loading sprite
print(f"Error loading sprite: {spriteName}, Frame {frame} - {e}")
# Store loaded sprites
sprites[spriteName] = spriteFrames
# Move the the next row
rowIndex += 1
return sprites
Spritesheet Metadata JSON
{
"SpriteSheets": {
"SpriteSheetExample_Player": {
"spriteMetadata": {
"spriteWidth": 96,
"spriteHeight": 96
},
"rows": {
"player_idle_right": 5,
"player_idle_left": 5,
"player_walk_left": 8,
"player_walk_right": 8,
"player_shoot_left": 5,
"player_shoot_right": 5,
"player_jump_left": 8,
"player_jump_right": 8
}
},
"SpriteSheetExample": {
"spriteMetadata": {
"spriteWidth": 96,
"spriteHeight": 96
},
"rows": {
"example1": 4,
"example2": 6
}
}
}
}
Main Use this to display all the frames at once on 1 single screen at interval.
# NOTE: Spritesheet is made of 96x96
import pygame as pg
import json
import spritesheet
pg.init()
SCREEN_WIDTH = 384
SCREEN_HEIGHT = 192
# Speed / FPS
SPEED = 60
# Setting up display and clock
pg.display.set_caption("Spritesheets")
screen = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pg.Clock()
# Setting up default colours
darkGray = (50, 50, 50)
black = (0, 0, 0)
# Load sprite metadata file
with open("sprite_metadata.json", "r") as file:
spriteMetadata = json.load(file)
# Loading spritesheet in
playerSprites = spritesheet.SpriteSheet("character_sprite.png").getSpriteImages(
spriteSheetMetadata=spriteMetadata["SpriteSheets"]["SpriteSheetExample_Player"]
)
animations = [
playerSprites["player_idle_right"],
playerSprites["player_idle_left"],
playerSprites["player_walk_right"],
playerSprites["player_walk_left"],
playerSprites["player_shoot_right"],
playerSprites["player_shoot_left"],
playerSprites["player_jump_right"],
playerSprites["player_jump_left"],
]
animation_states = [[0, 0] for _ in animations]
FRAME_UPDATE_INTERVAL = 10
drawRow = 0
numDrawn = 0
xDrawPos = 0
# Game Loop
running = True
while running:
clock.tick(SPEED)
for event in pg.event.get():
if event.type == pg.QUIT:
running = False
screen.fill(darkGray)
xDrawPos = 0
yDrawPos = 0
# Render
for index, frames in enumerate(animations):
currentFrame, counter = animation_states[index]
counter += 1
if counter >= FRAME_UPDATE_INTERVAL:
currentFrame = (currentFrame + 1) % len(animations[index])
counter = 0
animation_states[index] = [currentFrame, counter]
screen.blit(frames[currentFrame], (xDrawPos, yDrawPos))
xDrawPos += 96
if xDrawPos >= SCREEN_WIDTH:
xDrawPos = 0
yDrawPos += 96
pg.display.update()
pg.quit()
Here is the spritesheet I found on google you can use. Make sure to name it character_sprite.png
-
4\$\begingroup\$ Please do not edit the question, especially the code, after an answer has been posted. Changing the question may cause answer invalidation. Everyone needs to be able to see what the reviewer was referring to. What to do after the question has been answered. \$\endgroup\$pacmaninbw– pacmaninbw ♦2025年02月07日 21:08:28 +00:00Commented Feb 7 at 21:08
1 Answer 1
Overview
The code layout is good, and you used meaningful names for classes, functions and variables. The comments in the code are helpful.
Portability
When I run the code, I get this error:
AttributeError: module 'pygame' has no attribute 'Clock'
We are likely using different versions of Python. My pygame version is 2.5.2, which matches yours I think.
When I change:
clock = pg.Clock()
to:
clock = pg.time.Clock()
then the error goes away.
Magic numbers
You did a good job in Main with assigning magic numbers to constants like
SCREEN_WIDTH
. There is one more that would be worth setting to a constant,
since you use 96
in a couple of places, like:
xDrawPos += 96
Naming
The PEP 8 style guide recommends snake_case for function and variable names. For example, in Main:
drawRow = 0
numDrawn = 0
xDrawPos = 0
would become:
draw_row = 0
num_drawn = 0
x_draw_pos = 0
In the SpriteSheet
class:
def getSpriteImage
would become:
def get_sprite_image
Documentation
PEP-8 also recommends adding docstrings for classes and functions.
-
1\$\begingroup\$ Thanks for the review. Issue you’re having with pygame is that you’re using the official release and I’m using pygame-community edition hence pygame-ce. It is maintained more and has unique shortcuts. Magic numbers are there because I’ve quickly wrote main to showcase the example. Some of the variables under naming shouldn’t even be there. Will fix once I get home. And the camel case style or naming that I use is because I come from C# dev background hence the camel case naming. I need to get used to python snake_case naming. \$\endgroup\$ThisQRequiresASpecialist– ThisQRequiresASpecialist2025年02月07日 15:42:23 +00:00Commented Feb 7 at 15:42