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)
\$\endgroup\$
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)
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.