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.


        




    



Thursday, 15 June 2023

Let's Study Programming by Making Games: Pygame Unit: 02 - bouncy_text.py

 

bouncy_text.py



Hi there! Welcome again to tutorial lesson 02 of Python and Pygame. Before we start out with our first larger project tutorial project (working title: "ASCII'n Vaders"), we'll continue going over some more basics and movement. In the last lesson we got our "Hello Pygame World!" text string moving from right to left and then rinse and repeat.

This time our objective is to bounce it around within the borders of our screen surface. Here I have included listing within our source file bouncy_text.py or whatever you may name it:


#bouncy_text.py
import pygame, sys

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

CYAN_BLUE = ("#4B8BBE")
SHANDY = ("#FFE873")

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

def draw_text(font, text, x, y, color):
    text_img = font.render(text, True, color)
    text_img_rect = text_img.get_rect(center = (x, y))
    screen.blit(text_img, text_img_rect)
    return text_img_rect



text_x, text_y = SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2
text_x_speed = 5
text_y_speed = 2
text_x_dir = 1
text_y_dir = 1

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

    text_rect = draw_text(font_60, "Hello Pygame World!", text_x, text_y, CYAN_BLUE)
    



    x_check = text_rect.right < 0 or text_rect.left > SCREEN_WIDTH
    y_check = text_rect.bottom < 0 or text_rect.top > SCREEN_HEIGHT
    if x_check:
        text_x_dir *= -1
    if y_check:
        text_y_dir *= -1



    text_x += text_x_speed * text_x_dir
    text_y += text_y_speed * text_y_dir


    


    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()



Ok, I hope you took a good look. Nothing special so far if you have some familiarity with any programming language and Python's syntax in general. Let's go over the lines of code that are different from our last lesson.

def draw_text(font, text, x, y, color):
    text_img = font.render(text, True, color)
    text_img_rect = text_img.get_rect(center = (x, y))
    screen.blit(text_img, text_img_rect)
    return text_img_rect



The first slight but change we've introduced introduced in here is the return statement within our text draw function. This enables us to get the rectangle data out of the function to be used within our main loop as an easy means to check where exactly our text is position at a given moment in time and compare it to the coordinates of our screen borders.

text_rect = draw_text(font_60, "Hello Pygame World!", text_x, text_y, CYAN_BLUE)

Said rectangle will be captured in a variable called text_rect. The arguments within the function brackets stay pretty much same. Nothing new here.

Now we come to the actual section where the positional checking takes place.

x_check = text_rect.right < 0 or text_rect.left > SCREEN_WIDTH
y_check = text_rect.bottom < 0 or text_rect.top > SCREEN_HEIGHT
if x_check:
    text_x_dir *= -1
if y_check:
        text_y_dir *= -1



In here we check whether the right hand side of the text rectangle is less than 0 or the left hand side is greater than our screen width. So in either case the expression will return true and stored in a boolean variable we named x_check which as the name suggests accounts for the x - axis or the x coordinates of our text rectangle.

Of course the same has be done with the y - axis now only comparing the bottom of the rectangle with the top of the screen as well as checking if the rectangle top has gone beyond the bottom of the screen.

The next two lines are pretty straight forward. If the x_check evaluates to true, it is going to reverse the text_x_dir directional variable by multiplying it with negative one. If it is headed right say has a value of positive one, it is going to be reversed to negative one and vice versa, giving us a simple back bouncing effect.
Same principle holds true with the y_check, reversing the direction whenever the text has gone either past the top or bottom of the screen. Both directional variables of course had to be defined beforehand in the space above our main game loop, underneath our other initials regarding speed and position of the text rectangle.

text_x, text_y = SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2
text_x_speed = 5
text_y_speed = 2
text_x_dir = 1
text_y_dir = 1



Just between our visibility check code and the even handler, we'll place our movement code which updates the text rectangles x and y position respectively to blit it at a new position with each main loop iteration, by multiplying the speed values with the respective directions and storing it back in each positional variable.

text_x += text_x_speed * text_x_dir
text_y += text_y_speed * text_y_dir


And tadaa! If you hit the run button our text will whoosh around the screen and bounce back once it is offscreen, just like one of those antiquated CRT-screensavers from the 90's and earlier. (Yes, don't ask. I am that old already).


Ok, that's a short one for now. But it is gonna get more exciting as we go along. In the next lesson we'll learn why object oriented programming is quite a practical thing if you don't want to repeat the same code over and over again, especially if you want to draw many objects to your screen (what real games generally do).
Bye for now. Have a good one. And have fun practicing Python! I'll also throw in a cartoon or two next time to keep the motivation up :)



