In the last post, we learned a little about pygame events: What they are, how to create them and how to use them to achieve the effect of moving the Dino's legs as it runs. In this post, we'll be concluding our work on the Dino by giving it the ability to jump whenever it wants to avoid bumping into a deadly Cactus on it's path.
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 create your own custom pygame event without much fuss. If you can't, then quickly checkout the previous post before proceeding with this one.
Alright, let's dive in!
Quick Reality Check
Our game is beginning to shape up:
The game starts up when we hit the SPACE key, causing our Dino to move it's legs as it runs across the unfolding path. Let's take a quick look at the final game:
We see that eventually, we're going to need Cactus to show up at random points on the unfolding path as deadly obstacles to the Dino. Life is no fun without obstacles right ?
With that in mind, we're going to need the Dino to be able to jump over those obstacles as they appear in order to stay alive. That's our focus in this post.
Our codebase right now (Yours should be an exact match):
import pygame
import os
pygame.init()
# Constants
FPS = 60
HORIZON_VEL = 0.8
SCREEN_WIDTH, SCREEN_HEIGHT = 720, 400
DINO_WIDTH, DINO_HEIGHT = 80, 80
WHITE = (255, 255, 255)
WINDOW = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT))
clock = pygame.time.Clock()
pygame.display.set_caption("Gino")
# Fetch the images
HORIZON = pygame.image.load(os.path.join('Assets', 'sprites', 'Horizon.png'))
HORIZON_Y_POS = SCREEN_HEIGHT//2 + SCREEN_HEIGHT//6
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
# User Events
SWITCH_FOOT = pygame.USEREVENT + 1
# Global variables
offset_x = 0
left_foot = True
# Timer to switch foot
pygame.time.set_timer(SWITCH_FOOT, 125)
def draw_window(play):
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))
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.
"""
global left_foot
game_running = True
play = False
while game_running:
# Poll for events
for event in pygame.event.get():
if event.type == pygame.QUIT:
game_running = False
# 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()
Jump Dino, Jump !
So, how to do we go about making the Dino jump ?
Looking back to when we talked about Coordinates, it's clear that in order to create the effect of the Dino jumping whenever we hit the SPACE key, we're going to need to find a way to change the Dino's coordinate value along the y axis.
But first things first, how do we enable our game to know when the SPACE key is being pressed ? And most importantly, long presses ?
If you've been paying attention in previous posts, I'm sure the first thing that comes to your mind right now would be to use the event checking loop from earlier posts. something like this:
...
for event in pygame.event.get():
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_SPACE:
# Cause Dino to jump at this point.
...
If these were your thoughts, then you're not wrong. But for this particular case, this approach may not work very well. Because, we want this key press event to be handled in a way that does not stop the rest of the game loop while handling our long key press. i.e We want the key press events to be handled Asynchronously so that even if we hold down on the SPACE for hours, all resulting events will be handled while the rest of the program runs as expected.
To achieve this, we'll be making use of the in-built pygame.key.get_pressed()
method which returns a dictionary from which we can fetch the key we pressed by name. So, our code for listening to key press events for the SPACE key will be something like this:
...
def main():
"""main code for the game.
"""
...
while game_running:
...
while play:
...
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]: # Listen for SPACE key pressed
# Make Dino jump
draw_window(play)
draw_window(play)
pygame.quit()
...
Alright, that's all well and good. But, we're still back where we started in terms of making the Dino actually jump ?
An idea we could explore would be adjusting the Dino's y coordinate value each time. But this would create no noticeable change. The reason for this can be found in our discussion on the Refresh rate of the game and how to control it. i.e The Surface of the game is being replaced about 60 times every second. This makes it that even if we change the y value directly, that change is never shown because the Surface where that change happened is very quickly replaced before we can see it. Yeah, it really happens that fast.
So, we're going to need to tackle this using two approaches in conjunction:
-
We need to establish a bounding box using the in-built
pygame.Rect
class. This bounding box will make it possible to control the positioning of the Dino on the screen dynamically:
...
def draw_window(play, dino):
...
if play:
if left_foot:
WINDOW.blit(DINO_LEFT, (dino.x, dino.y))
else:
WINDOW.blit(DINO_RIGHT, (dino.x, dino.y))
...
else:
WINDOW.blit(DINO_STANDING, (dino.x, dino.y))
pygame.display.update()
...
def main():
"""main code for the game.
"""
...
# Help us manage Dino's position dynamically
dino = pygame.Rect(DINO_X_POS, DINO_Y_POS, DINO_WIDTH, DINO_HEIGHT)
while game_running:
...
while play:
...
draw_window(play, dino=dino)
draw_window(play, dino=dino)
pygame.quit()
...
- Then, we need to have a vertical offset value which we can increase continuously even as the Surfaces are replaced to give the illusion of continuous upward motion. We'll make this a global variable for now so it's value is easily accessible:
...
offset_y = 0 # Global vertical offset
...
def draw_window(play, dino):
global offset_y
...
if play:
if left_foot:
WINDOW.blit(DINO_LEFT, (dino.x, dino.y - offset_y))
else:
WINDOW.blit(DINO_RIGHT, (dino.x, dino.y - offset_y))
...
else:
WINDOW.blit(DINO_STANDING, (dino.x, dino.y - offset_y))
pygame.display.update()
...
def main():
"""main code for the game.
"""
global offset_y
...
# Help us manage Dino's position dynamically
dino = pygame.Rect(DINO_X_POS, DINO_Y_POS, DINO_WIDTH, DINO_HEIGHT)
while game_running:
...
while play:
...
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
# Make Dino jump
offset_y += 25
draw_window(play, dino=dino)
draw_window(play, dino=dino)
pygame.quit()
...
Now, when you run the program and press SPACE while the Dino is running along the plain, you see something like this:
But now we have a new problem:
Our Dino goes up but never comes back down — And I'm pretty sure the law of gravity isn't to happy about being ignored.
So to fix this, we want our program to be able to take note of when the Dino is not on the ground and then gradually lower it to the ground. Sounds easy enough, I think the following code should do the trick:
...
def main():
"""main code for the game.
"""
...
while game_running:
...
while play:
...
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE]:
# Make Dino jump
offset_y += 25
# If Dino is not yet on the ground, lower it to the ground gradually
if (dino.y - offset_y) < DINO_Y_POS:
offset_y -= 5
draw_window(play, dino=dino)
draw_window(play, dino=dino)
pygame.quit()
...
Now run your program and as your Dino runs on the plain, press SPACE. You should see something like this:
Alright it goes up rapidly and then comes down gradually, gravity would be pleased. But we still have a problem.
From the original game:
We see that the Dino can't jump past a certain point on the screen and when it approaches that point, it waits a tiny bit and then begins to fall back to the ground regardless of whether you're holding down the SPACE key or not. So, how do we accomplish this ?
After some thinking, I came up with the idea of using a flag to indicate when the Dino is falling to the ground. So, whenever the Dino achieves maximum height, the flag is activated and all subsequent SPACE key presses are totally ignored until the Dino is completely on the ground.
Here's my implementation:
...
dino_falling = False # Flag to indicate that Dino is falling.
...
def main():
"""main code for the game.
"""
global dino_falling
...
while game_running:
...
while play:
...
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE] and not dino_falling:
# If we're not yet at max height, keep going up
if (dino.y - offset_y) > 20:
offset_y += 25
else:
pygame.time.wait(20) # Wait 20 milliseconds
dino_falling = True # Then, start falling
# If Dino is not yet on the ground, lower it to the ground gradually
if (dino.y - offset_y) < DINO_Y_POS:
offset_y -= 5
# If we're on the ground, stop falling
if (dino.y - offset_y) >= DINO_Y_POS:
dino_falling = False
draw_window(play, dino=dino)
draw_window(play, dino=dino)
pygame.quit()
...
Go ahead and run the program now — It should look something like this:
Although, we've achieved the core jumping functionality, there's one more minor bug in how our Dino jumps which defies human and animal anatomy.
Take a look at the final game once again:
Notice that whenever the Dino jumps up, it's always doing so in the standing posture. i.e It's legs are not moving
So, how do we get our program to work the same way ?
At this point, I decided to take the simpler approach and just use another global flag. The aim of this flag will be to indicate when the Dino jumping. I should mention that this is a bit of a quick-and-dirty technique. In future posts, we’ll refine this idea and explore cleaner approaches. In fact, I encourage you to look for better alternatives if you're up for it. However, let's proceed:
...
dino_falling = False # Flag to indicate that Dino is falling.
dino_jumping = False # Flag to indicate that Dino is jumping.
...
def draw_window(play, dino):
global dino_jumping
...
if play:
if dino_jumping:
# Don't move legs
WINDOW.blit(DINO_STANDING, (dino.x, dino.y - offset_y))
else:
# Move legs
if left_foot:
WINDOW.blit(DINO_LEFT, (dino.x, dino.y - offset_y))
else:
WINDOW.blit(DINO_RIGHT, (dino.x, dino.y - offset_y))
...
else:
WINDOW.blit(DINO_STANDING, (dino.x, dino.y - offset_y))
pygame.display.update()
def main():
"""main code for the game.
"""
global dino_falling, dino_jumping
...
while game_running:
...
while play:
...
keys = pygame.key.get_pressed()
if keys[pygame.K_SPACE] and not dino_falling:
dino_jumping = True
...
# If Dino is not yet on the ground, lower it to the ground gradually
if (dino.y - offset_y) < DINO_Y_POS:
offset_y -= 5
# If we're on the ground, stop falling
if (dino.y - offset_y) >= DINO_Y_POS:
dino_falling = False
dino_jumping = False
draw_window(play, dino=dino)
draw_window(play, dino=dino)
pygame.quit()
...
Now run the program one more time:
Alright, you've made real progress.
Congrats on making it this far, all that's left right now is the aesthetic polish of the game and adding those deadly obstacles we've been dying to add.
I encourage you to check out the full code on Github as I made a few small changes that weren’t covered in this post.
In this post, we tried to make our Dino jump on command and in the process of doing that, we learned a little bit about listening to key presses asynchronously using the in-built pygame.key.get_pressed()
method and using the in-built pygame.Rect
class to generate a bounding box that we can used to control game element's positioning on the screen more dynamically.
In the next post, we'll start adding those life threatening cacti I talked about earlier. But for now:
Thanks for reading.
Top comments (0)