Monday, 19 June 2023

Let's Study Programming By Making Games: Pygame Unit: 03


Hello again! Welcome to another Pygame tutorial which is going to help achieving fluency in Python in a more enjoyable, visual way.
Today's little project will be based on all the concepts we discussed in the former lesson. Our objective is to create some more text objects on screen, that bounce around within the view port borders. Moreover, we'll include a player object that will be able to fire bullets that capable of changing the text color of the text objects it hits.

As you can imagine, the screen is going to get slightly more crowded than in our previous basic code snippets. For a real extremely busy game of course a no brainer, but even for our little current educational project, keeping code organized is imperative. Think about the tedium it would be creating lots of variables and naming them specifically, say for enemy1 through enemy99, player_attribute1 to player_attribute100, etc etc. Not only would it take more time writing each specific container for values out, it would also hamper readability and increase the chances of introducing bugs into your code. And while this is quite a bad thing in itself when you revisit your code and have absolutely no idea what you've meant with a particular chunk of code, it is especially hell for your friends and colleagues you've teamed up with.

So in order to keep things neat and tidy, we'll be using classes and their instances as a kind of structure that wraps around related data similar to functions. Classes can be considered blueprints for any object in your game or app you want to create. Instantiation in a jiffy is like taking a copy of that blueprint to build a specific thing that is stored away in a variable. The cool thing is, we can now manipulate all the related data of that instance as we see fit. Ok, but enough jibber jabber for now. Let's see how it works in practice.

Like before, I'll show you the complete code listing and we'll discuss what each part does in detail.
Here we go:



import pygame, sys
import random

pygame.init()
SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 768
screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
FPS = 60



font_60 = pygame.font.SysFont("Comic Sans MS", 60)



class Text_Class():
    def __init__(self, font, text, color,
                 x_pos, y_pos, x_move_dir, y_move_dir, speed):
        self.text_img = font.render(text, True, color)
        self.text_img_rect = self.text_img.get_rect(center = (x_pos, y_pos))
        self.move_dir = [x_move_dir, y_move_dir]
        self.speed = speed
        self.color = color
        self.text = text
        self.font = font


    def update(self):
        self.text_img = self.font.render(self.text, True, self.color)
        delta_x = self.move_dir[0] * self.speed
        delta_y = self.move_dir[1] * self.speed


        
        self.text_img_rect.x += delta_x
        self.text_img_rect.y += delta_y


    def screen_boundary_check(self):
        x_check = self.text_img_rect.right < 0 or self.text_img_rect.left > SCREEN_WIDTH
        y_check = self.text_img_rect.bottom < 0 or self.text_img_rect.top > SCREEN_HEIGHT

        if x_check:
            self.move_dir[0] *= -1

        if y_check:
            self.move_dir[1] *= -1


    def draw(self):
        screen.blit(self.text_img, self.text_img_rect)
        pygame.draw.rect(screen, self.color, self.text_img_rect, 6)



class Player_Text(Text_Class):
    def __init__(self, font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed):
        super().__init__(font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed)
        self.bullet_fired = False
        self.last_bullet_fired = pygame.time.get_ticks()
        self.bullet_interval = 500

    def get_input(self):
        key = pygame.key.get_pressed()
        if key[pygame.K_a]:
            self.move_dir[0] = -1
        elif key[pygame.K_d]:
            self.move_dir[0] = 1
        else:
            self.move_dir[0] = 0

        if key[pygame.K_w]:
            self.move_dir[1] = -1
        elif key[pygame.K_s]:

            self.move_dir[1] = 1
        else:
            self.move_dir[1] = 0

        if key[pygame.K_SPACE] and not self.bullet_fired:
            self.bullet_fired = True

        
            
    def shoot_bullet(self):
        if self.bullet_fired:
            current_time = pygame.time.get_ticks()
            if current_time - self.last_bullet_fired > self.bullet_interval:
                bullet = Bullet_Char(font_60, "!", "violet", self.text_img_rect.midtop[0], self.text_img_rect.midtop[1] - 40, 0, -1, 10)
                bullet_char_list.append(bullet)
                self.last_bullet_fired = pygame.time.get_ticks()
        self.bullet_fired = False

                             


    def update(self):
        self.get_input()
        self.shoot_bullet()
        delta_x = self.move_dir[0] * self.speed
        delta_y = self.move_dir[1] * self.speed

        if self.text_img_rect.left + delta_x <= 0 or self.text_img_rect.right + delta_x >= SCREEN_WIDTH:
            delta_x = 0

        if self.text_img_rect.top + delta_y <= 0 or self.text_img_rect.bottom + delta_y >= SCREEN_HEIGHT:
            delta_y = 0

        self.text_img_rect.x += delta_x
        self.text_img_rect.y += delta_y

    def draw(self):
        screen.blit(self.text_img, self.text_img_rect)