Wednesday, 7 June 2023

Let's Study Programming by Making Games: Pygame Unit 01


Hello again, greetings and welcome to Pygame Unit 01. Yes, we have started with Unit 00. If you are already acquainted with computer science and programming you may know about the fact that computers and by extension programmers start counting with 0. So it is a good opportunity to get used to it especially when we start using lists and their indices extensively.

Anyways, before we continue with some on screen movement, let's take a short glimpse at our last source file, particularly the few lines I've added to our bare bones code I added at the end of lesson 0.

First of all, you'll notice the line pygame.display.set_caption("Hello Pygame World!").
This command is very straight forward as it simply writes the we entered into the method right into the title bar beside our program icon (I'll also show you later how to customize the pygame icon as well).

The next line is a little more tricky: clock = pygame.time.Clock()

It creates a clock object that we use to measure time by calling clock.tick() each iteration of the main loop. Normally the loop would iterate as fast as our processor us to do so. This can actually turn into a major issue, as one computer may be faster than another, leading to sluggishly slow animations on one machine while having lightning fast motion on another leaving the player unable to react properly to the gameplay. This is not exactly one any budding game dev wants as a user experience.

By feeding in the value we stored in FPS, in this case 60, we can mitigate at least one part of the problem. This way each iteration of the main loop is capped to a maximum of 1/60th of a second giving effectively 60 frames per second as the upper limit.
It still may dip down occasionally depending on the number and complexity of the objects drawn to the screen, and floating point calculations done in the background etc, but for now this is the easiest way help us keep our frame rate roughly stable. Another method for frame rate independence is time based movement by multiplying the change in time called delta time as a factor to each object movement, scaling, transformation involved on the screen. But for now we'll stick to the easier method with fewer lines of code.

So, again hit the run button in your favorite IDE and be amazed of wonderful blue letters on a yellow background. Another point of notice is an additional event check in the event handler. This one refers to the pygame.KEYDOWN event and the if - statement scans for the particular pygame.K_ESCAPE key. This means, whenever you hit the escape button on your keyboard, you'll be able to instantly exit out of the loop. Just a neat little addition as a quick emergency exit button. There are many more scancodes for buttonpresses, input and game controller usage. We'll cover them later, promised.

Now it is about time to get some movement into our text.

First we'll create a couple of additional variables to account for the changing text postion, speed and direction.




Let's go just above our main loop and create text_x and text_y, assign SCREEN_WIDTH // 2 and SCREEN_HEIGHT // 2 as coordinates to them which will give our text intially a centered position on screen.

Beneath it, we'll give text_x_speed a value of 5 pixel per frame, so roughly about 300 pixels per second given a frame rate of 60 FPS.

Back in our main loop we'll slot in text_x and text_y variables as arguments into our draw_text function. Right beneath it, in order to change the text position with each loop iteration, we'll subtract text_x_speed from tex_x to move it to the left.

The expression text_x -= text_x_speed is equivalent to text_x = text_x - text_x_speed
This means that each loop iteration 5 is subtracted from text_x and stored back in text_x for usage in our draw text function.

By the way, if you haven't been aware of it by now, you may have noticed that coordinate system on our computer screen differs a bit from what we've learned back in high school. While the x axis behaves pretty much the same, the y axis is in fact reversed corresponding to a lower position on screen the higher the y value gets. This puts the point origin (0, 0) in the topleft of the screen rather than its classical cartesian math class equivalent that situated in the lower left corner. But no problem at all. With a little bit of experimentation and practice it's gonna be a piece of cake.

Alright. With these few lines done, we it should technically work. Once we run this, the text will indeed move to left, disappear at the screen border and just keep moving while out of view. Quite fine, but we want something a little more sophisticated, resetting its position and moving left within our viewport again.

To do that, we'll add another check beneath the text_x update.
if text_x < -500:
    text_x = SCREEN_WIDTH + 500


This introduces a threshold at the left hand side offscreen. Once the text_x variable is about 500 pixels to the left of our screen, we'll assign another value to it: 500 pixels to the right of the screen. And tada, our text object is teleported to the right, ready to fly by our screen anew. This repeats over and over until we decide to break out of the loop.

Alrighty then! This is all for the moment. But no worries! In our next exercise we'll create some more text objects, bounce them around on our screen, while getting familiar with object oriented programming based on classes and at some later point move them with keyboard input.
See ya next time. Have fun programming and keep at it!


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...