Sunday, 6 August 2023

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's go and build a slighty bigger exercise project that has some of the most common elements found in almost any computer or video game like object movement, player input, collision and the like. This example takes inspiration from Taito's arcade classic hit 'Space Invaders' and we'll be trying to replicate some of the mechanics though not slavishly adhering to them. There is still some wiggle room for own interpretations. So, for laughs and giggles let's call this title "Commander Sheppy VS. The U.A.P's". (You may call it any name of your choosing but this one just came to mind when I did some digital drawings, and if you happen to be a "Mass Effect" fan you might catch the reference.)

Anyways, we'll start with the basic setup, drawing the background and displaying a moveable player character on screen, as well as the HUD at the top of the viewport.

Before we start coding however, we've got to set up the folder structure in order for the source code to find the dependencies aka the assets like graphics and sound files. Simply create a project folder eg. "SheppyVsUAPs" and place your main.py file in there. Then create two sub folders, one called "img" and another one labeled as "sound".

Of course you can also download my personal project folder at my gitHub account here: https://github.com/MerlinSolis  if you wish to follow along. The complete main.py file as well as all images, sounds, as well as the font we use for the U.I. are included.



Step 1: Creating the Window


First are going to import the modules we need for our project:

# imports
import pygame as pg
import sys
from pygame.math import Vector2


As you can see I have imported pygame as pg, just a little shorthand making typos less likely and saving myself some typing time ;)
sys for now we'll just use to cleanly exit our interpreter at the end of our code and from pygame.math we'll import the Vector2 class in order to make movement of our character, bullets and ufos a bit easier.

# basic setup
pg.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 1024, 768
screen = pg.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT), 16)
clock = pg.time.Clock()
FPS = 60
pg.display.set_caption("Sheppy VS The UAP's")



This gives us the basic framework for a pygame window by initializing pygame, setting the resolution and creating a display surface to draw on. After that comes our frame timer, that will cap the execution of the main loop to around 60 frames per second. Further we set the caption in our window's title bar to the incredibly catchy title "Sheppy VS The UAP's".


Next comes the main loop to keep the window open and where we'll be updating our objects each frame once we have them defined properly.

# background base color black
bg_col = (0,0,0)
# loop condition
run = True
while run:
    # frame timer
    clock.tick(FPS)
    # show the caption together with the current frames per second value
    pg.display.set_caption(f"Sheppy VS The UAPs | FPS: {clock.get_fps():.1f}")
    # fill the background with black
    screen.fill(bg_col)

    

    # event handling loop
    for event in pg.event.get():
        # window X quits the program
        if event.type == pg.QUIT:
            run = False
        # Escape (boss) key quits the program
        if event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE:
            run = False




    # update the screen
    pg.display.update()

# exit the interpreter
pg.quit()
sys.exit()



So, when we run this now, we'll get a black rectangle with a given size of 1024 by 768 pixels to be used as our canvas drawing all the game objects we need.


Step 2: Loading the Background Image and displaying it


All fine and dandy but we'll add a legit background image for this type of game. Let's create an image loading area under our initial set-up area:


# loading images
img_bg = pg.image.load("img/background-black.png").convert()
img_bg = pg.transform.scale(img_bg, (SCREEN_WIDTH, SCREEN_HEIGHT))


Here we load the starry background image in our img folder, convert it into pygame's native format and then scaling it to the size of our display surface.


lastly we'll have to blit it to screen inside the main loop to make it visible. Just beneath the screen.fill(bg_col) command we'll write:

# draw starry background image
screen.blit(img_bg, (0, 0))



Ok, the starry sky is set, however we still need some sort of ground for our character to walk on. A ground that is made of square tiles.

Step 3: Creating the Ground



First we have to load 32 x 32 square tile from our image directory at the top in our image loading section.

img_ground_tile = pg.image.load("img/ground_tile.png").convert_alpha()


This will give us the image but we'll a little more than that. We'll create an empty ground tile list which will be used to store our individual tile objects. Of course we also need a grid which comes as a two dimensional list where each list entry corresponds to an individual row on the grid, and the numeric values in each row represent each column.

# ground set-up
ground_tile_list = []
ground_layout = [
        #0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1,2,3,4,5,6,7,8,9,0,1
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],#0
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],#1
        [1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1],#2
        [1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1] #3
    ]


As you may have noticed, these lists within the list store 0 and 1 integers. I've decided to encode the areas where a tile is placed as '1' and empty spaces as '0' respectively.