class Bullet_Char(Text_Class):
    def __init__(self, font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed):
        super().__init__(font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed)
        self.offscreen = False
        self.random_col = (random.randint(0,255), random.randint(0,255), random.randint(0,255))
        self.hit_something = False



    def screen_boundary_check(self):
        x_check = self.text_img_rect.right < 0 or self.text_img_rect.left > SCREEN_WIDTH
        y_check = self.text_img_rect.bottom < 0 or self.text_img_rect.top > SCREEN_HEIGHT
        if x_check:
            self.offscreen = True
        if y_check:
            self.offscreen = True

    def draw(self):
        screen.blit(self.text_img, self.text_img_rect)

    

        
        


hello_pygame_world = Text_Class(font_60, "Hello Pygame World!", "red",
                                SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2,
                                1, 0, 5)

hello_python = Text_Class(font_60, "Hello Python!", "green",
                          200, 100, 0, 1, 2)

hello_world = Text_Class(font_60, "Hello World!", "blue",
                         300, 200, -1, -1, 3)

player_string = Player_Text(font_60, "=A=", "white", SCREEN_WIDTH // 2, SCREEN_HEIGHT - 100,
                           0, 0, 5)

text_object_list = [hello_pygame_world, hello_python, hello_world, player_string]
bullet_char_list = []




run = True
while run:
    clock.tick(FPS)
    pygame.display.set_caption(f"Hello Pygame World | FPS: {clock.get_fps():.1f}")
    screen.fill("black")

    for obj in text_object_list:
        obj.update()
        obj.screen_boundary_check()
        obj.draw()

    for bullet_index, bullet in enumerate(bullet_char_list):
        bullet.update()
        bullet.screen_boundary_check()
        bullet.draw()

        for obj in text_object_list:
            if obj.text_img_rect.colliderect(bullet.text_img_rect):
                obj.color = bullet.random_col
                bullet.hit_something = True


               
        if bullet_char_list[bullet_index].offscreen:
            del bullet_char_list[bullet_index]
        elif bullet_char_list[bullet_index].hit_something:
            del bullet_char_list[bullet_index]

    



    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
        if event.type == pygame.KEYDOWN and event.key == pygame.K_ESCAPE:
            run = False

    pygame.display.update()
pygame.quit()
sys.exit()





Alrighty then, I must admit it is definitely chunkier than the previous example. However it would be even larger had we not used classes at all. Ok, apart from the usual boiler plate and basic frame work code we now have a Text_Class right at the start instead of our draw_text function we had before. And in a glance we can see that it encapsulates quite a bit of functionality. You'll surely notice all those functions inside the class that fulfill certain purposes just as the static functions we already know from earlier. In the case of functions inside the class body, we refer to them as methods as they relate to particular classes and their instances.

The probably most important method within this or any other class may be the def __init__() method which is the so called constructor of the class. It is invoked just in the moment when an object is created or instantiated to be precise. The init method can be empty but is probably most useful when parameters and their respective arguments are passed into them. This gives us the functionality to initialize an object's attributes when it is created, for example its starting position, speed, health and much more.


