Game Making with Python and Pygame Part 5

in #python5 years ago

Refactor the Sprite into Its Own Class

## Refactor the Sprite into Its Own Class

See the previous parts linked at the bottom of this post if you haven't already.


In this session we're going to refactor the ball into its own class, and then instantiate multiple copies into the window all with different starting points and velocities. To start with we need to pull the parts that deal with the ball out of our MainWindow class. We want to give it an update method that will deal with updating the position of the ball and a draw method that will deal with blitting the sprite into the window.

As we're going to need to know the window size in the Ball class, change things so that we have global WIDTH and HEIGHT variables. I have them matching the size of the image I'm using. If you make the window smaller than the image it will just place the top left of the image in the top left of the window. As the position deletes to (0, 0). If you make the window bigger than the size of the image then it works fine too,but if the ball animation is still running then when it goes over the parts outside of the image it leaves a trail. Try both and see what happens.

We're going to change our class from last time so that the MainWindow class has a self.ball object which is created by calling the constructor on our new Ball class and two new method calls to update() and draw() methods on the Ball object. All other references to the things relating to the ball are moved into the Ball class itself.

So the updated MainWindow class will look something like this:

import sys
import pygame

WIDTH = 800
HEIGHT = 600

class MainWindow:
    def __init__(self):
        pygame.init()

        self.background = pygame.image.load("myrock.jpg")
        self.background_rect = self.background.get_rect()

        self.ball = Ball()

        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption('Look At My Rock and Ball- Game Making Part 5')

        self.clock = pygame.time.Clock()
        self.time_counter = 0

    def main_game_loop(self):
        while True:
            self.time_counter += self.clock.tick()
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
            if self.time_counter > 15:
                self.ball.update()
                self.DISPLAYSURF.blit(self.background, self.background_rect)
                self.ball.draw(self.DISPLAYSURF)
                self.time_counter = 0
            pygame.display.update()

As the files are getting larger I'm going to only show parts of them now, in the text, and build them up to the final working code as we go. I'll try and include the full code at the end of each part. The bare bones of a Ball class would look like this (along with the two lines to make everything run):

class Ball:
    def __init__(self):
        self.image = pygame.image.load("ball.png")
        self.image_rect = self.image.get_rect()
        self.velocity = [5, 4]

    def update(self):
        pass

    def draw(self, a_surface):
        pass

game = MainWindow()
game.main_game_loop()

This literally just loads in the ball image, creates a rect for it and creates the velocity values. Currently if we put the two parts together nothing will display. I'm just going to show the Ball class now as we update it. If we add in the blit command to the draw method, then we should see the ball make an appearance. Basically we pass in the surface that represents the window so that we know what to blit it onto.

class Ball:
    def __init__(self):
        self.image = pygame.image.load("ball.png")
        self.image_rect = self.image.get_rect()
        self.velocity = [5, 4]

    def update(self):
        pass

    def draw(self, a_surface):
        a_surface.blit(self.image, self.image_rect)

Running it now, should show you a static ball in the top left hand corner. We're going to make it so that the ball is spawned at a random point on the window (but at least 100 pixels from the right and bottom edges). You will need to add the line import random along with the other imports. Then add in calls to random.randint() to set the x and y coordinates to be random values. We then use these values to set the position of the top-left corner of the ball.

class Ball:
    def __init__(self):
        x = random.randint(0, WIDTH - 100)
        y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load("ball.png")
        self.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)
        self.velocity = [5, 4]

    def update(self):
        pass

    def draw(self, a_surface):
        a_surface.blit(self.image, self.image_rect)

Run the code several times, so that you can see the ball appearing in different places in the window. It might be that we want to spawn balls at specific locations on the screen. We can do this by creating x and y arguments to the constructor of the Ball class (__init__()), assigning them default values of 0 and 0 and then assigning random values to them if they are both 0.

class Ball:
    def __init__(self, x=0, y=0):
        if x == 0 and y ==0:
            x = random.randint(0, WIDTH - 100)
            y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load("ball.png")
        self.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)
        self.velocity = [5, 4]

    def update(self):
        pass

    def draw(self, a_surface):
        a_surface.blit(self.image, self.image_rect)

Test that it works as before. Then change the line where the constructor is called to instantiate a Ball object to read self.ball = Ball(50, 50) instead of self.ball = Ball() this should make a ball always appear at 50, 50 (50 from the left edge, 50 from the top). Test it, then change it back. Then add in the code that will update the position of the ball, in the update() method. This is essentially identical to the code we had previously, before we refactored things

class Ball:
    def __init__(self, x=0, y=0):
        if x == 0 and y ==0:
            x = random.randint(0, WIDTH - 100)
            y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load("ball.png")
        self.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)
        self.velocity = [5, 4]

    def update(self):
        self.image_rect = self.image_rect.move(self.velocity)

        if self.image_rect.left < 0 or self.image_rect.right > WIDTH:
            self.velocity[0] = -self.velocity[0]
        if self.image_rect.top < 0 or self.image_rect.bottom > HEIGHT:
            self.velocity[1] = -self.velocity[1]

    def draw(self, a_surface):
        a_surface.blit(self.image, self.image_rect)

Running the code now, you should see the ball appear at a random point on the screen, but it will always travel in the same direction (5 across, 4 down to begin with). We can add random values to make this part random too.

