Building Chrome Dinosaur Game in Pygame (Part 3: Running Dino)


In the last post, we coded up the infinite scrolling horizon upon which the Dino runs and in the process we learned how to display images on the canvas, how coordinates work in pygame, how to use the pygame.time.Clock class in controlling the Refresh rate of the video game among a few other lessons. In this post, we’ll code up the Dino and give it the ability to run for dear life.

But before we go any further, I need to make it clear that I’m assuming the following things about you:

  • You understand basic English. Trust me, this is not a given.
  • You possess basic knowledge of the Python programming language. If not, check out this coding resource to learn.
  • You’re following along on a Mac, Linux or Windows Laptop or Desktop. You can’t use your smartphone for this I’m afraid.
  • You can comfortably change the Refresh rate of our game without breaking a sweat. If you can’t, then quickly checkout the previous post before proceeding with this one.

Alright, let’s dive in!




Who left a dinosaur on the open road ?

In our previous post, we left our code base looking something like this (adjust your code to match what is displayed):

import pygame
import os

pygame.init()

# Constants
FPS = 60
HORIZON_VEL = 0.8
SCREEN_WIDTH, SCREEN_HEIGHT = 720, 400
WHITE = (255, 255, 255)

WINDOW = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()

pygame.display.set_caption("Gino")

# Fetch the image
HORIZON = pygame.image.load(os.path.join('Assets', 'sprites', 'Horizon.png'))
HORIZON_Y_POS = SCREEN_HEIGHT//2 + SCREEN_HEIGHT//6


# Global variables
offset_x = 0


def draw_window():
    global offset_x
    horizon_tiles = 2
    horizon_width = HORIZON.get_width()

    WINDOW.fill(WHITE)  # White

    for i in range(horizon_tiles):
        WINDOW.blit(HORIZON, (horizon_width * i + offset_x, HORIZON_Y_POS))

    offset_x -= HORIZON_VEL
    if abs(offset_x) > SCREEN_WIDTH + 100:
        offset_x = 0


def main():
    """main code for the game.
    """
    # global offset_x

    game_running = True

    while game_running:
        clock.tick(FPS)
        # Poll for events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_running = False

        draw_window()

        pygame.display.update()

    pygame.quit()


if __name__ == '__main__':
    main()
Enter fullscreen mode

Exit fullscreen mode

So, the first order of business today will be to display the Dino on the canvas. And using what we did in the previous post as a reference, it’s clear we’re talking about displaying an image using the WINDOW.blit() method. So, let’s do that:

...

DINO_WIDTH = 80
DINO_HEIGHT = 80

DINO_STANDING = pygame.transform.scale(
     pygame.image.load(os.path.join('Assets', 'sprites', 'Dino_Standing.png')),
     (DINO_WIDTH, DINO_HEIGHT)
)
DINO_Y_POS = HORIZON_Y_POS - DINO_HEIGHT + DINO_HEIGHT//4

# Global variables
offset_x = 0


def draw_window():
    global offset_x
    horizon_tiles = 2
    horizon_width = HORIZON.get_width()

    WINDOW.fill(WHITE)  # White

    for i in range(horizon_tiles):
        WINDOW.blit(HORIZON, (horizon_width * i + offset_x, HORIZON_Y_POS))

    WINDOW.blit(DINO_STANDING, (30, DINO_Y_POS))

    ...


 ...
Enter fullscreen mode

Exit fullscreen mode

A lot of this code should be familiar to you by now. But if you take a closer look, you’ll notice that we passed the pygame.image.load(...) method as an argument to a pygame.transform.scale(...) method. This method does what it’s name suggests: Scales the width and height of the loaded image to meet specific values which we passed in as a tuple of the DINO_WIDTH and DINO_HEIGHT constants defined above, both set to a value of 80. You’ll also notice that the position of the Dino on the y axis is also calculated based on the initial HORIZON_Y_POS from the last post and assigned to a constant DINO_Y_POS which is used when displaying the DINO_STANDING image loaded earlier through the use of WINDOW.blit(). This is so that the Dino will appear as though it were standing on the horizon