We'll also create a custom class that takes in all the attributes needed for each ground tile.

# blueprint for tiles
class Tile():
    def __init__(self, pos, image, scale=1):
        self.image = pg.transform.scale(image, (image.get_width() * scale, image.get_height() * scale))
        self.rect = self.image.get_rect(topleft = (pos))

    def draw(self):
        screen.blit(self.image, self.rect)

    def update(self):
        self.draw()


This comes in handy as we not only want to create a tile floor but also the destructable heaps of cover behind which the player can hide from incoming shots.
As you can see the tile class is very simple, just scales the image, positions it with the help of a rectangle and finally draws it to the screen. The rectangle will also be useful to determine whether a bullet has collided with a tile which subsequently will be deleted to mimic a degrading cover wall. Later more to that.


Now that we have the components, we need to assemble the tiles. To do that we define a function named generate_ground.

# create the tiled ground
def generate_ground(ground_tile_list, ground_layout, Tile, tile_img):
    for row_index, row in enumerate(ground_layout):
        for col_index, col in enumerate(row):
            if col == 1:
                x_pos = col_index * tile_img.get_width()
                y_pos = (SCREEN_HEIGHT - tile_img.get_height() * 4) + (row_index * tile_img.get_height())
                new_tile = Tile((x_pos, y_pos), tile_img)
                ground_tile_list.append(new_tile)
    return ground_tile_list

ground_tile_list = generate_ground(ground_tile_list, ground_layout, Tile, img_ground_tile)


It does what the name suggests, and loops through each row and column of the ground_layout, pastes a tile at the given x and y positions, and saves it in the ground_tile_list. Once all tiles have been placed according to the layout, we can now iterate through each and every one of them within the ground tile list to draw them onto the display surface.

Just underneath where we blitted the starry night sky image, we can add a for-loop that does exactly that:

# draw the tiled ground
for tile in ground_tile_list:
        tile.update()



If now run the program, we'll see the ground tiles positioned at the bottom of the screen, 30 square tiles accross, and 4 squares from the bottom. The spaces that were designated as '0' in the ground_layout list are of course void of any tile. That's where we are going to place our player character, "Sheppy".
    



Step 4: Create the Player


Initially we'll have to load the raw sprite sheets of our venerable Commander Shepard into its file handles respective to walking directions and scale them up to an acceptable size, in this case by a factor of 2, but you may tinker with it at your own gusto and see how large or small you wish to make your game character sprite.

scale_factor = 2
img_shep_walk_left = pg.image.load("img/shepard_walk_left.png").convert_alpha()
img_shep_walk_left = pg.transform.scale(img_shep_walk_left,
                                        (img_shep_walk_left.get_width() * scale_factor,
                                         img_shep_walk_left.get_height() * scale_factor))
img_shep_walk_right = pg.image.load("img/shepard_walk_right.png").convert_alpha()
img_shep_walk_right = pg.transform.scale(img_shep_walk_right,
                                         (img_shep_walk_right.get_width() * scale_factor,
                                          img_shep_walk_right.get_height() * scale_factor))


We also require a dictionary into which we put our frames for easy access by using keywords such as "walk_left" and "walk_right".

# the sliced images are going to be stored here!
animation_dict = {
    "walk_left": [],
    "walk_right": []
    }


You'll notice that both lists on the value side of the dictionary are still empty. We are going to fill them with frames in just a moment.


In order to use the individual frames on those two .png files, we need to write a custom function that extracts the frames from each image sheet. The function takes in two arguments, the raw_image_sheet, of which we have two, and the number of frames each sheet holds. Basically we loop the number of frames contained on the sheet and store each of them in a temporary variable. For the image slicing to occur, we use the .subsurface(x_posistion, y_position, width, height) method. Right underneath the extracted frame is appended to the animation list.
Once the for loop is done with extracting all frames and storing them in the animation list, this list is being returned by the function ready to be used outside the function.


# loading a raw image sheet and returning a sliced sprite list
def load_sprite_sheet(raw_image_sheet, animation_steps):
    animation_list = []
    for x in range(animation_steps):
        temp_img = raw_image_sheet.subsurface(x * (raw_image_sheet.get_width() / 4), 0, (raw_image_sheet.get_width() / 4),
                                              raw_image_sheet.get_height())
        animation_list.append(temp_img)
    return animation_list

# store the sliced frames in the animation dictionary
animation_dict["walk_left"] = load_sprite_sheet(img_shep_walk_left, 4)
animation_dict["walk_right"] = load_sprite_sheet(img_shep_walk_right, 4)