class Text_Class():
    def __init__(self, font, text, color,
                 x_pos, y_pos, x_move_dir, y_move_dir, speed):
        self.text_img = font.render(text, True, color)
        self.text_img_rect = self.text_img.get_rect(center = (x_pos, y_pos))
        self.move_dir = [x_move_dir, y_move_dir]
        self.speed = speed
        self.color = color
        self.text = text
        self.font = font


In this particular case we take in the font we specified, the text string we want to display, the color, x and y positions, x and y movement directions as well as the speed. Take note of the 'self' right at the beginning. This references the class' attributes that can be passed from one method to the other and therefore easily changed.

self.text_img = font.render(text, True, color)
self.text_img_rect = self.text_img.get_rect(center = (x_pos, y_pos))

Here for example we create the text image with our font.render() function and capture it as a class attribute self.text_img.
Similarly we get a rectangle using self.text_img.get_rect() from formerly declared image and feeding the x and y position from our constructor, and apply this position to the center handle of our rectangle. That way we can easily center objects on screen.

The next class attribute is self.move_dir which is a list that holds two elements we passed in: x_move_dir and y_move_dir which stand for x movement direction and y movement direction respectively.

We also capture speed, color, text, and font in their respective class variables for further use in the methods further down the class body. What follows is the update method.
    

def update(self):
        self.text_img = self.font.render(self.text, True, self.color)
        delta_x = self.move_dir[0] * self.speed
        delta_y = self.move_dir[1] * self.speed


        self.text_img_rect.x += delta_x
        self.text_img_rect.y += delta_y




The purpose of this method, once it is called, is generally the movement or the positional update of the corresponding object. For that we employ two local variables delta_x and delta_y which represent the positional change over time as the product of the x and y directional components multiplied by the speed. This is then added to the image rectangle x and y. It doesn't seem to make much sense right away, you may assume. Why not assign move_dir * speed directly to the rectangle's x and y components? Well, it actually gives us an additional level of control, particularly when it comes to collisions at some other time.
The self.text_img = self.font.render(...) line which we already have in our init method does exactly the same thing as before. We've just placed it in here in order make dynamically updating the text color possible, when the text object is being hit by bullets fired from the player.


The next method this class contains, is the screen_boundary_check() method. It is something like a visibility notifier. Once the object goes offscreen on either x or y axis, the movement direction is reversed, letting the object apparently bounce back into our view port.


def screen_boundary_check(self):
        x_check = self.text_img_rect.right < 0 or self.text_img_rect.left > SCREEN_WIDTH
        y_check = self.text_img_rect.bottom < 0 or self.text_img_rect.top > SCREEN_HEIGHT

        if x_check:
            self.move_dir[0] *= -1

        if y_check:
            self.move_dir[1] *= -1


This is very similar to what we've done before in the previous lesson. Again, take note of the self.move_dir instance variable. Because it contains a list with 2 elements, index 0 refers to the x component and index 1 to the y component we passed in earlier.


Lastly we have our draw method within the class which does exactly what its name says: draw the instance to the display surface using screen.blit. Additionally we also draw the surrounding collision and movement rectangle to the screen to make it a bit more interesting with pygame.draw.rect. The last argument of 6 pixels refers to the rectangle border size, which when undefined, defaults to a solid filled rectangle.

 def draw(self):
        screen.blit(self.text_img, self.text_img_rect)
        pygame.draw.rect(screen, self.color, self.text_img_rect, 6)



Ok, Let's see what we got here. This should be enought to draw something moving to the screen, just as before. Ah, we still have to create instances and initialize them with values.

Just above our main loop we'll create our text objects with their respective handles, colors, positions, directions and speeds fed in as arguments into the contructor brackets:


hello_pygame_world = Text_Class(font_60, "Hello Pygame World!", "red",SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2, 1, 0, 5)

hello_python = Text_Class(font_60, "Hello Python!", "green", 200, 100, 0, 1, 2)