When you run the program, you’ll get something that looks like this:

First Step: Dino standing on scrolling horizon

Wow, our Dino looks good !!!

But it does not seem to be moving. Actually, it’s be standing still while the rest of the world moves past it. We don’t want that, so how do we fix it ?

Essentially, all we need to do is create the illusion of motion. And, the best way to do that is by getting it’s legs to move. But, how do you get the legs of an image to move ? Is that even possible ?

Well, it’s not. But we can switch between two images of the Dino so fast that it would appear as though the Dino were indeed moving it’s legs. And luckily for you, we have two images in the Assets/ directory in our project that can be useful in achieving this:

  • Assets/sprites/Dino_Left_Run.png: An image of the Dino with it’s left leg extended.
  • Assets/sprites/Dino_Right_Run.png: An image of the Dino with it’s right leg extended.

And we’ll use both of these to achieve our goal soon. But right now, you may be thinking: how do we signal to the Dinosaur to switch between two images of itself fast enough to create the illusion of motion ?

In order to achieve this, we need to learn about two new ideas:



Events

An event in pygame is a message that tells your program something has happened — like a key being pressed, the mouse moving, or the game window closing.
You can think of it as a notification system: whenever a specific action occurs, Pygame generates an event. By listening for these events, your program knows when and how to respond.

We’ve actually already worked with events in our code:

...

def main():
    """main code for the game.
    """
    ...

    while game_running:
        ...

        # Poll for events
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                game_running = False

        ...

    pygame.quit()

...
Enter fullscreen mode

Exit fullscreen mode

In the example above, we were listening for the pygame.QUIT event which tells us when the user hits the ❌ on the game window. pygame comes with a lot more events that we can use and it also gives us the ability to define custom events when the need arises:


EVENT_NAME = pygame.USEREVENT + n
Enter fullscreen mode

Exit fullscreen mode

where n is any number from 1 upwards. At the end of the day, pygame.USEREVENT is really just a constant containing a unique number. So, when we add to that number, we’re creating a more unique number to act as our event ID. That’s essentially, how that line works behind the scenes. Now, EVENT_NAME can now be caught within the event checker loop like all other events:

for event in pygame.event.get();
    if event.type == EVENT_NAME:
        # Do something in response to that event here
Enter fullscreen mode

Exit fullscreen mode

Learn more about pygame events here and here



Repeating events with pygame.time.set_timer()

Earlier on, we used the pygame.time.Clock class to create an instance which used the tick() instance method to control the Refresh Rate of our game. So, now let’s take a brief look at it’s cousin: pygame.time.set_timer()

The pygame.time.set_timer() method basically takes two arguments:

  • An Event: The event we want to repeat
  • A number: The time in milliseconds over which that event is repeated

And then pygame.time.set_timer() will emit the specified event for every interval of time as specified by the second argument:


# Dispatch EVENT_NAME every second
pygame.time.set_timer(EVENT_NAME, 1000)
Enter fullscreen mode

Exit fullscreen mode

This is really convenient because it’s exactly what we need in order to make our program work as intended:

...
DINO_STANDING = pygame.transform.scale(
     pygame.image.load(os.path.join('Assets', 'sprites', 'Dino_Standing.png')),
     (DINO_WIDTH, DINO_HEIGHT)
)

DINO_LEFT = pygame.transform.scale(
    pygame.image.load(os.path.join('Assets', 'sprites', 'Dino_Left_Run.png')),
    (DINO_WIDTH, DINO_HEIGHT)
)

DINO_RIGHT = pygame.transform.scale(
     pygame.image.load(os.path.join('Assets', 'sprites', 'Dino_Right_Run.png')),
     (DINO_WIDTH, DINO_HEIGHT)
)

DINO_Y_POS = HORIZON_Y_POS - DINO_HEIGHT + DINO_HEIGHT//4


# Defining event
SWITCH_FOOT = pygame.USEREVENT + 1


# Global variables
offset_x = 0
left_foot = True   # We'll use this to switch between left and right foot

# Send SWITCH_FOOT event every 125 milliseconds
pygame.time.set_timer(SWITCH_FOOT, 125)