The returned lists are stored in the animation dictionary under their corresponding keywords "walk_left" and "walk_right".
Awesome! Now that we have the player images ready, we only need a Player class to control them. Let's create one.



# blue print for the player character
class PlayerObject():
    def __init__(self, pos, speed, direction, animation_dict):
        self.anim_update_interval = 120 # ms per frame
        self.frame_update_time = pg.time.get_ticks()
        self.pos = Vector2(pos)
        self.direction = Vector2(direction)
        self.frame_index = 0
        self.action = "walk_right"
        self.animation_dict = animation_dict
        self.image = self.animation_dict[self.action][self.frame_index]
        self.rect = self.image.get_rect(center = (self.pos))
        self.delta_x = 0
        self.speed = speed

    def update_action(self, new_action):
        if new_action != self.action:
            self.action = new_action
            self.frame_index = 0
            self.frame_update_time = pg.time.get_ticks()

        
    
    def get_input(self):
        keys = pg.key.get_pressed()
        if keys[pg.K_d]:
            self.direction.x = 1
            self.update_action("walk_right")
        elif keys[pg.K_a]:
            self.direction.x = -1
            self.update_action("walk_left")
        else:
            self.direction.x = 0
            self.frame_index = 0

    def screen_boundary_check(self):
        if self.rect.left + self.delta_x < 0:
            self.delta_x = 0 - self.rect.left
            self.frame_index = 0
        elif self.rect.right + self.delta_x > SCREEN_WIDTH:
            self.delta_x = SCREEN_WIDTH - self.rect.right
            self.frame_index = 0

            

    def move(self):
        self.delta_x = self.speed * self.direction.x
            self.screen_boundary_check()
        self.pos.x += self.delta_x
        self.rect.center = self.pos

    def animate(self):
        self.image = self.animation_dict[self.action][self.frame_index]
        current_time = pg.time.get_ticks()
        if current_time - self.frame_update_time > self.frame_update_time:
            self.frame_index += 1
            self.frame_update_time = pg.time.get_ticks()

        if self.frame_index >= len(self.animation_dict[self.action]):
            self.frame_index = 0

    def draw(self):
        screen.blit(self.image, self.rect)

    
    def update(self):
        self.get_input()
        self.move()
        self.animate()
        self.draw()

        

This gives us the framework for our player. The player class constructors constructor takes position, speed, direction, and the animation dictionary as parameters to be initialized and captured in the def __init__() method for later use within the class.
Our player class consists also of following methods, that do a specific part in the functioning of the player sprite.

def update_action(self, new_action) changes the animation walking cycle from left to right once it is called in the input method. For example, if we are currently walking right with our player and then hitting the button that is associated with walking left, the animation (called action in here) switches to the left facing frame list, resets the frame index to 0 so that it starts from the beginning and also resets the frame timer to a new time stamp.

Besides switching the animation frame sets by calling update_action(), the input method also sets the direction vector on the x axis to 1 for right, and -1 for left. If neither the 'd'-key nor 'a'-key are pressed, it simply sets the direction vector to 0, so no movement at all, and the frame_index to 0, which means the first frame of the current set practically stopping the animation from walking to standing.

The screen_boundary_check method checks whether the player character's bounding rectangle is getting close to either the left side of the screen or the right side and moves the player as long as the distance to the screen boundary is greater than 0. Once the player is too close, the movement is restricted and the animation is stopped by setting the frame index to 0.

Move() does what the name suggests by multiplying speed by direction and storing it in delta_x which is then used to update the position vector and the bounding rectangle of the player. It also restricts the movement once the boundary check method comes into play.

The animate() method acts as a kind of flip book, looping through the images stored in the animation dictionary, updating each image when the animation timer exceeds the specified interval, in our case 120 milliseconds. Once the loop cycle is completed, it resets the frame index to 0 just to repeat it all over again.

The draw() method simply blits the image to the display surface at the position specified by the position of the bounding rectangle.

The update() method wraps all other remaining methods into itself and is called in the main loop applying all changes at around 60 times a second.

If we now create an instance called "sheppy":

sheppy = PlayerObject((SCREEN_WIDTH / 2, SCREEN_HEIGHT - 100), 5, (0,0), animation_dict)


and call sheppy.update() in the main loop, we'll be able to move Sheppy around at the bottom of the screen in the area where we created our ground tiles earlier.






