MrValdez's Blog

Game Engine Development: Pygame - Part 2

Posted on Nov. 20, 2018

Part 1

Sprites

Right now, we only have the game window. Its boring.

Open up your paint application. I believe Microsoft Paint is installed by default in most computers. Don't be shy with Paint, we are programmers not artists. We'll start with programmer's art and change it later.

Kirby is the most famous example of a programmer art. Not knowing how to draw is not an excuse to start

We are going to draw our main character.

Here's my attempt at making a game character. I saved this as hero.png.

You may laugh, but in my experience, a lot of students want their first attempt to be flawless. This have a negative effect on programming momentum and finishing the project. The sooner you can accept that getting the code running is more important than how pretty your game is, the faster you can prototype.

Let's add our hero to the game:

import pygame

resolution = (640, 480)
isRunning = True

pygame.init()
screen = pygame.display.set_mode(resolution)

hero = pygame.image.load("hero.png")
hero_pos = [0, 0]

while isRunning:
    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                isRunning = False

        if event.type == pygame.QUIT:
            isRunning  = False

    screen.blit(hero, hero_pos)

    pygame.display.flip()

pygame.quit()

This code will load the hero sprite for the game. hero will contain the image and hero_pos have the x, y coordinate.

When we put a sprite on screen, we call it "blitting". screen.blit() handles this part. I intentionally skipped discussing pygame.display.flip() until we are at this section.

First, a review: animation is just a series of static non-moving objects. Each frame is drawn by an artist and the next frame have a similar, but not quite similar, drawing to the previous frame. The video below shows a how a flipbook works:

In the old days of early computing, drawing an image is slow. If you draw an image, clear the screen, then draw a new one, you will see flickering. Video cards have a buffer (or a section of memory) which tells the output what to draw. Whatever you write (or blit) on this buffer will appear on screen.

Early programmers realized that we can make two video buffers (which they called the front and back buffer). The apllication can draw on a back buffer while the slow video card display the front buffer. Then, when the program is ready, we flip between the front and back buffer. This is just a simple pointer redirection (although modern video cards do this on hardware), so its fast and can minimize flickering.

pygame.display.flip() is the command to do the above. Some games delay the flip every 20 frames per second. Most flip as fast as possible. For this tutorial, we'll be using the latter. We might discuss the advantages of the former in an article far into the future (it's in my list of things to do about in this series)

Our repository should look like this now.

Moving the sprite

I kept saying animation, but our hero isn't moving. Let's fix that. Every frame, we're going to make them move by 7 units forward.

import pygame

resolution = (640, 480)
isRunning = True

pygame.init()
screen = pygame.display.set_mode(resolution)

hero = pygame.image.load("hero.png")
hero_pos = [0, 0]

while isRunning:
    for event in pygame.event.get():
        if event.type == pygame.KEYDOWN:
            if event.key == pygame.K_ESCAPE:
                isRunning = False

        if event.type == pygame.QUIT:
            isRunning  = False

    hero_pos[0] += 10
    hero_pos[1] += 10

    screen.blit(hero, hero_pos)

    pygame.display.flip()

pygame.quit()

Did you see that? We are now moving at the speed of light! ...technically. You see, we are looping as fast as our CPU can let us[1].

Different CPU speed have a direct correlation with each tick of the game loop. What we need is an algorithm to slow down our game loop if we are going too fast and skip frames if we are going too slow.

[1] CPUs run on electricity, you see. And that's running at almost the speed of light.

Good thing pygame already implemented this algorithm:

import pygame

resolution = (640, 480)
isRunning = True

pygame.init()
screen = pygame.display.set_mode(resolution)
clock = pygame.time.Clock()

hero = pygame.image.load("hero.png")
hero_pos = [0, 0]

while isRunning:
    tick = clock.tick(60)

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

        if event.type == pygame.QUIT:
            isRunning  = False

    hero_pos[0] += 10
    hero_pos[1] += 10

    screen.blit(hero, hero_pos)

    pygame.display.flip()

pygame.quit()

pygame.tick.Clock() returns a pygame clock instance. Every iteration, we run clock.tick(fps) and pygame will try to maintain the given frames per second. While the best user experience is 60fps (and the magical 120fps is recommended for virtual reality), if your rendering is real slow, it can dip below that.