hello_world = Text_Class(font_60, "Hello World!", "blue", 300, 200, -1, -1, 3)



We surely could now call all the respective methods by hand within our main loop. But I think it is much more convenient to store them all in a list which we'll call text_object_list. And from there loop through every instance and apply the methods.

text_object_list = [hello_pygame_world, hello_python, hello_world]

That way we can write it out quite concisely within the main loop.

for obj in text_object_list:
    obj.update()
    obj.screen_boundary_check()
    obj.draw()



And tadaa! We'll now see RGB colored text objects floating about the black screen and bouncing back once a screen border is crossed.


Ok, sure. We learned a few programming principles but let's make it a little more exciting. Let's through a player object into the mix. This is also a very good time to get acquinted with inheritance between objects. It is as the name suggests a bit analogous to the inheritence occuring in biology, meaning that certain attributes are passed down from a parent to a child. Similarly we can do something alike with classes. In order to save us a lot of repetitive typing for classes and their objects that basically do the same thing apart of minor differences, we can create child classes (also called sub classes) that inherit functionality from their parent classes (also called super classes). This is what we are going to do with our player object class that will contain the basic functionality of the text class apart from some additional stuff that is unique to the player, for example taking user input. So let's take a look at this class:


class Player_Text(Text_Class):
    def __init__(self, font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed):
        super().__init__(font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed)
        self.bullet_fired = False
        self.last_bullet_fired = pygame.time.get_ticks()
        self.bullet_interval = 500

    def get_input(self):
        key = pygame.key.get_pressed()
        if key[pygame.K_a]:
            self.move_dir[0] = -1
        elif key[pygame.K_d]:
            self.move_dir[0] = 1
        else:
            self.move_dir[0] = 0

        if key[pygame.K_w]:
            self.move_dir[1] = -1
        elif key[pygame.K_s]:
            self.move_dir[1] = 1
        else:
            self.move_dir[1] = 0

        if key[pygame.K_SPACE] and not self.bullet_fired:
            self.bullet_fired = True

        
            
    def shoot_bullet(self):
        if self.bullet_fired:
            current_time = pygame.time.get_ticks()
            if current_time - self.last_bullet_fired > self.bullet_interval:
                bullet = Bullet_Char(font_60, "!", "violet", self.text_img_rect.midtop[0], self.text_img_rect.midtop[1] - 40, 0, -1, 10)
                bullet_char_list.append(bullet)
                self.last_bullet_fired = pygame.time.get_ticks()
        self.bullet_fired = False

                             


    def update(self):
        self.get_input()
        self.shoot_bullet()
        delta_x = self.move_dir[0] * self.speed
        delta_y = self.move_dir[1] * self.speed

        if self.text_img_rect.left + delta_x <= 0 or self.text_img_rect.right + delta_x >= SCREEN_WIDTH:
            delta_x = 0

        if self.text_img_rect.top + delta_y <= 0 or self.text_img_rect.bottom + delta_y >= SCREEN_HEIGHT:
            delta_y = 0

        self.text_img_rect.x += delta_x
        self.text_img_rect.y += delta_y

    def draw(self):
        screen.blit(self.text_img, self.text_img_rect)





It very much looks like our previous text object creating class. However, there is a major difference. The player class, in order to inherit from the base text class, firstly requires the Text_Class as an argument. Then follows the __init__() method which just requires the same parameters as before. For the sub class to make use of all super class attributes and methods we require the so called super().__init__() method, which in fact is the constructor of the class we want to inherit from. This also takes all the parameters we fed in previously. Technically the first three lines would be sufficient to do exactly the same as the Text_Class we inherit from. All the hard work is basically done in the background without us needing to spell the attributes and methods out again.

class Player_Text(Text_Class):
    def __init__(self, font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed):
            super().__init__(font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed)
            self.bullet_fired = False
               self.last_bullet_fired = pygame.time.get_ticks()
            self.bullet_interval = 500