This will be all for the moment. See you all in the next section of this particular tutorial or another topic around programming, design, graphics, entertaining tech and the likes that are in the making. Have a good one! Yours truly, Merlin.

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!


Monday, 29 May 2023

Let's study programming by making games: Pygame Unit 0


"Hello Python World!"

Pygame Tutorial 0


Greetings, hello and welcome to Merlin's relaxing space of learning and
creative past times around the world of coding, digital art and general
nerdy-ness.

Today we are going to start off our informal class (no pressure at all) with a
teeny-weeny bit of Python. No, I am not referring to the infamous and
dangerous snake species, although this kind of python does have snakes in its
neat logo and may be likewise as intoxicating (in a positive way).

The Python I am talking about is the general purpose programming language that
really spread out all around the world, both in the corporate and the open
source community after its release in 1991 by its designer Guido van Rossum.
Nowadays you can find it almost anywhere in the computer science and tech
world mainly due to its beginner friendly structure that does a lot data
hiding and blackboxing as compared to languages like C or C++, as well as the
great variety of libraries and modules that are available for almost any
conceivable programming problem out there.

It is therefore no wonder that the Python language is often spoken in the
same breath with big modern buzzwords such as 'Machine Learning', 'Artificial



 

 

 

Intelligence', 'Data Science', 'Robotics', 'Cybersecurity' and many more. So
in any way, whether you wish to pursue a professional career in those or
similar fields or simply love to tinker with technology in a creative way,
feel free to download your personal Python copy from python.org free of cost.

And although we might not be writing code for future moon- and mars
colonization missions, interstellar spaceship propulsion or your run of the
mill sentient super computer, we still may benefit from knowing at a least a
few technical details. Similarly, it may not be necessary to know every
function of the inner workings of an automotive combustion engine, yet knowing
for example the key differences of a diesel, gasoline, or electrical motor
definetely may come in handy.

Anyways, learning a programming language can at times become quite the tedium, just like all things learned by theory, practice, repetition, memorization and so on. However if we wrap everything into a meaningful narrative and
combine it with something that provides visible results, we might smoothen the
otherwise bumpy ride and even have some fun at the same time.
Gamification is another magic word in the learner's community - learning by
using techniques employed in games. In our case we'll not only play games but
also indulge in the challenge of making games.




In order to do that we'll have one of Python's most enjoyable modules named
Pygame to our disposal which basically is a library of functions, classes,
methods - tools that allow us to manipulate graphics and sound with relative
ease. Though not as powerful as say engines like Godot, Unity and Unreal, to
name a few, it is a good didactic tool teaching game programming routines that
also apply to other systems. Pygame is especially useful in getting the ropes
of 2D and pseudo 3D game programming akin to the "golden age" of videogames
with its arcade machines, as well as the era of NES, SNES and Sega
Genesis/Megadrive homeconsoles. And depending on one's experience and skill
level interesting self-made games are in one's reach.

Alrighty then! Once you've made your mind, pip install the pygame library in
your windows command line and we're ready to go!


Ok, without further ado, let's christen our dev-journey with the simple
one-liner almost everybody starts out with. Let's just keep the tradition
alive, shall we?

Just type:


print("Hello World!")

into your favorite text editor, save it as hello.py and run it with the Python

interpreter. Easy, isn't it? It is similar to other code lines you might
have encountered some other time, for example:

cout << "Hello World!" << endl;

in C++ or:

console.log("Hello World!");

in JavaScript. Both require some 'boilerplate code' beforehand to make them
work, which in Python is simply abstracted away.


Anyhow, now that we have done our duty to tradition, let's mix it up a bit
with our custom pygame hello world program.

Let's start by creating the basic pygame window. First we have to import the
pygame module.


import pygame

Additionally we will also import the sys module in order to call sys.exit() at
the end of the program, enabling us cleanly shut the python interpreter down
once we exit the program.

import sys

Our next line will initialize pygame by calling:

pygame.init()

Now we are going to define the size of our prospective screen in pixels:

SCREEN_WIDTH = 1024
SCREEN_HEIGHT = 768


I've written those variable names in capitals just to denote that these values
are not to be changed during program run time.

Next we'll have to create a display surface:

screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))

This line stores the display surface object returned by the .set_mode() method
which takes our screensize values as arguments for further use.

But technically these few lines should be enough to show something on screen.
Feel free to run your interpreter. And if you watch closely, you should notice
a black window flashing and then disappearing after a few fractions of a
second. A good sign, but not actually what we are looking to achieve. We
somehow have to keep this window open.

