2
\$\begingroup\$

I've written a class that allows me to easily create sprites on the screen, and then do things like: setting an image or an animation; move the sprite as a platformer or as a top down game; and test for many things like collision and orientation. So far I've found this to be very useful, and it has made it really easy to make games. Now I've finished getting everything to work, and writing a text file of the documentation I thought that I'd post it here for review (I'll post the documentation if requested).

The idea is that it would be imported into a file using import classes.sprite.Sprite. With classes being the folder that it's in, sprite.py the name of the file, and Sprite the name of the class.

The folder structure would be:

Projects/Pygame/Game/main.py
Projects/Pygame/Game/classes/sprite.py
Projects/Pygame/Game/data/

The data folder is used to store any images or text files that the game will need.

import os, pygame
from pygame.locals import *
from math import *
pygame.init()
WIDTH = None
HEIGHT = None
SCREEN = None
FPS = None
path = os.path.abspath(os.path.join(os.path.dirname( __file__ ), '..', 'data'))
def load_rotated_image(sprite):
 '''Loads an image for the rotate functions in Sprite class'''
 sprite.surface = pygame.transform.rotate(sprite.image, sprite.angle)
 sprite.rect = sprite.surface.get_rect(center = (sprite.rect.x + sprite.rect.width / 2, sprite.rect.y + sprite.rect.height / 2))
def init(surface, fps):
 '''Initialises the sprite class'''
 global WIDTH, HEIGHT, SCREEN, FPS #I know globals are bad but I don't know how to avoid them here
 SCREEN = surface
 WIDTH, HEIGHT = SCREEN.get_size()
 FPS = fps
class Sprite(object):
 '''Creates a sprite object'''
 def __init__ (self, x = 0, y = 0, width = 50, height = 50, colour = (255, 255, 255), boundry = None, Clamp = False, flip = False):
 '''Initialises the sprite object''' 
 self.x = x
 self.y = y
 self.width = width
 self.height = height
 if boundry == None: self.boundry = (0, 0, WIDTH, HEIGHT)
 else: self.boundry = boundry
 self.clamp = Clamp
 self.colour = colour
 self.do_flip = flip
 self.angle = 0
 self.animation = False
 self.appear = True
 self.flip = False
 self.moving = False
 self.surface = pygame.Surface((self.width, self.height))
 self.image = self.surface
 self.rect = self.surface.get_rect()
 self.rect.x = x
 self.rect.y = y 
 self.surface.fill(colour)
 self.xvel = 0
 self.yvel = 0
 def set_image(self, image, scale = 1, size = (0, 0), colourkey = (255, 0, 255)):
 '''Initilizes a sprite to display an image'''
 self.surface = pygame.image.load(os.path.join(path, image)).convert()
 #self.surface = pygame.image.load(image).convert()
 if not colourkey == None: self.surface.set_colorkey(colourkey)
 if size != (0, 0):
 self.width = round(size[0])
 self.height = round(size[1])
 else:
 self.width = round(scale * self.surface.get_width())
 self.height = round(scale * self.surface.get_height())
 self.image = pygame.transform.scale(self.surface, (self.width, self.height))
 self.surface = self.image
 self.rect = self.surface.get_rect()
 self.rect.x = self.x
 self.rect.y = self.y
 def set_animation(self, frames = [], scale = 1, fps = 1, animate_on_move = False, idle_frame = None, colourkey = (255, 0, 255)):
 '''Initilizes a sprite to display an animation'''
 self.animation = True
 self.frames = []
 self.animate_on_move = animate_on_move
 self.idle_frame = idle_frame
 self.change = max(round(FPS / abs(fps)), 1)
 self.tick = 0
 for image in frames:
 self.frame = pygame.image.load(os.path.join(path, image)).convert()
 if not colourkey == None: self.frame.set_colorkey(colourkey)
 self.width = round(scale * self.frame.get_width())
 self.height = round(scale * self.frame.get_height())
 self.frames.append(pygame.transform.scale(self.frame, (self.width, self.height)))
 self.rect = self.frames[0].get_rect()
 self.rect.x = self.x
 self.rect.y = self.y
 self.frame = 0
 def show(self):
 '''Show this sprite'''
 self.appear = True
 def hide(self):
 '''Hide this sprite'''
 self.appear = False
 def move(self, speed = 5, colliders = [], platformer = False, jump_height = 1):
 '''Allows the arrow keys to control the sprite'''
 keys = pygame.key.get_pressed()
 if keys[K_a] or keys[K_LEFT]:
 self.rect.x -= int(abs(speed))
 self.flip = False
 self.moving = True
 if self.rect_collide(colliders):
 self.rect.x += int(abs(speed))
 if keys[K_d] or keys[K_RIGHT]:
 self.rect.x += int(abs(speed))
 self.flip = True
 self.moving = True
 if self.rect_collide(colliders):
 self.rect.x -= int(abs(speed))
 if (keys[K_a] or keys[K_LEFT]) and (keys[K_d] or keys[K_RIGHT]) : self.moving = False
 if platformer == True:
 if not (keys[K_a] or keys[K_LEFT] or keys[K_d] or keys[K_RIGHT]): self.moving = False
 self.rect.y += 10
 if (keys[K_w] or keys[K_UP] or keys[K_SPACE]) and self.rect_collide(colliders) and self.yvel < 1:
 self.yvel = int(abs(speed)) * (2 + ((jump_height - 1) / 2))
 self.rect.y -= 15
 if self.rect_collide(colliders): self.yvel = 0
 self.rect.y += 15
 self.rect.y -= 10
 self.rect.y -= self.yvel
 if self.rect_collide(colliders):
 if self.yvel > 0: self.rect.y += self.yvel + 1
 else: self.rect.y += self.yvel
 self.yvel = 0
 else:
 if not (keys[K_a] or keys[K_LEFT] or keys[K_d] or keys[K_RIGHT] or keys[K_w] or keys[K_UP] or keys[K_s] or keys[K_DOWN]): self.moving = False
 if (keys[K_w] or keys[K_UP]) and (keys[K_s] or keys[K_DOWN]) : self.moving = False
 if keys[K_w] or keys[K_UP]:
 self.rect.y -= int(abs(speed))
 self.moving = True
 if self.rect_collide(colliders):
 self.rect.y += int(abs(speed))
 if keys[K_s] or keys[K_DOWN]:
 self.rect.y += int(abs(speed))
 self.moving = True
 if self.rect_collide(colliders):
 self.rect.y -= int(abs(speed))
 def render(self, colour = None, frame = None):
 '''Renders the sprite'''
 if not colour == None:
 self.colour = colour
 self.surface.fill(self.colour)
 if self.clamp:
 self.rect.x = min(max(self.rect.x, self.boundry[0]), self.boundry[0] + self.boundry[2] - self.rect.width )
 self.rect.y = min(max(self.rect.y, self.boundry[1]), self.boundry[1] + self.boundry[3] - self.rect.height)
 if self.appear:
 if self.animation:
 self.tick += 1
 if self.tick == self.change:
 if self.frame + 1 == len(self.frames): 
 self.frame = 0
 else:
 self.frame += 1
 self.tick = 0
 if not frame == None: self.frame = frame % len(self.frames)
 if self.animate_on_move:
 if not self.moving: self.frame = self.idle_frame
 self.rect.width = self.frames[self.frame].get_width()
 self.rect.height = self.frames[self.frame].get_height()
 if self.do_flip:
 if self.flip: SCREEN.blit(pygame.transform.flip(self.frames[self.frame], True, False), (self.rect.x, self.rect.y))
 else: SCREEN.blit(self.frames[self.frame], (self.rect.x, self.rect.y))
 else: SCREEN.blit(self.frames[self.frame], (self.rect.x, self.rect.y))
 else:
 if self.do_flip:
 if self.flip: SCREEN.blit(pygame.transform.flip(self.surface, True, False), (self.rect.x, self.rect.y))
 else: SCREEN.blit(self.surface, (self.rect.x, self.rect.y))
 else: SCREEN.blit(self.surface, (self.rect.x, self.rect.y))
 def wrap(self):
 '''Wraps the sprite around the edge of the screen'''
 self.wrap_around = False
 if self.rect.x < 0:
 self.rect.x += WIDTH
 self.wrap_around = True
 if self.rect.x + self.width > WIDTH:
 self.rect.x -= WIDTH
 self.wrap_around = True
 if self.rect.y < 0:
 self.rect.y += HEIGHT
 self.wrap_around = True
 if self.rect.y + self.height > HEIGHT:
 self.rect.y -= HEIGHT
 self.wrap_around = True
 if self.wrap_around:
 if self.animation:
 if self.do_flip:
 if self.flip: SCREEN.blit(pygame.transform.flip(self.frames[self.frame], True, False), (self.rect.x, self.rect.y))
 else: SCREEN.blit(self.frames[self.frame], (self.rect.x, self.rect.y))
 else: SCREEN.blit(self.frames[self.frame], (self.rect.x, self.rect.y))
 else:
 if self.do_flip:
 if self.flip: SCREEN.blit(pygame.transform.flip(self.surface, True, False), (self.rect.x, self.rect.y))
 else: SCREEN.blit(self.surface, (self.rect.x, self.rect.y))
 else: SCREEN.blit(self.surface, (self.rect.x, self.rect.y))
 self.rect.x %= WIDTH
 self.rect.y %= HEIGHT
 def rect_collide(self, sprites):
 '''Returns True if two sprites are colliding'''
 self.rect = pygame.Rect(self.rect.x, self.rect.y, self.rect.width, self.rect.height)
 if self.appear:
 for sprite in sprites:
 if self.rect.colliderect(sprite) and sprite.appear:
 return True
 return False
 def clamp(self, boundry = None, Clamp = None):
 '''Clamps a sprite to a boundry'''
 if not Clamp == None: self.clamp = Clamp
 else:
 if self.clamp == True: self.clamp = False
 if self.clamp == False: self.clamp = True
 if not boundry == None:
 self.boundry = boundry
 def Collect(self, player):
 '''Makes an object collectable'''
 if player.rect_collide([self]):
 self.hide()
 return True
 def mouse_hover(self):
 '''Returns True if the mouse is over the sprite'''
 mouse_pos = pygame.mouse.get_pos()
 if self.rect.collidepoint(mouse_pos): return True
 else: return False
 def mouse_click(self, button = 1):
 '''Returns True if the sprite is clicked'''
 mouse_pos = pygame.mouse.get_pos()
 mouse_pressed = pygame.mouse.get_pressed()
 if self.rect.collidepoint(mouse_pos) and mouse_pressed[button - 1]: return True
 else: return False
 def move_in_direction(self, magnitude, direction = None):
 '''Moves the sprite a certain distance in the direction it's facing'''
 if direction != None:
 self.rect.x += magnitude * cos(radians(direction))
 self.rect.y -= magnitude * sin(radians(direction))
 else:
 self.rect.x += magnitude * cos(radians(self.angle))
 self.rect.y -= magnitude * sin(radians(self.angle))
 def point_in_direction(self, direction):
 '''Points a sprite in a specific direction'''
 self.angle = direction
 load_rotated_image(self)
 def point_towards(self, pos = (0,0), sprite = None, anchor = 'center'):
 '''Points sprite towards another sprite'''
 if sprite == None: self.angle = 360 - atan2(pos[1] - self.rect.centery, pos[0] - self.rect.centerx) * 180 / pi 
 else: exec('self.angle = 360 - atan2(sprite.rect.' + anchor + '[1] - self.rect.centery, sprite.rect.' + anchor + '[0] - self.rect.centerx) * 180 / pi')
 load_rotated_image(self)
 def distance_to(self, pos = (0, 0), sprite = None, anchor = 'center'):
 '''Returns the distance to a sprite or point'''
 exec('self.pos = (self.rect.' + anchor + '[0], self.rect.' + anchor + '[1])') 
 if sprite == None: return sqrt(abs(self.pos[0] - pos[0])**2 + abs(self.pos[1] - pos[1])**2)
 else: return sqrt(abs(self.pos[0] - sprite.rect.x)**2 + abs(self.pos[1] - sprite.rect.y)**2)
 def turn(self, angle):
 '''Turns the sprite a certain amout of degrees'''
 self.angle += angle
 load_rotated_image(self)
Jamal
35.2k13 gold badges134 silver badges238 bronze badges
asked Mar 19, 2017 at 19:57
\$\endgroup\$

1 Answer 1

3
\$\begingroup\$

I wonder why you don't use pygame's sprite class. Then you can also put the sprites into one of the pygame.sprite.Group container types and use the sprite collision functions. You could also use pygame.math.Vector2 for some attributes as the position and velocity.

class MySpriteSubclass(pygame.sprite.Sprite):
 """Creates a sprite object."""
 def __init__(self, x=0, y=0, width=50, height=50, colour=(255, 255, 255),
 boundry=None, Clamp=False, flip=False):
 # Call parent class' __init__
 super(MySpriteSubclass, self).__init__()
 self.image = some_previously_loaded_image
 self.rect = self.image.get_rect(center=(x, y))
 # Use vectors for the position and velocity.
 self.pos = pygame.math.Vector2(x, y)
 self.vel = pygame.math.Vector2(0, 0)

To update the pos just write:

self.pos += self.vel
self.rect.center = self.pos

Some methods like move and render look a bit complicated and can probably be simplified or split apart. comments would help.


It also seems you can simplify some of the "point/direction..." methods with the help of pygame's Vector2 class. Especially the use of exec looks odd to me. You could use getattr to get the anchor point.

sprite = MySpriteSubclass()
anchor = 'topleft'
anchor_x, anchor_y = getattr(sprite.rect, anchor)

I must say that I find the Sprite base class too big and it does have too many responsibilities. I'd rather use simpler, more specialized classes instead of using it for every kind of object. For example the board tile class in your noughts and crosses game does not need all the move, point_towards, distance, etc. functions. The MySprite class (Tile would be a better name) in my example there demonstrates how short and simple it can be. I'd suggest to write more specialized classes and then see if you can abstract common parts away through composition or inheritance.


Here are some tips how you could change some of the mentioned methods with the help of vectors:

# vector can be a tuple or pygame.math.Vector2.
def distance_to(self, vector):
 return self.pos.distance_to(vector)
def move_in_direction(self, magnitude, angle):
 # Create a unit vector, rotate it and scale by magnitude.
 vector = pg.math.Vector2(1, 0).rotate(angle) * magnitude 
 self.pos += vector
def mouse_hover(self):
 """Return True if the mouse is over the sprite."""
 mouse_pos = pygame.mouse.get_pos()
 # collidepoint returns a bool so we don't need if-else blocks here.
 return self.rect.collidepoint(mouse_pos)
answered Mar 20, 2017 at 1:57
\$\endgroup\$

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.