We can add print(tick) to see how many milliseconds it took to render one frame. But take note that print commands slow down the game since we're doing an I/O operation. We'll discuss how to print text on screen later.

Fixing the movement artifacts

The first artifact that we need to fix is the non-transparent background of our sprite.

To humans, the sprite that we should see is the stickman. But to computers, the whole image needs to be rendered. We need to tell pygame which part is transparent and which is not.

We can open a fancy image program and mark specific parts of our png image as transparent via the alpha channel. Or, we could use what old school game engines do: use a color key. Check the new image and code below:

I've chosen the color 255, 128, 128 (red, green, blue) as the color key, but you can use whatever you want.

import pygame

resolution = (640, 480)
isRunning = True

pygame.init()
screen = pygame.display.set_mode(resolution)
clock = pygame.time.Clock()

hero = pygame.image.load("hero.png").convert()
hero.set_colorkey((255, 128, 128))
hero_pos = [0, 0]

while isRunning:
    screen.fill((255, 255, 255))
    tick = clock.tick(60)

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

        if event.type == pygame.QUIT:
            isRunning  = False

    hero_pos[0] += 10
    hero_pos[1] += 10

    screen.blit(hero, hero_pos)

    pygame.display.flip()

pygame.quit()

Before this section, we just use load up a png file. Every time we blit, the png is converted with into pygame's image format and then blitted. This is slow. We removed this extra step In the above code.

Pygame have its own internal image format that is optimized for speed and modification via code. We use image.convert() to do this before the game loop. With the file now converted, we can modify the image and tell pygame that if it tries to blit a pixel with the same color as the colorkey, it should not be blitted.

After running the game, you should see the above image. The image is now transparent! But we are getting a different artifact.

You see, when we blitted on the screen, we never cleared it. In essence, we are just drawing on top of what was already drawn last frame.

There's two ways of fixing this: draw a background image every frame, or clear the screen every frame. Both are valid and for the sake of simplicity, we will be using the latter (I don't want to draw a background image with programming art, for now).

import pygame

resolution = (640, 480)
isRunning = True

pygame.init()
screen = pygame.display.set_mode(resolution)
clock = pygame.time.Clock()

hero = pygame.image.load("hero.png").convert()
hero.set_colorkey((255, 128, 128))
hero_pos = [0, 0]

while isRunning:
    screen.fill((255, 255, 255))
    tick = clock.tick(60)

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

        if event.type == pygame.QUIT:
            isRunning  = False

    hero_pos[0] += 10
    hero_pos[1] += 10

    screen.blit(hero, hero_pos)

    pygame.display.flip()

pygame.quit()

Every frame, we fill the screen with a white background color. You can use a different color if you want.

After you run the game, you will realize something: this isn't a game. This is just a screensaver.

Sprite movement

A large part of video games is that a player can control the game. So let's do that:

import pygame

resolution = (640, 480)
isRunning = True

pygame.init()
screen = pygame.display.set_mode(resolution)
clock = pygame.time.Clock()

hero = pygame.image.load("hero.png").convert()
hero.set_colorkey((255, 128, 128))
hero_pos = [0, 0]

while isRunning:
    screen.fill((255, 255, 255))
    tick = clock.tick(60)

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

        if event.type == pygame.QUIT:
            isRunning  = False

    keystate = pygame.key.get_pressed()
    if keystate[pygame.K_LEFT]:
        hero_pos[0] -= 10
    if keystate[pygame.K_RIGHT]:
        hero_pos[0] += 10
    if keystate[pygame.K_UP]:
        hero_pos[1] -= 10
    if keystate[pygame.K_DOWN]:
        hero_pos[1] += 10

    screen.blit(hero, hero_pos)

    pygame.display.flip()

pygame.quit()

We can use the events from pygame.event.get() to check which keys are pressed. In my games, I separate the game inputs from the OS events. So I use pygame.key.get_pressed() to check if a key is pressed.

You can check this page for a list of the available keys.

Notice that we check each key separately. We did not use a elif above. This will allow us to press multiple buttons at the same time.

Pygame also have a way to check for joystick button presses, but since not everyone have a joystick, we will skip this part. But if you are releasing your game, I highly recommend supporting joysticks. I know I enjoy playing on them:


Next, we'll talk on the game objects pattern. Also, we'll do a little refactoring.

In the meantime, play around with the game by downloading from the repository.

Categories: gamedev