In order to do that we need something that is called 'game loop' or 'main
loop'. A loop is a structure that iterates as fast as possible through all the
lines of code it contains in repetitive succession until an exit condition is
met.

First we'll define a boolean variable called 'run' and assign the value 'True'
to it.

run = True
while run:
    # do something
    # draw something
    # calculate something
    # check collision
    # update movement
    # take input

    pygame.display.update()
    # alternatively pygame.display.flip()



This is followed by the while key word followed by the boolean condition and a
colon. After that we repeat through all the lines of code we've put into the
loop indented by one level (usually 4 spaces). All lines that are preceded by
a # symbol are considered comments and therefore ignored by the interpreter or
compiler.

At the very end of our main loop we put our pygame.display.update() method or
as an alternative, the pygame.display.flip method. In layman's terms this line
updates all the changes made in the top section of the main loop in order to
draw it to the screen. As an analalogy just visualize drawing an image onto
one side of a piece of cardboard and after being done flipping it to the
front, enabling your friend to see what you have drawn. Only difference that
this usually happens as fast as your computer can possibly run (which can even
be too fast requiring a system of frame rate independence or at least frame
limiting to properly animate).

Ok, one step further. We finally have a black surface in the proper size we
defined beforehand in our SCREEN_WIDTH and SCREEN_HIGHT constants. The only
problem we have now is the inability to quit out of the loop and end the
program from within itself. Only we can do is use the task manager or force
quit the interpreter. Not ideal if we'd rather use a button or the X in the
upper right corner of the window.

What we need now is an event handler, something that listens for inputs that
we can grab and links them up with the exit condition of our loop.


run = True
while run:
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
    

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


With the event handler in form of a for loop that iterates through all
possible pygame events, we use an if statement to isolate the event.type ==
pygame.QUIT. this event refers to a mouseclick on the window close button in
the upper right. Once we click on the X, the run variable is set to 'False'
and we exit loop deactivating pygame with the quit method and exiting the
system.

Neat, now we've got the absolute bare bone frame work for our game window. But
we're far from done yet. Let's display something. Our print() statement we've
used earlier won't do us any good in this case, however.

What we basically got to do is loading a font stored either in our folder
structure or one of the standard windows fonts or whatever is installed as
part of the operating system. We'll do the latter this time.

Somewhere above our main loop we'll add this line:

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

The SysFont() method takes the name of the font as first argument, and the
desired pixel size as an integer value as the second argument. The returned
font object can now be stored in a variable for further use. In this case we
called it font_60.


Now in order to properly use this font object, let's create a custom draw_text
function which we can then call in our main loop.

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)



This function takes the font object we just created, the input and output text
we want to draw, the x and y coordinates, and the color of the text.

The next line within the body of the function basically rasterizes the font we
feed in, turning it into a pixel image. The font.render() method takes the
text we entered, set's the anti-aliasing to either True or False (so, either
smooth or pixelated edges) and the color we fed in.
From this image we take a rectangle which comes handy in exact positioning as
we assign our x and y coordinates to center handle point of the rectangle.
Depending on how we want to place the image, we may choose between following
points to manipulate: center, topleft, midtop, topright, midright, bottomright,
midbottom, bottomleft, midleft.

The last line in the function body "blits" (bit block transfer) the image to
the display surface 'screen' at position of text_img_rect.


Before we call the draw function in the main loop, let us define some colors
somewhere at the top.

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


In this case we used hexadecimal values but color names or RGB values (0-255)
can be plugged in as well.


Now that we have them, let's return to our main loop.

run = True
while run:
    # we can fill the background with a color
    screen.fill(SHANDY)
    
    # call the text function to print message to pygame window
    draw_text(font_60, "Hello Pygame World!",
        SCREEN_WIDTH // 2, SCREEN_HEIGHT // 2,
        CYAN_BLUE)

    
    # event handler
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
    

    # screen update
    pygame.display.update()

# exit program
pygame.quit()
sys.exit()



Ok, this should give us a slight pygame-esque variation to the standard "Hello
World!" program. No worries, these are the simple beginnings. In our next
lesson we'll continue with a little bit more motion, animation, collisions and
more. See you around and have a good one. 

And keep playing and tinkering without fear of making mistakes - all part of the learning process.
Here is a screen shot of our complete starter code. As you might notice, there are a few additional lines. But you surely won't have trouble figuring out their meaning. We'll go into detail at a later point anyways.







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