def draw_window():
    global offset_x
    global left_foot

    horizon_tiles = 2
    horizon_width = HORIZON.get_width()

    WINDOW.fill(WHITE)  # White

    for i in range(horizon_tiles):
        WINDOW.blit(HORIZON, (horizon_width * i + offset_x, HORIZON_Y_POS))

    # WINDOW.blit(DINO_STANDING, (30, DINO_Y_POS))

    if left_foot:
        WINDOW.blit(DINO_LEFT, (30, DINO_Y_POS))
    else:
        WINDOW.blit(DINO_RIGHT, (30, DINO_Y_POS))

    ...


def main():
    """main code for the game.
    """
    global left_foot

    ...

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

            if event.type == SWITCH_FOOT:
                left_foot = not left_foot

        ...

    pygame.quit()
...
Enter fullscreen mode

Exit fullscreen mode

A few things to take note of:

  • We use the left_foot variable to determine when to switch between foots. So whenever the SWITCH_FOOT event is detected, the line left_foot = not left_foot simply inverts the initial value of left_foot i.e if it was False, then it becomes True and vice versa.
  • We commented out the line WINDOW.blit(DINO_STANDING, (30, DINO_Y_POS)). This is only a temporary measure. We’ll fix it soon

Alright, run your code ! your Dino should now be running for dear life:

Dino running across the scrolling horizon




Controlling game play

But, we don’t want the game to start playing by itself whenever we start it. Actually, we want it to start only when we hit the SPACE key on the keyboard. So, I re-engineered the code to make this possible:

"""
main.py: Gino game.
"""
...

def draw_window(play):
    ...

    if play:
        if left_foot:
            WINDOW.blit(DINO_LEFT, (30, DINO_Y_POS))
        else:
            WINDOW.blit(DINO_RIGHT, (30, DINO_Y_POS))

        offset_x -= HORIZON_VEL
        if abs(offset_x) > SCREEN_WIDTH + 100:
            offset_x = 0
    else:
        WINDOW.blit(DINO_STANDING, (30, DINO_Y_POS))

    pygame.display.update()


def main():
    """main code for the game.
    """
    ...
    play = False

    while game_running:

        # Poll for events
        for event in pygame.event.get():
            ...

            # Start playing game when SPACE pressed
            if event.type == pygame.KEYDOWN and event.key == pygame.K_SPACE:
                play = True

        while play:
            clock.tick(FPS)
            for event in pygame.event.get():
                if event.type == pygame.QUIT:
                    game_running, play = False, False

                if event.type == SWITCH_FOOT:
                    left_foot = not left_foot

                # FIXME: experimental feature: testing pause functionality
                if event.type == pygame.KEYDOWN and event.key == pygame.K_RETURN:
                    play = False

            draw_window(play)

        draw_window(play)

    pygame.quit()


if __name__ == '__main__':
    main()

Enter fullscreen mode

Exit fullscreen mode

Essentially, I nested a new loop inside the game loop. This new loop will control when the game starts playing: So now, when we start the game, everything stays static until the SPACE key is pressed, at which point, the game play resumes as usual. A few things to note:

  • We introduce a new play variable to control when the game is in play mode and when it’s not. This variable is then passed into the draw_window() function and used to determine when to display a moving or standing dinosaur and also when the horizon should scroll leftward and when it should not.

  • Also, we used two pygame.KEYDOWN events at the two modes of the game so we can know when the user hits the SPACE or ENTER keys to start or stop game play respectively.

Now with these changes, we’re done. Run the program and see your Dino obeying all your commands:

Dino moving and stopping in response to key events

Congrats !!! you just successfully tamed a Dinosaur.

View the full code for this stage of the project on Github


So in this post, we learned a little bit more about events in pygame, how to create a custom event, how to make an event repeat after a specified time period, and we achieved our goal of displaying the Dino and making it move on the canvas.

In the next post, we’ll explore how to make the Dino jump as it tries to avoid Cactus on the road. That would be a great idea, I think. But for now:

Thanks for reading.



Source link

Leave a Reply

Your email address will not be published. Required fields are marked *