class Ball:
    def __init__(self, x=0, y=0):
        if x == 0 and y ==0:
            x = random.randint(0, WIDTH - 100)
            y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load("ball.png")
        self.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)
        self.velocity = [random.randint(-5, 5), random.randint(-5, 5)]

    def update(self):
        self.image_rect = self.image_rect.move(self.velocity)

        if self.image_rect.left < 0 or self.image_rect.right > WIDTH:
            self.velocity[0] = -self.velocity[0]
        if self.image_rect.top < 0 or self.image_rect.bottom > HEIGHT:
            self.velocity[1] = -self.velocity[1]

    def draw(self, a_surface):
        a_surface.blit(self.image, self.image_rect)

Running this multiple times, should give you the ball appearing in different random positions, while also travelling in different directions. The whole program should now look like this:

import sys
import pygame
import random

WIDTH = 800
HEIGHT = 600

class MainWindow:
    def __init__(self):
        pygame.init()

        self.background = pygame.image.load("myrock.jpg")
        self.background_rect = self.background.get_rect()

        self.ball = Ball()

        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption('Look At My Rock and Ball - Game Making Part 5')

        self.clock = pygame.time.Clock()
        self.time_counter = 0

    def main_game_loop(self):
        while True:
            self.time_counter += self.clock.tick()
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
            if self.time_counter > 15:
                self.ball.update()
                self.DISPLAYSURF.blit(self.background, self.background_rect)
                self.ball.draw(self.DISPLAYSURF)
                self.time_counter = 0
            pygame.display.update()

class Ball:
    def __init__(self, x=0, y=0):
        if x == 0 and y ==0:
            x = random.randint(0, WIDTH - 100)
            y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load("ball.png")
        self.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)
        self.velocity = [random.randint(-5, 5), random.randint(-5, 5)]

    def update(self):
        self.image_rect = self.image_rect.move(self.velocity)

        if self.image_rect.left < 0 or self.image_rect.right > WIDTH:
            self.velocity[0] = -self.velocity[0]
        if self.image_rect.top < 0 or self.image_rect.bottom > HEIGHT:
            self.velocity[1] = -self.velocity[1]

    def draw(self, a_surface):
        a_surface.blit(self.image, self.image_rect)

game = MainWindow()
game.main_game_loop()

As we said previously it's relatively easy to create multiple balls at the same time and have them display accordingly. All we need to do is to make a few changes. We can create an empty list as part of the MainWindow class and use a loop to assign 20 balls to it. We then need to iterate through the list of balls to update them each time, and to draw them too. Being able to do this easily is part of the reason we would want each of our sprites to be in some sort of class. Rather than coded directly into the MainWindow class.

import sys
import pygame
import random

WIDTH = 800
HEIGHT = 600

class MainWindow:
    def __init__(self):
        pygame.init()

        self.background = pygame.image.load("myrock.jpg")
        self.background_rect = self.background.get_rect()

        self.balls = []
        for _ in range(20): 
            self.balls.append(Ball())

        self.DISPLAYSURF = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption('Look At My Rock and Ball - Game Making Part 5')

        self.clock = pygame.time.Clock()
        self.time_counter = 0

    def main_game_loop(self):
        while True:
            self.time_counter += self.clock.tick()
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    pygame.quit()
                    sys.exit()
            if self.time_counter > 15:
                for ball in self.balls:
                    ball.update()

                self.DISPLAYSURF.blit(self.background, self.background_rect)

                for ball in self.balls:
                    ball.draw(self.DISPLAYSURF)

                self.time_counter = 0
            pygame.display.update()

class Ball:
    def __init__(self, x=0, y=0):
        if x == 0 and y ==0:
            x = random.randint(0, WIDTH - 100)
            y = random.randint(0, HEIGHT - 100)
        self.image = pygame.image.load("ball.png")
        self.image_rect = self.image.get_rect()
        self.image_rect.topleft = (x, y)
        self.velocity = [random.randint(-5, 5), random.randint(-5, 5)]

    def update(self):
        self.image_rect = self.image_rect.move(self.velocity)

        if self.image_rect.left < 0 or self.image_rect.right > WIDTH:
            self.velocity[0] = -self.velocity[0]
        if self.image_rect.top < 0 or self.image_rect.bottom > HEIGHT:
            self.velocity[1] = -self.velocity[1]

    def draw(self, a_surface):
        a_surface.blit(self.image, self.image_rect)

game = MainWindow()
game.main_game_loop()

I've taken the decision to put two loops in the main loop, one to update the balls and the other to draw them, we could have done this with a single loop, with both method calls inside it, but this would have to be after the call that blits the background image to the screen (otherwise the balls would be underneath the background image). You should end up with something that looks a bit like this:

Balls and Rock
Image by the Author - Amos1969 - Dave A

That's it for Part 5, in Part 6 we'll go back and look at how to add controls to a sprite so that we can move it round the screen.

Like and follow if you're enjoying these. If you have any comments or suggestions for improvements or things you'd like it do, let me know in the comments.


Previous parts:

Part 1 - How to get set up with an up-to-date version of Pygame: Part 1

Part 2 - How to make a window appear using basic code: Part 2

Part 3 - Refactoring the code into a class and adding a background image: Part 3

Part 4 - Added a moving sprite to the window: Part 4

Sort:  

Congratulations @amos1969! You have completed the following achievement on the Steem blockchain and have been rewarded with new badge(s) :

You received more than 500 upvotes. Your next target is to reach 1000 upvotes.

You can view your badges on your Steem Board and compare to others on the Steem Ranking
If you no longer want to receive notifications, reply to this comment with the word STOP

Vote for @Steemitboard as a witness to get one more award and increased upvotes!

Coin Marketplace

STEEM 0.16
TRX 0.16
JST 0.030
BTC 58180.92
ETH 2477.99
USDT 1.00
SBD 2.42