But because it is supposed to be our player class with some extras like taking keyboard input, being able to shoot, etc. we need the additional space for some extra variables. For instance a boolean variable self.bullet_fired, initially set to False, to act as a trigger for us, as we want to control the amount of bullets we are spraying. We only wish to shoot in certain intervals and not litter our screen and consequently our memory with hundreds or thousands of bullet objects. To facilitate a bullet cooldown phase, we also require a bullet timer which is initialized by self.last_bullet_fired = pygame.time.get_ticks(). Then we also set an interval, how long our cooldown is supposed to take - in this case 500 milliseconds.

Of course we also we also need user input. To facilitate that, we define a get_input() method which we'll later call in our update method where it scans for user input every single frame.

def get_input(self):
        key = pygame.key.get_pressed()
        if key[pygame.K_a]:
            self.move_dir[0] = -1
        elif key[pygame.K_d]:
            self.move_dir[0] = 1
        else:
            self.move_dir[0] = 0

        if key[pygame.K_w]:
            self.move_dir[1] = -1
        elif key[pygame.K_s]:
            self.move_dir[1] = 1
        else:
            self.move_dir[1] = 0

        if key[pygame.K_SPACE] and not self.bullet_fired:
            self.bullet_fired = True



Here we use the pygame.key.get_pressed() function that listens similar to the event handler in the main loop for any key presses that occur. This creates dictionary or hash table with all the inputs that we can consequently check using if statesment and by putting the scancodes for the respective keys or buttons in square brackets.
First we scan for the keys we want to implement for the x axis, 'a' for moving left, by setting the self.move_dir[0] to -1. Likewise we scan for the 'd' key in order to move right, setting self.move_dir[0] to 1.
If we press neither key, denoted by the else keyword, we set self.move_dir[0] to 0, as such having no movement at all on the x axis.

Underneath we do a similar check for the 'w' and 's' keys, setting move_dir[1] either to -1 to move up, or move_dir[1] to 1 in order to move down on the y axis.

Finally, we check for spacebar presses while self.bullet_fired is still flagged as False. If that is the case, we set self.bullet_fired to True, which then can trigger the shoot_bullet() method that in turn can spawn bullet objects.


This is where we come to the said method: shoot_bullet().

 
 def shoot_bullet(self):
        if self.bullet_fired:
            current_time = pygame.time.get_ticks()
            if current_time - self.last_bullet_fired > self.bullet_interval:
                bullet = Bullet_Char(font_60, "!", "violet", self.text_img_rect.midtop[0], self.text_img_rect.midtop[1] - 40, 0, -1, 10)
                bullet_char_list.append(bullet)
                self.last_bullet_fired = pygame.time.get_ticks()
        self.bullet_fired = False



It starts execution once we've pushed the spacebar and set self.bullet_fired to True. Then it takes a current time stamp which and subtract the time we lastly fired a bullet which is then compared against the self.bullet_interval of 500 milliseconds we specified earlier. If and only if more than 500 milliseconds have passed since the last bullet fired, then we create a new instance of the bullet class we still have to specify. But it really is no biggy as it also inherits from our Text_Class. We initialize the bullet with the same font, an '!' exclamation mark as the bullet shape we use, and a spawn position just above our player rectangle, about 40 pixels above the self.text_img_rect.midtop handle. It has no x movement at all, so the x direction value is 0, while the y direction value is -1 enabling the bullet to travel towards the top of the screen. Speed is set to 10, letting it traverse 600 pixels in about a second.

In order to fire a couple of bullets in succession as well as check for collision and the like, we need to store the bullet objects in a list we defined previously, by bullet_char_list.append(bullet). Right after that we need to update our timer by getting a new time stamp for self.last_bullet_fired = pygame.time.get_ticks(). Then dedented by one level the bullet fired flag has to be set to False again, enabling the firing cycle all over again.


 def update(self):
        self.get_input()
        self.shoot_bullet()
        delta_x = self.move_dir[0] * self.speed
        delta_y = self.move_dir[1] * self.speed

        if self.text_img_rect.left + delta_x <= 0 or self.text_img_rect.right + delta_x >= SCREEN_WIDTH:
            delta_x = 0

        if self.text_img_rect.top + delta_y <= 0 or self.text_img_rect.bottom + delta_y >= SCREEN_HEIGHT:
            delta_y = 0

        self.text_img_rect.x += delta_x
        self.text_img_rect.y += delta_y



