7
\$\begingroup\$

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

sprites

pacmaninbw
26.2k13 gold badges47 silver badges113 bronze badges
asked Feb 7 at 0:13
\$\endgroup\$
1
  • 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\$ Commented Feb 7 at 21:08

1 Answer 1

5
\$\begingroup\$

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.

answered Feb 7 at 12:42
\$\endgroup\$
1
  • 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\$ Commented Feb 7 at 15:42

Your Answer

Draft saved
Draft discarded

Sign up or log in

Sign up using Google
Sign up using Email and Password

Post as a guest

Required, but never shown

Post as a guest

Required, but never shown

By clicking "Post Your Answer", you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.