In the last post, we successfully implemented the Jump functionality for our Dino so it could avoid deadly obstacles on it's path and in the course of doing that, we learned a bit about creating bounding boxes and listening for key presses asynchronously. In this post, we're going to be adding those deadly obstacles the Dino is going to need to avoid.
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 create bounding boxes to aid in dynamic postioning of elements on screen. If you can't, then quickly checkout the previous post before proceeding with this one.
Alright, let's dive in!
Displaying Obstacles
We've covered much of the core functionality of our game. All that's left now is adding the Cacti that will serve as obstacles for our Dino:
Our codebase is currently too big to be displayed in it's entirety any longer. So with that in mind, I'll only be displaying the snippets most relevant to what we're currently working on. To see the full code from the last post, check it out on Github.
Now, let's talk about the obstacles in our game. A quick peek at the final game above reveals a few characteristics of the obstacles in our game:
- They all have varied heights. i.e Some are tall while others are short
- They are all of equal width.
- They randomly appear on the screen alone or in a cluster of two or three cacti.
- They always appear at an interval that ensures enough space for our Dino to jump over each set without bumping into the next.
- Once they appear, each set seems to scroll across the screen with the horizon.
Bearing these in mind, let's get busy implementing all these features into our game.
To keep things clear, we'll take it one step at a time:
Varied Height, Equal Width
To display a cactus on the screen, we need to import our Cactus image:
...
CACTUS = pygame.image.load(os.path.join(
'Assets', 'sprites', '1_Cactus.png')).convert_alpha()
...
def draw_window(play, dino):
...
...
def main():
...
...
NOTE: The convert_alpha()
is a pygame image method which converts the imported image into the image format of your game while preserving it's original per-pixel transparency values.
Notice from the main game that each set of obstacles is always displayed as a group of one to three members. This suggests that we can represent each set as a collection. i.e An array or list which houses the set of obstacles currently being displayed.
Also take note of the fact that the number of cacti in each set is always random with each set having cacti of similar height.
As a first step, let’s display our obstacles at the right edge of the screen and make sure that each time we restart the game, the number of cacti are randomized. Here's how I accomplished exactly that:
...
CACTUS = pygame.image.load(os.path.join(
'Assets', 'sprites', '1_Cactus.png')).convert_alpha()
...
CACTUS_WIDTH = 30 # Defines the uniform width of all cactus
...
def draw_window(play, dino, obstacles):
...
# Displaying the obstacles
for obstacle in obstacles:
cactus_image = pygame.transform.scale(
CACTUS, (obstacle.width, obstacle.height)).convert_alpha()
WINDOW.blit(cactus_image, (obstacle.x, obstacle.y))
...
...
def main():
...
cacti = []
num_cactus = random.randint(1, 3) # Generate either 1, 2 or 3 cactus
cactus_height = random.randint(50, 80)
for i in range(num_cactus):
# Determine both the x and y positions of each cactus
cactus_y_pos = HORIZON_Y_POS - cactus_height + cactus_height//4
cactus_x_pos = SCREEN_WIDTH - 300 + i*CACTUS_WIDTH # i*CACTUS_WIDTH ensures that each cactus is displayed standing on it's own
# Add a new cacti bounding box to the group
cacti.append(pygame.Rect(
cactus_x_pos, cactus_y_pos, CACTUS_WIDTH, cactus_height))
while game_running:
...
while play:
...
draw_window(play, dino=dino, obstacles=cacti)
draw_window(play, dino=dino, obstacles=cacti)
pygame.quit()
...
NOTE: Unlike the Dino image assets, where we called pygame.transform.scale()
as soon as we loaded them, the cactus images are scaled at the point of display. This is because the Dino has a fixed, uniform height, while the cacti vary in height. We therefore need to scale them only once the variable height values are known.
Now, run your code. You should see something like this:
Cactus scrolls across the screen
Our obstacles are now on screen. But, we need to figure out how to make them move across the screen in unison.
We’ve actually done something similar with the horizon: by using an offset_x variable that keeps increasing, we subtract it from the horizon’s x-value to continuously push it left, creating the illusion of motion..
So, we just need to do that for our obstacles, right ?
Yes, for the most part. Although it's worth taking into consideration that unlike offset_x
which had to be reset to 0
whenever it's absolute value exceeded the screen's width, our new obstacle_offset_x
will be allowed to grow indefinitely, producing continuous leftward motion without a wrapping effect.
It’s also a good idea to move the logic for creating a new set of cacti into its own function. That way, you can easily reuse it whenever the program needs to generate cacti. I'll call this new function: generate_cacti()
and it'll receive a single argument: starting_point
which will prove useful later.
Alright, let's take a look at the code with all these changes in place:
...
CACTUS = pygame.image.load(os.path.join(
'Assets', 'sprites', '1_Cactus.png')).convert_alpha()
...
CACTUS_WIDTH = 30 # Defines the uniform width of all cactus
...
obstacle_offset_x = 0 # The offset for moving obstacles along horizon
...
def draw_window(play, dino, obstacles):
...
global obstacle_offset_x
...
# Displaying the obstacles
for obstacle in obstacles:
cactus_image = pygame.transform.scale(
CACTUS, (obstacle.width, obstacle.height)).convert_alpha()
WINDOW.blit(cactus_image, (obstacle.x + obstacle_offset_x, obstacle.y))
if play:
...
offset_x -= HORIZON_VEL
obstacle_offset_x -= HORIZON_VEL
...
else:
...
...
def generate_cacti(starting_point = 0):
cacti = []
num_cactus = random.randint(1, 3) # Generate either 1, 2 or 3 cactus
cactus_height = random.randint(50, 80)
for i in range(num_cactus):
# Determine both the x and y positions of each cactus
cactus_y_pos = HORIZON_Y_POS - cactus_height + cactus_height//4
cactus_x_pos = starting_point + SCREEN_WIDTH + i*CACTUS_WIDTH # i*CACTUS_WIDTH ensures that each cactus is displayed standing on it's own
# Add a new cacti bounding box to the group
cacti.append(pygame.Rect(
cactus_x_pos, cactus_y_pos, CACTUS_WIDTH, cactus_height))
return cacti
def main():
...
cacti = generate_cacti()
while game_running:
...
while play:
...
draw_window(play, dino=dino, obstacles=cacti)
draw_window(play, dino=dino, obstacles=cacti)
pygame.quit()
...
NOTE: Both obstacle_offset_x
and offset_x
are updated by the same value, HORIZON_VEL
. This keeps the horizon and obstacles moving left at the same rate, creating the illusion that the Dino is running forward along a fixed but continuously unfolding path.
Now, run your code. You should see something like this:
New cacti are born when old ones die
Now, we need to make it possible for our game to generate a new set of obstacles as soon as the old set scrolls off the screen.
To create the feeling of new obstacles being revealed as the game unfolds, I generate new obstacles only after the current ones have moved off-screen. I do this by creating a custom user event, GENERATE_OBSTACLES
, and posting it to the event queue with pygame.event.post()
whenever the last cactus in the list goes off the screen (i.e. when obstacles[-1].x + obstacle_offset_x < 0
).
Once the GENERATE_OBSTACLES
event is added to the queue, we handle it by retrieving the x-position of the last cactus, clearing the old obstacles, and generating a new set starting from that position. This makes the new obstacles scroll in naturally from the right edge of the screen.
Alright, here's the code with these new ideas incorporated:
...
CACTUS = pygame.image.load(os.path.join(
'Assets', 'sprites', '1_Cactus.png')).convert_alpha()
...
CACTUS_WIDTH = 30 # Defines the uniform width of all cactus
...
# User Events
SWITCH_FOOT = pygame.USEREVENT + 1
GENERATE_OBSTACLES = pygame.USEREVENT + 2
...
obstacle_offset_x = 0 # The offset for moving obstacles along horizon
...
def draw_window(play, dino, obstacles):
...
global obstacle_offset_x
...
# Displaying the obstacles
for obstacle in obstacles:
cactus_image = pygame.transform.scale(
CACTUS, (obstacle.width, obstacle.height)).convert_alpha()
WINDOW.blit(cactus_image, (obstacle.x + obstacle_offset_x, obstacle.y))
# Determine: If left-most obstacle is completely off the left edge
if (obstacles[-1].x + obstacle_offset_x) < 0:
# Then, generate a new set of obstacles:
pygame.event.post(pygame.event.Event(GENERATE_OBSTACLES))
if play:
...
offset_x -= HORIZON_VEL
obstacle_offset_x -= HORIZON_VEL
...
else:
...
...
def generate_cacti(starting_point = 0):
cacti = []
num_cactus = random.randint(1, 3) # Generate either 1, 2 or 3 cactus
cactus_height = random.randint(50, 80)
for i in range(num_cactus):
# Determine both the x and y positions of each cactus
cactus_y_pos = HORIZON_Y_POS - cactus_height + cactus_height//4
cactus_x_pos = starting_point + SCREEN_WIDTH + i*CACTUS_WIDTH # i*CACTUS_WIDTH ensures that each cactus is displayed standing on it's own
# Add a new cacti bounding box to the group
cacti.append(pygame.Rect(
cactus_x_pos, cactus_y_pos, CACTUS_WIDTH, cactus_height))
return cacti
def main():
...
cacti = generate_cacti()
while game_running:
...
while play:
...
for event in pygame.event.get():
...
if event.type == GENERATE_OBSTACLES:
last_obstacle_x = cacti[-1].x
cacti.clear() # Remove all obstacles in initial set
cacti = generate_cacti(starting_point=last_obstacle_x)
...
draw_window(play, dino=dino, obstacles=cacti)
draw_window(play, dino=dino, obstacles=cacti)
pygame.quit()
...
NOTE: Our custom event, GENERATE_OBSTACLES
, is first wrapped in a pygame.event.Event()
object and then posted to the event queue with pygame.event.post()
. From there, it behaves like any other event we can read later in the game loop.
Go ahead and run your code — the game should now be more exciting and look something like this:
Check out the full code for this part of the project on Github.
Alright, we've successfully added obstacles to our game. Our game is really beginning to shape up and become playable. Feel free to share what you've built so far with your friends. I'm sure they'll be amazed at your programming brilliance.
Although, our Dino still doesn't die when it collides with the obstacles. We would like it to die when that happens.
So, in the next post, we'll be learning about collision detection and how we can implement that in our game. But for now:
Thanks for reading.
Top comments (0)