In our update method we call input and shoot bullet methods, move our player object as usually and implement another type of screen boundary check because the other one we used for our moving text objects does not work anymore due to our keyboard inputs simply overriding the direction variable.

if self.text_img_rect.left + delta_x <= 0 or self.text_img_rect.right + delta_x >= SCREEN_WIDTH:
            delta_x = 0

if self.text_img_rect.top + delta_y <= 0 or self.text_img_rect.bottom + delta_y >= SCREEN_HEIGHT:
            delta_y = 0


These two lines check whether our projected movement path, our rectangle sides + our change in x or y intersects with the left, right, top, or bottom of the screen. When it does, we set each delta component to 0, making the player object halt in its tracks, preventing us from leaving the view port.


At the end of the player class we have our draw method, common to the other classes. In this case we override it, making a custom method basically, as we do not want to draw a rectangle around our player.


Now that we have the player class down, we need to initialize a player instance and then we should be able to run it within the main loop. Let's just call it player_string and group it with the other text objects.



player_string = Player_Text(font_60, "=A=", "white", SCREEN_WIDTH // 2, SCREEN_HEIGHT - 100, 0, 0, 5)

Then we add its instance to our object list. Like so:

text_object_list = [hello_pygame_world, hello_python, hello_world, player_string]



When we now run the interpreter, lo and behold, we should see our 'player space ship' (It does look suspiciously like a Star Trek Voyager comm-badge, doesn't it?) floating about the screen reacting to our keyboard inputs. However, if we try to fire a bullet, we get an error message. Of course  we still have to create a template for our bullet objects.


class Bullet_Char(Text_Class):
    def __init__(self, font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed):
        super().__init__(font, text, color, x_pos, y_pos, x_move_dir, y_move_dir, speed)
        self.offscreen = False
        self.random_col = (random.randint(0,255), random.randint(0,255), random.randint(0,255))
        self.hit_something = False



    def screen_boundary_check(self):
        x_check = self.text_img_rect.right < 0 or self.text_img_rect.left > SCREEN_WIDTH
        y_check = self.text_img_rect.bottom < 0 or self.text_img_rect.top > SCREEN_HEIGHT
        if x_check:
            self.offscreen = True
        if y_check:
            self.offscreen = True

    def draw(self):
        screen.blit(self.text_img, self.text_img_rect)




Again, because we have a lot of shared behavior with the other classes we already made, the Bullet_Char class as well will inherit its core attributes from the Text_Class. We use the def __init__() and super().__init__() just as as we did previously with the player class.
Then we add some unique features to the bullet class:
 
self.offscreen = False

Just a trigger that is set to True, once it leaves our view port, as we don't want to have it zipping around in the vast memory universe inside our computer where it is absolutely no use to anyone. We'll use that boolean to check each bullet instance and then delete the reference to it, basically sending it to the garbage collection limbo, freeing space for other more urgent stuff to calculate or render.

For the next line we need the random number module, so make sure you import random at the top of the source file.
This will enable us to create a number tuple with random RGB values for each color channel. Its purpose is to be passed on to the text object when the collision check later on yields true.

self.hit_something = False

As the name implies, this is the flag that is switched to True once a collision occurs.


Then comes a custom screen boundary check which essentially works similar to the others but has its own quirks. The offscreen variable is set to True depending on whether it leaves the x or y boundaries.

def screen_boundary_check(self):
        x_check = self.text_img_rect.right < 0 or self.text_img_rect.left > SCREEN_WIDTH
        y_check = self.text_img_rect.bottom < 0 or self.text_img_rect.top > SCREEN_HEIGHT
        if x_check:
            self.offscreen = True
        if y_check:
            self.offscreen = True




The draw method again is pretty self explanatory. This one is also a custom override compared to the inherited Text_Class method of the same name, as we don't want to implement the colored rectangle draw functionality.

def draw(self):
        screen.blit(self.text_img, self.text_img_rect)



Now, in order to draw the bullets on screen when they are created i.e. the bullet list is populated, we could do something similar to our object for loop:

for obj in text_object_list:
        obj.update()
        obj.screen_boundary_check()
        obj.draw()


Translating it to:

for bullet in bullet_char_list:
    bullet.update()
    bullet.screen_boundary_check()
    bullet.draw()




This would be enough for simply bringing them onscreen. But there is an issue with that, we need a tidy bit of additional functionality for keeping track of collisions, and deletions.
So what we need is another keyword called enumerate as part of our for loop. If we wrap it around our bullet_char_list not only will it provide us with the current object value stored in one list entry, no, it will also give us the corresponding index number. And once we know that one, we can identify each bullet fired and do something with it.

Therefore the modified for loop will look like this:

for bullet_index, bullet in enumerate(bullet_char_list):
        bullet.update()
        bullet.screen_boundary_check()
        bullet.draw()




Bullet stores the object, and bullet_index the number.

Now we need to check for collisions between the objects within the text_object_list and bullets themselves.
For that we nest another for loop within the for enumerate loop.

for bullet_index, bullet in enumerate(bullet_char_list):
        bullet.update()
        bullet.screen_boundary_check()
        bullet.draw()
    
        for obj in text_object_list:
            if obj.text_img.colliderect(bullet.text_img.rect):
                obj.color = bullet.random_col
                bullet.hit_something = True





For each bullet we loop through each text object and see whether an overlap has occured between a text object rectangle and a bullet rectangle. In case that happened, we transfer the random color we made once that particular bullet was spawned to the text object's color attribute. Further we set the flag within the bullet we so descriptively named bullet.hit_something to True. That way we can keep all "clean" and "dirty" bullets apart.

The next step is to delete the bullets from their list.
Unfortunately we just cannot say del bullet. We require the index applied to the list in order for it to function.

This gives us:


if bullet_char_list[bullet_index].offscreen:
       del bullet_char_list[bullet_index]
elif bullet_char_list[bullet_index].hit_something:
       del bullet_char_list[bullet_index]


If that particular bullet within the bullet_char_list with that current index is flagged offscreen == True, then delete that bullet at that location in the list.
Otherwise if that particular bullet in said list with that index is flagged hit_something == True then we delete it as well.



When we now run our program it should work just fine, zipping around with our little 'A-Wing' space fighter changing the colors of the text objects we fire exclamation marks at.

That was a lot to take in, object oriented programming in the context of pygame. But once you wrap your head around it, it shouldn't be too daunting. Besides, we just used classical objects for our on screen objects but there might be an even more efficient way - namely pygame's native sprite classes which offer even more functionality in regards to collisions, transformations, removing them from memory and much more.


In the next unit, we'll have a little intermission from coding, to mix things up a bit with a little comic and discussing the importance of story telling and narrative design. In our little technical exercise here there is almost no narrative one might argue, however, if you look around you'll find story telling almost everywhere. It is just a human trait hailing back from the cave dweller days. And so, even our little space ship may be associated within some kind of story like context. Think of all those ancient arcade game cabinets. Space invaders and Galaga/Galaxian and co. even though simplistic by today's standards, tried to invoke certain emotions.

Anyways, I hope you had fun coding. Keep practicing and ever learning. The future is now. See ya around!
If you ever feel like chatting, visit me on instagram at merlinsolisstewart.


        




    



No comments:

Post a Comment

Pygame Unit 04 - Make An Invaders Game: Part 1

Pygame Tutorial 04 - Let's make an Invaders style shooter Part 1 Now that we have some of the basics down in previous tutorials, let...