Building Chrome Dinosaur Game in Pygame (Part 2: Infinite Horizons)


In the last post, we took a look at a basic boilerplate for pygame and then we learned how to change the background of our game’s canvas. In this post, we’re going to start coding up the elements in our game. Specifically, we’ll be working on coding up the infinite plain surface or horizon that our Dino seems to be running on:

Demo of the final game shown again for emphasis

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 change the background color of the canvas to pink if asked to do so. If You can’t, then quickly checkout the previous post before proceeding with this one.

Alright, let’s dive in!




Step 1: display the horizon

So let’s pick up where we left off. We currently have a blank white screen staring at us whenever we run our program. But, we would like to see an infinite horizon displayed on that white screen. After all, Our Dino needs to run on something. So the question is: How do we do that ?

Your mind’s likely revisiting the work we did in the last post where used a new Surface to introduce a new background color on the screen. If so, congrats ! you’re beginning to get the madness. Not yet ? don’t be too hard on yourself, it takes some time to get used to these ideas.

YES. We’ll indeed be using Surfaces. But we’re not layering a new color this time around. On second thought, where do we get an infinite horizon from ? Are we using images ?

If that’s what you’re thinking, then YES, we’ll use an image. And lucky you, the image you need is right where it needs to be: In your project directory. Specifically, it’s in the Assets/ directory which is on the same level as the main.py where we’ve been writing all the code so far. If you look in that directory, you’ll find a Horizon.png image located in Assets/sprites/ subdirectory as demonstrated below:

├── main.py
└── Assets
    ├── sprites
    │   └── Horizon.png
Enter fullscreen mode

Exit fullscreen mode

Alright, now we have our image, how do we layer that image in a new surface on the canvas ?

Well, just as the canvas WINDOW came with a fill() method for layering colors, it also comes with a blit() method for displaying images. Don’t ask me why they chose to call it that though (I got no clue).

So, from the previous post, our code should look something like this:

pygame.init()
SCREEN_WIDTH, SCREEN_HEIGHT = 720, 400
WHITE = (255, 255, 255)

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

pygame.display.set_caption("Gino")


def main():
    """main code for the game.
    """
    game_running = True

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

        WINDOW.fill(WHITE)  # White
        pygame.display.update()

    pygame.quit()


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

Exit fullscreen mode

So we’re ready to add a horizon. Right ?

Well, not so fast. unlike when we added a background color meant to cover the whole canvas, an image will need to be placed at a specific point on said canvas. To do this, we need to understand the concept of Coordinates in pygame.

In general, the Coordinate of a thing is simply a way to describe the location of that thing on a surface e.g The earth’s surface, the surface of your graph book in maths class e.t.c. For most purposes, the coordinate is usually described in terms of two directions

  • The horizontal or left-to-right direction. Known as the x axis
  • The vertical or top-to-bottom direction. Known as the y axis

This same idea is used in pygame for positioning elements on the canvas. However, there’s a small twist in the way pygame represents coordinates:

Whereas, the starting point (0, 0) of the coordinate is usually located in the center:

Showing the point (0,0) on a normal coordinate system

In pygame, that same coordinate would be located at the top-left corner:

Showing the point (0,0) in pygame's coordinate system

In essence, pygame elements are positioned relative to the left and top of the canvas respectively.


Now that we’ve covered all the theory, let’s start building. The first step is to load the image into our program:

import os
...

pygame.init()
...

# Load Horizon image
HORIZON = pygame.image.load(os.path.join('Assets', 'sprites', 'Horizon.png'))


def main():
    ...
Enter fullscreen mode

Exit fullscreen mode

The pygame.image.load() makes the image available. I make use of the os.path.join() method because I want the path Horizon.png file to be correct regardless of the Operating System being used to run the code at any point in time. However, I’ve seen some people using raw strings here. Although, I don’t advise that.

Now that we have the horizon’s image, we need to display it on screen. The WINDOW.blit() method takes the image to be displayed (HORIZON in our case) and then it’s coordinates are passed in as a tuple:

...
def main():
    """main code for the game.
    """
    game_running = True

    while game_running:
        ...

        WINDOW.fill(WHITE)  # White
        WINDOW.blit(HORIZON, (0, SCREEN_HEIGHT//2))

        pygame.display.update()

    pygame.quit()


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

Exit fullscreen mode

When you run the code, you’ll get something like this:

horizon positioned at center

From the image above, we can see that the horizon is positioned in the center of the screen or y = SCREEN_HEIGHT//2 and spans the full width of the screen from the left or x = 0. But for our game, we want the horizon closer to the bottom of the canvas. So, let’s make some changes:

...
HORIZON_Y_POS = SCREEN_HEIGHT//2 + SCREEN_HEIGHT//6

def main():
    """main code for the game.
    """
    game_running = True

    while game_running:
        ...

        WINDOW.fill(WHITE)  # White
        WINDOW.blit(HORIZON, (0, HORIZON_Y_POS))

        pygame.display.update()

    pygame.quit()


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

Exit fullscreen mode

Now when you run the code, you should see something like this:

horizon positioned closer to the bottom

So, mission accomplished ?

Almost, we still have to make the horizon scroll.

NOTE: In all the snippets above, the line WINDOW.fill(...) is always written before the line WINDOW.blit(...). Doing otherwise, would cause the white background to be displayed on top of the layer with the horizon thus erasing the horizon and displaying a white screen. i.e The order of the code matters




Step 2: Making the horizon scroll

When take a look at the final game again:

Demo of the final game shown again for emphasis

We notice that the Dino doesn’t actually move. Actually, it’s environment moves left towards it while it jumps up regularly to avoid being hit by the approaching Cactus. It is a part of the moving environment.

In order to make the horizon scroll to the left, we’ll take advantage of the fact that the game loop repeats forever and we’ll try moving the horizon image a bit to the left every time the game loop repeats. A good way I found to accomplish that was to introduce an offset along the x axis which would increase with each rep:

...

# Global variables
offset_x = 0

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

    game_running = True

    while game_running:
        ...

        WINDOW.fill(WHITE)  # White
        WINDOW.blit(HORIZON, (offset_x, HORIZON_Y_POS))

        offset_x -= 1

        pygame.display.update()

    pygame.quit()


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

Exit fullscreen mode

offset_x is declared as a global variable and the line global offset_x is placed at the top of the main function so we can modify this global variable within it instead of creating a local variable with the same name. Using global variables is never a good idea but we’ll address that concern in a future post. Right now, let’s just keep trying to hack together a solution that works.

Essentially, the line offset_x -= 1 causes the x axis of the horizon image to shift ever so slightly to the left with each game loop iteration. When we run the program now, we should see something like this:

Results of first attempt to make horizon scroll to the left

Wow, it scrolled !!!

But it’s going too fast. Why is it behaving like that ?

Well it turns out that the reason it moves so fast across the screen is a result of how fast the current surface of the canvas are being replaced by a newer surface. This rate of change in the surfaces is what is known as the game’s Refresh Rate and it’s a common fixture of every video game. It’s measured in terms of frames per second or fps for short. Most games run at the industry standard rate of 60 fps. So, it’s a good idea to calibrate our game to use that rate as well. But how ?

The way the pygame documentation recommends is through the use of the pygame.time.Clock class which comes with a tick() method which takes the fps value we want as an integer and slows down our game loop so that it repeats enough times to satisfy our specified refresh rate. Here’s our code with these changes included:

...
FPS = 60
clock = pygame.time.Clock()

# Global variables
offset_x = 0

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

    while game_running:
        clock.tick(FPS)
        ...

    pygame.quit()


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

Exit fullscreen mode

NOTE: the line clock.tick(...) must always be located within the body of the main game loop. Otherwise, it would have no effect on the refresh rate of the game.

Now when you run the game, you get a much slower scroll:

The horizon scrolling to the left at a very friendly pace

Congrats, the horizon is now scrolling to the left at a more approachable pace. But, we don’t want it to ever go off the screen. Instead, we want it to scroll leftward infinitely. How do we accomplish this ?

Well, I have an idea:

  • Firstly, we need to have more than one image of the horizon displayed on the screen in sequence. Considering the fact that the horizon image already spans the full width of the canvas, I decided to layer two horizon images in sequence:
...
def main():
    """main code for the game.
    """
    ...
    horizon_width = HORIZON.get_width()
    horizon_tiles = 2

    while game_running:
        ...

        WINDOW.fill(WHITE)  # White

        # Displaying the two horizon images back to back
        for i in range(horizon_tiles):
            WINDOW.blit(HORIZON, (horizon_width * i + offset_x, HORIZON_Y_POS))

        offset_x -= 1

        pygame.display.update()

    pygame.quit()


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

Exit fullscreen mode

  • Finally, we need to reset the horizon images to their original positions when the first one goes off the screen by a considerable margin. Here’s how I would achieve that:
...

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

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

        pygame.display.update()

    pygame.quit()


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

Exit fullscreen mode

Now run the program. You should have an infinite horizon on your canvas:

Horizon scrolling infinitely leftward

Congrats !!!, you just successfully added the very first game element. View the full code on Github.

But, don’t limit yourself to my code and ideas. Go ahead. Tweak the code. Break things. That’s how you learn best.


So in today’s post we learned how to:

  • Display images on the canvas.
  • Work with the coordinate system in pygame and use it to effectively position game elements on the canvas.
  • Control the game’s refresh rate using pygame’s inbuilt pygame.time.Clock class and why it’s important to do so.

And we achieved our goal of creating an infinite horizon for our Dino to run on in future posts. It will be so happy to see all the good work we’ve done.

Speaking of the Dino, in the next post we’ll focus on adding it to the canvas and making it move those adorable little legs. But for now:

Thanks for reading.



Source link

Leave a Reply

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