Introduction: Coin Collector (Pygame Zero Beginner Tutorial)

About: Hello, my name is Michael. I am an electrical and computer engineer turned educator. I have a lot of experience in embedded software and hardware design. I am taking my technical experiences and leveraging the…

Follow along with this Pygame Zero tutorial to implement the game: "Coin Collector". In this game, coins fall down with increasing frequency and speed that you must collect. If you miss more than 5 coins, you lose.

I use this tutorial in my high school Game Design course at NCSSM. After learning the basics of Pygame Zero, we begin our unit on 2D games. This tutorial is the first in a series they complete as they are learning the fundamentals of developing 2D games. At the end of the unit, they create a 2D game of their own. Below are links to all the tutorials:

Note: This tutorial gives an exhaustive overview of how to implement this game, with complete explanations for each step. Feel free to skip over certain parts you feel more comfortable with. I wrote a thorough explanation of each concept, so that when you encountered something you weren't as familiar with, you could read in more detail.

Supplies

All you need is a Windows/Mac/Linux machine with Python installed. If you are on a Chromebook or can't install Python on your machine, you can use the Pygame template on replit for free. To set up your local environment, follow the steps below.


Install Python

https://www.python.org/downloads/

Go to the page listed above. Download the latest version available for your operating system. Run the installer and follow the instructions. Make sure you install pip and add the Python executable to the path (should be check boxes for each).

Install VSCode

There are many, many different options for a text editor for developing code. Far be it from me to mandate a specific code editor only for you to blame me later on when you find an editor more suitable for your purposes. Each editor has its pros and cons; it is really based on personal preference and how you will use it. Feel free to search around and find one that works for you. I recommend using VScode.

VScode - https://code.visualstudio.com/ - A powerful code editor that is quickly becoming the standard for many forms of software development. Includes a lot of features and has many useful packages/extensions that can be installed. 

Install VSCode Extensions

Below is a list of helpful extensions. Open the extensions tab, search for the extension, and install it.

Install Pygame Zero

Instructions: https://pygame-zero.readthedocs.io/en/stable/installation.html

Once you have Python installed, we will need to install Pygame Zero. To do this, you will need to "Command Prompt" on Windows or "Terminal" on Mac. Once you have done this, enter the following command:

pip install pgzero

If pip is not installed, try running the following command:

python -m ensurepip --upgrade

You may experience some weirdness on Macs. Try running the following commands.

python3 -m ensurepip --upgrade
pip3 install pgzero

Setup VSCode Environment

Create a folder where you want to save your project. Make sure it is an easily accessible location on your computer. In VScode, click the "File" menu and select "Open Folder". Select the folder containing the examples and assets.

Running Your Games

Inside VScode, open one of the examples or create a new file and copy your code from repl.it. Once the file is open, click the "play" button in the top right-hand corner to run the code.

Creating New Files

On the left side of VScode is the file navigation pane. You can create a new file by clicking on the icon. All files should end with .py, indicating that they are a Python source file.

Step 1: Assets

Before you start developing your game, you will need to download all the assets. Assets are the general term we use to describe any sort of image or sound used in the game. Instructables will not let you upload a zip file, so you will need to download each image above individually and each sound file below individually. Assets for your game must be stored in their corresponding directory:

  • images - Directory for storing images. Must be .png files.
  • sounds - Directory for storing sound effects. Must be .wav or .ogg files.
  • music - Directory for storing backing music tracks. Must be .mp3 files.

Asset names follow the same rules as Python variables. Must be lowercase, must not contain spaces, can only contain letters, numbers, or underscores, can only begin with a letter.

Step 2: Pygame Zero Set Up

In order to be able to utilize the Pygame Zero library, we need to import the library and run the game loop. The following code should be in all of your games:

import pgzrun # first line - imports the pygame zero module


pgzrun.go() # last line - executes the game loop

The game loop consists of endless calls to your draw() and update() functions that we will implement later. It also has some other features like event listening to check for input from the keyboard and mouse or check to see if clock events have occurred.


Step 3: Pygame Constant Set Up

Pygame Zero uses three special constants to set up the game. They look like variables, but they are all uppercase to indicate that they are constant values and cannot be changed after they are set the first time.

The first two constants are WIDTH and HEIGHT. These constants set the width and height of the game window. You should set these constants at the top of your program just after the pgzrun import statement.

For this game, we will set the width to 480 and the height to 720.

WIDTH = 480
HEIGHT = 720

The other Pygame constant is TITLE, which sets the name of Pygame window.

TITLE = "Coin Collector"

Step 4: Actor/Scene Setup

In this game, there are three key Actors: the background scene, the robot player, and the coin. An Actor is just a representation of some visual object within our game. It can be a player, enemy, background, obstacle, etc. It is a way of showing and controlling an image.

Actor reference: https://pygame-zero.readthedocs.io/en/stable/builtins.html#actors

Start by making the background scene Actor and setting it to the "grass" image.

bg = Actor("grass")

We need to set the initial position of the background. We will set it to the center. We can combine this with the step above or individually set the pos. pos represents the position of center of the Actor. To get the exact center of the screen, divide the WIDTHand HEIGHT by two.

bg = Actor("grass", (WIDTH // 2, HEIGHT // 2))
# OR
bg = Actor("grass")
bg.pos = (WIDTH // 2, HEIGHT // 2)

Positions are provided and set as tuples. Tuples are a list-like data type that stores a collection of values; however, it cannot be changed. Tuples are written with parentheses as opposed to square brackets. However, we can retrieve their values in the same way using indexing with square brackets. See the example below.

new_pos = (300, 400)
new_pos[0] # gives us the x coordinate
new_pos[1] # gives us the y coordinate
new_pos[0] = 100 # cannot be done! Can only assign new tuple
new_pos = (100, 400) # correctly makes new assignment

Next, create the robot Actor and set it to the "robot_idle" image. In a future step, we will get it to show the left and right versions of the image. Set the actor's position to be centered, but standing at the very bottom of the screen. The graphic above shows all the ways we can get or set the position of an Actor using tuples.

robot = Actor("robot_idle")
robot.midbottom = (WIDTH // 2, HEIGHT)

We can also get or set the Actor's position by just changing a single x or y coordinate. We can do this using the anchors shown above. y, top, and bottom would give the y-coordinate of that location of the actor. x, right, left would give the x-coordinate of that location of the actor.

Finally, create a coin Actor and set it to the "coin_gold" image. There will be many instances of this coin, but for now we will start with one.

coin = Actor("coin_gold")

We want the coin to appear at a random position. To do this, we will need to import the randint() function by adding from random import randint to the top of our program. Then we can use randint(a, b) by supplying two values identifying the start and end of the range of values from which we want to choose a number randomly. See the example below that choose a random x-coordinate between 0 and WIDTH and a y-coordinate between 0 and HEIGHT.

from random import randint

...

coin.x = randint(0, WIDTH)
coin.y = randint(0, HEIGHT)

Remember, pos and x and y represent the center of the actor. With the above code, we could randomly generate the coordinate (0, 0), which would center the coin at this location placing it half off the screen. We can adjust for this by adding a buffer of 20 pixels like shown below.

coin.x = randint(20, WIDTH - 20)
coin.y = randint(20, HEIGHT - 20)

Now it is time to draw these Actors into the scene/game. We will do this using the variable representing each Actor with the .draw() method. This code will go inside the draw() function because it is displaying the new frame. Remember to always start your draw() functions with screen.clear() or it will display artifacts from the previous time something was drawn to the screen.

def draw():
screen.clear()
bg.draw()
robot.draw()
coin.draw()

Another thing to keep in mind is that the order in which you draw things matters. If you draw the background after the robot and the coin, the background will be placed over top of the other Actors, hiding them from view.

Step 5: Basic Movement

We want to be able to move our robot character with the keyboard. There are many different ways to go about it based on the behavior you want. For this game, we want the player to move while the button is pressed.


To make this happen, we will add code to our update() function since it is called consistently. The on_key_down() and on_key_up() event listeners are only triggered once when a button is pressed and released. By adding a conditional statement to update() to check if the button is pressed, it will have the effect of continually moving while the button is pressed. Even though we don't have a loop in our program, update() behaves somewhat like a loop because it is called endlessly.


To check if a key on the keyboard is pressed in update(), we use the keyboard.KEY_NAME syntax, which is a boolean expression that tells us whether a button is pressed. The various keyboard KEY_NAME constants are described here.


def update():
if keyboard.LEFT:
# do something
elif keyboard.RIGHT:
# do something
if keyboard.UP:
# do something
elif keyboard.DOWN
# do something

To move the robot, we need to modify its position. The best way to do that is to add or remove by the same consistent value. We will store this value in a global variable called velocity.


By storing the value in a global variable, we only need to change the value in the variable to increase the robot's velocity, as opposed to multiple places in the code. It also allows us to modify velocity from anywhere in the code if we want the player to speed up due to a power-up or some other effect. Below show's an example of how to define and use the variable velocity.


# define global variables towards the top of your program
velocity = 5

...

# modify the player's position within update()
# add code below to corresponding keyboard conditionals
robot.x -= velocity # moves Actor left
robot.y += velocity # moves Actor down

We want our actor to move up, down, left, and right when the arrow keys are pressed by the value set in velocity.



Step 6: Bounding Player Movement

At this point, your player robot should be moving up, down, left, and right while the arrow key buttons are pressed. But you've probably noticed that the robot can move beyond the extents of the screen and never stops. That is because the actor's x and y coordinates are unaware of the screen size! We will have to implement this logic ourselves.

To do this, we will only allow the robot to move in update() if the key is pressed AND it is not at the edge of the screen. We will need to modify our movement conditionals within update().

We can do this by checking if the Actor's position is beyond the extents of the screen. We want to make sure that when moving horizontally, it does not go beyond the left edge of our screen (x must be greater than 0) or the right edge of our screen (x must be less than WIDTH). When moving vertically, the Actor cannot go beyond the top edge of the screen (y must be greater than 0) or the bottom edge of our screen (y must be less than HEIGHT). We can modify our conditional statements to look like this.

def update():
if keyboard.LEFT and robot.x > 0:
robot.x -= velocity
elif keyboard.RIGHT and robot.x < WIDTH:
robot.x += velocity
elif keyboard.UP and robot.y > 0:
robot.y -= velocity
elif keyboard.DOWN and robot.y < HEIGHT:
robot.y += velocity

However, pos doesn't tell us if the edge of the actor is beyond the extents of the screen. It only tells us if the center is off the screen. To check the edges, we need to use one of the other anchor points for an Actor listed below. There are a few ways to go about this, but the following way makes the most sense to me.

robot.left    # gets Actor's leftmost x position
robot.right # gets Actor's rightmost x position
robot.top # gets Actor's topmost y position
robot.bottom # gets Actor's bottommost y position

Substitute these values into the conditional statements shown above to prevent the robot from moving off the screen. No part of the image should move beyond the extent of the screen.

Step 7: Basic Animation Movement

Now, we want to investigate how to modify which image is being shown for a specific Actor. We will animate the player or create a sprite as it is known in video game design.


To change the image we can use the following code, where image is the parameter we want to change and the value is a String value representing the file name of the image from the images directory that we want to change it to:


robot.image = "robot_left"

To check which image is currently being shown, we can retrieve the value using robot.image and compare its value within the condition of an if statement. See the example below:


if player.image == "p1_stand":
player.image = "p1_walk"
elif player.image == "p1_walk":
player.image = "p1_stand"

In this case, we have three robot images: robot_idle, robot_left, robot_right. We want to modify which way the robot is facing based on which direction the robot is moving. If it is moving left, show the left facing image. If it is moving right, show the right facing image. If it is moving up or down or standing still, show the front facing image.


There are a number of ways to do this. We have our on_key_down() and on_key_up() event listeners, which are triggered when a keyboard button is pressed and released, respectively. The way I would approach this is to add the following code to the corresponding conditional statements in update() to update the image based on which keyboard key is pressed.


robot.image = "robot_left"
robot.image = "robot_right"

By adding that code to your update(), it should show the image moving left and right when you press the corresponding button. However, the Actor would never return to the front facing image. To handle this case, I would use the on_key_up() event listener to return the Actor to its front facing image when the left or right buttons are released.


# called when a keyboard button is released
def on_key_up(key):
# change to forward facing image when left/right keys released
if key == keys.LEFT or key == keys.RIGHT:
robot.image = "robot_idle"


Step 8: Coin Spawning

In this game, we will want to spawn many coins. To be able to use the same section of code repeatedly, it is best to place this code inside a function, so we can call it repeatedly. Move your code that creates a coin actor inside a function called spawn_coin().


def spawn_coin():
coin = Actor("coin_gold")
coin.x = random.randint(20, WIDTH - 20)
coin.y = random.randint(20, HEIGHT - 20)
coins.append(coin)

Notice the last line in the function. It appends the new coin Actor to a list of Actors called coins. Remember, a list is a collection of items that we want to group together, so we don't have to keep track of individual variables for every item. It will be helpful to keep a list of all the coin Actors, so we can draw each of them on the screen and detect if the player has collected any of them. To do this, we will need to define a global list variable called coins. This will need to go towards the top of your program along with the definition of velocity.


# global variables
velocity = 5
coins = []

Now that we have created a function to create a new coin Actor and add it to the list coins, we can schedule that function to be called at a regular interval using Clock. The following line of code shows how to schedule a callback to spawn_coin to occur every 0.5 seconds.


Clock reference: https://pygame-zero.readthedocs.io/en/stable/builtins.html#clock


# clock.schedule_interval(callback_function, interval)
clock.schedule_interval(spawn_coin, 0.5)

Place this line of code just before pgzrun.go() outside all functions. That ensures that Pygame will schedule the callback to occur just before repeatedly calling update() and draw().


If you run the program at this point, you still won't see any coins on the screen. That's because we haven't drawn them yet in the draw() function. There are a few more steps involved here since we are drawing an entire list of Actors and not just a single actor. You will need to create a for loop to get each individual Actor, then you can draw them one at a time. It should look something like this.


for coin in coins:
coin.draw()

Now, you should have coins showing up at random locations on the screen every half a second!



Step 9: Coin Movement

Unlike the last game, we want the coins to move. We will need to adjust their position within update() to move them down the screen each frame.


First, let's start by making them spawn just above the top of the screen. To do this, we will need to adjust the y-position we set for each new coin. Instead of making it random, let's start at a fixed y-coordinate of -20. We still want the x-coordinate to be random so that it adjusts where they fall from.


coin.x = random.randint(20, WIDTH - 20)
coin.y = -20

Now, let's make the coins fall. To do this, we will need to set another global variable called coin_velocity. This will represent how fast our coins will fall and allow us to adjust the speed throughout the game to make it harder. Inside update(), we need to increment each coin's y-position by coin_velocity. We will need to use our for loop again to visit each coin in the list of coins.


# for each coin
for coin in coins:
# advance coin down the screen
coin.y += coin_velocity

Run the game and you should start to see the coins fall down the screen. But what happens when the coins fall below the screen? Based on our current code, we are still advancing its y-position and drawing it. Our program is still keeping track of every coin, despite the fact that it is not visible on the screen. This is taking up a lot of computer's memory and could eventually crash the program, so we should help it out!


We want to add code to remove each coin once it has gone below the screen. To do this, we can add some additional logic to our for loop above to remove the coin. We will add a conditional to check if the topmost point of our coin (coin.top) is beyond the height of the screen.


# for each coin
for coin in coins:
# advance coin down the screen
coin.y += coin_velocity
# if top of coin is below the height of the screen
if coin.top > HEIGHT:
# remove it from list
coins.remove(coin)

When you run your program again, you shouldn't see a difference, although your computer will thank you! At a certain point, it would have crashed your program by generating too many coins.



Step 10: Collision

To detect collision between two Actors, we use the .colliderect() method. This returns True if there's a collision and False otherwise. It used as shown below:


unicorn = Actor("unicorn")
monster = Actor("monster")

...
def update():
if unicorn.colliderect(monster):
# game over

Oftentimes, we will have a collection of similar Actors stored in a list. It can be helpful to loop through the list and find out if there's been a collision with any item in the list. We do something like the example below:


enemies = []     		 # create list of enemies
player = Actor("p1") # create player Actor
bat = Actor("bat") # create bat enemy Actor
spider = Actor("spider") # create spider enemy Actor
enemies.append(bat) # add to list of enemies
enemies.append(spider) # add to list of enemies

...

def update():
for enemy in enemies: # loop through list of enemies
if player.colliderect(enemy): # collision with one enemy
enemies.remove(enemy) # defeated enemy, remove from list

For our game, we want to detect if the robot Actor has collided with any of the Actors in coins. If so, the robot should collect the coin by removing it from the coins list, so it is no longer drawn to the screen. To do so, we should add a for loop to update() that checks for a collision between robot and any coin in the coins list.


for coin in coins:       		  # loop through list of coins
if player.colliderect(coin): # collision with one coin
coins.remove(coin) # picked up coin, remove from list


Step 11: HUD

Most games have a HUD or heads-up display. The HUD is overlaid on top of the game and displays information to the user about the game's state. In this game, the HUD shows the time elapsed, the score, and the number of lives remaining. In order to keep track of each of these values, we will need to add global variables to represent each and initialize their values. Add these global variables towards the top of your code along with the others.


timer = 0
score = 0
lives = 5

These values will need to be displayed to the screen using the screen.draw.text() method. To use this method, we provide a string and the position. We will insert numerical values into the string using f-strings. The position can be provided using the same anchors we use for Actors. Since we are adding this information to the screen, the code should be placed in draw().


# score will appear in top left
screen.draw.text(f"Score: {score}", topleft=(10, 10))
# number of lives remaining will appear in the top center
screen.draw.text(f"Lives: {lives}", midtop=(WIDTH // 2, 10))
# timer will appear in the top right
screen.draw.text(f"Time: {timer}", topright=(WIDTH - 10, 10))

These values should now appear on the screen when the program is run, but we haven't added any logic to change the values. Let's start with the timer. To implement a timer, we should schedule another interval callback using clock, similar to what we did for spawn_coin().


Create a function called increment_timer(). Add 1 to timer every time the function is called. Since timer is a global integer variable that we are modifying, we need to use the global keyword. The function should look something like this.


def increment_timer():
global timer # mark as global
timer += 1 # increment counter

To schedule the callback to this function to occur every second and make the time tick up on the screen, we will add the following line of code just before pgzrun.go():


clock.schedule_interval(increment_timer, 1.0)

Now, it's time to increment score. We should add one to the score every time the player collects a coin. This collision logic is located in update(), so we need to start by declaring score as global at the top of update() to be able to modify its value. Inside our collision conditional statement, we add one more line to increment the score by 1.


for coin in coins:       		  # loop through list of coins
if player.colliderect(coin): # collision with one coin
coins.remove(coin) # picked up coin, remove from list
score += 1

Now, it's time to decrement the number of lives. The player should lose one life every time they miss a coin. We already implemented conditional logic to detect when a coin has dropped off the bottom of the screen so we can remove it from the coins list in update(). All we need to do is declare lives as global within update() and decrement lives within this conditional statement. To prevent the number of lives from becoming negative, add an additional if statement to only decrement while the value is greater than 0.


# for each coin
for coin in coins:
# advance coin down the screen
coin.y += coin_velocity
# if top of coin is below the height of the screen
if coin.top > HEIGHT:
# remove it from list
coins.remove(coin)
# if there are still lives remaining
if lives > 0:
# decrement lives
lives -= 1

Run the code to make sure the game functions as expected. Next, we will add logic to detect when the player has lost and allow them the opportunity to start over again.



Step 12: Game Over

To handle the lose scenario, we are going to add a game_over() function. This is not a function that is built into Pygame, but rather, a function we will call once we've determined that the player has lost. We can use it to halt the timer, stop creating new objects, display a message, etc.


Before we define the function, we will need to add another global variable called over. This will be a boolean variable that will be True when the game is over and False when the game is running. Define it at the top with the other global variables and set its initial value to False.


over = False

In this game, our game_over() function should stop the interval timers for spawn_coin and increment_timer and set over to True. Note: over should be declared as global since it is a boolean variable that we are modifying.


# called during lose condition to trigger game_over
def game_over():
global over
# set over to True
over = True
# stop interval timers
clock.unschedule(increment_timer)
clock.unschedule(spawn_coin)

Defining a function does nothing until we call it! We want to call our game_over() function when the number of lives remaining has reached 0. We should do this inside of update() after we update the value of lives. Your conditional statement should look something like this:


if lives <= 0 and not over:  # same is "if lives <= 0 and over == False"
game_over()

Why did we write and not over? and not over means that game_over will only be called when the over variable is set to False. It prevents us from calling game_over() over and over and over again once the player has lost. We only want to call game_over() once. We can check if we've called it before, because we only change over to False within game_over().


We also want to display a "Game Over!" message once the player has lost. Since we are updating the screen, we will add this code to draw(). We can check the value of the over variable in a condition. If it is True, we can write a message on the center of the screen.


if over:  # same as "if over == True"
screen.draw.text("Game Over!", center=(WIDTH // 2, HEIGHT // 2))


Step 13: Start/Restart

Now, we want to clean up the start of our program and allow the user to restart the game after they lose. We will do this by creating a new function for our selves called start(). start() is responsible for setting the initial state of the game.


In this game, our start should do the following:



  • declare timer, score, lives, velocity, coin_velocity, over as global so they can be modified
  • set the initial values of timer, score, lives, velocity, coin_velocity, over
  • clear the coins list
  • schedule the increment_timer interval
  • schedule the spawn_coin interval
  • spawn the first coin
  • set the robot's initial position

To implement the functionality above, your code should look something like this. It may seem redundant, and that's because it is! We want to reset everything back to it's initial state for the user to start the game again.


# called at start or restart of game to reinitialize
def start():
global timer, score, lives, velocity, coin_velocity, over
over = False
timer = 0
score = 0
lives = 5
coin_velocity = 10
velocity = 5
coins.clear()
robot.midbottom = (WIDTH // 2, HEIGHT)
clock.schedule_interval(increment_timer, 1.0)
clock.schedule_interval(spawn_coin, 0.5)
spawn_coin()

Now that we have moved the clock.schedule_interval() calls to start(), we can remove those lines from the end of our program.


Add a call to start() at the very end of your program just before pgzrun.go(). This will execute all the code in the function and make sure everything is ready to go before calling the Pygame main loop, which repeatedly calls update() and draw().


start()
pgzrun.go()

Run your program to make sure everything works properly. It should behave the same because we have not added the last step of the restart functionality.


In order to make the program run again, we need to schedule Pygame to call the start() method to kick back off the game after some delay. We will do this by calling the clock.schedule_unique() method and telling it to call start after 5.0 seconds.


# clock.schedule_unique(callback, delay)
clock.schedule_unique(start, 5.0)

You should now have completed game over and restart functionality!



Step 14: Sounds & Music

It is time to add the bells and whistles to our game. We will start by adding music. The music files should be stored in the music directory and must be .mp3 files. To play music, use the music.play() method. To stop the music, use the music.stop() method.


Music reference: https://pygame-zero.readthedocs.io/en/stable/builtins.html#music


For this game, play the music file called "house.mp3". Begin playing the music in the start() function. Stop playing the music in the game_over() function.


music.play("house")

music.stop()

Now, it's time to add sounds. Sounds differ from music in that they are short audio files that are handled slightly different. They should be stored in the sounds directory and must be .wav or .ogg files. To play sounds, you must use the file name in the method call. For example, if I wanted to play a sound from "thunder.wav", I would say sounds.thunder.play().


Sounds reference: https://pygame-zero.readthedocs.io/en/stable/builtins.html#sounds


We want to add two sounds to the game. We want to play "find_money.wav" whenever the robot catches a coin. We want to play "gameover.wav" whenever the player loses.


sounds.find_money.play()

sounds.gameover.play()

You should now have a completed game!



Step 15: Fine-Tuning

Even though we are done creating the underlying mechanics, there is a lot of fine-tuning that should be done to make the experience more enjoyable. Try some of these out before submitting your project.


First, try modifying the velocity of the robot to find the optimal speed for moving around that makes the game challenging.


Then, play with the parameters for the coins. You can start by increasing the coin_velocity every time the player collects a new coin to make it harder and harder.


Another thing you can tweak is the rate at which the coins spawn. To do this, I created one final global variable called coin_interval. Instead of using a fixed interval of 0.5 seconds between each coin, I used this variable that I made smaller over time to increase the frequency of the coins.


clock.schedule_interval(spawn_coin, coin_interval)

To start, I set coin_interval to 1.0 seconds. Every time I collected a coin, I would reduce the interval by 5%. I did this by modifying coin_interval, unscheduling the previous interval, and scheduling the new interval. Try this out and see how it works. Try adjusting by different percentages and see how it changes the difficulty. Keep in mind that setting an interval to 0 or a negative number will crash your game, so try to avoid this!


coin_interval -= coin_interval * 0.05  # reduce by 5%
clock.unschedule(spawn_coin) # unschedule previous interval
clock.schedule_interval(spawn_coin, coin_interval) # schedule new interval


Step 16: Optional Extensions

  • Add different items that can fall. Use the various types of coins or gems in the images directory as assets in the game. You can assign different point values to the different coins.
  • Add bad items that fall. If the player catches one of the bad items, they immediately lose. Look through the images directory to see some of the assets available to you.
  • As opposed to auto-restarting the game, add a button that only allows the user to play it again only when pressed.
  • Add other sounds and music to the game. Search on itch.io for more assets.
  • Add power ups or power downs to the game that cause the player to move faster or slower.
  • Improve the up and down animations to make it look like the player is moving by cycling through left and right animations as they move up.

Step 17: Completed Code


import pgzrun # program must always start with this
from random import randint


# set Pygame constants
WIDTH = 480
HEIGHT = 720
TITLE = "Coin Collector"

# define actors
bg = Actor("grass", (WIDTH // 2, HEIGHT // 2))
robot = Actor("robot_idle")

# declare and initialize global variables
velocity = 5
coins = []
timer = 0
score = 0
lives = 5
coin_velocity = 10
coin_interval = 1.0
over = False


# called at start or restart of game to reinitialize
def start():
global timer, score, lives, coin_velocity, velocity, coin_interval, over
# initialize game variables
robot.midbottom = (WIDTH // 2, HEIGHT)
over = False
timer = 0
score = 0
lives = 5
velocity = 5
coin_velocity = 10
coin_interval = 1.0
coins.clear()
# schedule functions to run at specific intervals
clock.schedule_interval(increment_timer, 1.0)
clock.schedule_interval(spawn_coin, coin_interval)
spawn_coin()
# start the music
music.play("house")


# called during lose condition to trigger game_over and schedule start again
def game_over():
global over
over = True # set over to true
# schedule the start function to run in 2 seconds
clock.schedule_unique(start, 2.0)
# stop decrementing counter, will be scheduled again in start
clock.unschedule(increment_timer)
clock.unschedule(spawn_coin)
coins.clear()
# stop the music
music.stop()
sounds.gameover.play()


# scheduled callback for incrementing counter every second
def increment_timer():
global timer
timer += 1


# creates a new coin actor at a random location
def spawn_coin():
coin = Actor("coin_gold")
coin.x = randint(20, WIDTH - 20)
coin.y = -20
coins.append(coin)


# displays the new frame
def draw():
screen.clear()
bg.draw()
robot.draw()
for coin in coins:
coin.draw()

# display game information: score, lives, and time
screen.draw.text(f"Score: {score}", topleft=(10, 10))
screen.draw.text(f"Lives: {lives}", midtop=(WIDTH // 2, 10))
screen.draw.text(f"Time: {timer}", topright=(WIDTH - 10, 10))
# display "Game Over" message when the game ends
if over:
screen.draw.text("Game Over", center=(WIDTH // 2, HEIGHT // 2))


# updates game state between drawing of each frame
def update():
global score, coin_velocity, lives, coin_interval
# check keyboard inputs for movement
if keyboard.LEFT and robot.left > 0:
robot.x -= velocity
robot.image = "robot_left"
elif keyboard.RIGHT and robot.right < WIDTH:
robot.x += velocity
robot.image = "robot_right"
elif keyboard.UP and robot.top > 0:
robot.y -= velocity
elif keyboard.DOWN and robot.bottom < HEIGHT:
robot.y += velocity

# move coins and check if they're off-screen
for coin in coins:
coin.y += coin_velocity
if coin.top > HEIGHT:
coins.remove(coin)
if lives > 0:
lives -= 1

# check collision between robot and coins
for coin in coins:
if robot.colliderect(coin):
coins.remove(coin)
score += 1
coin_velocity += 0.05
coin_interval -= coin_interval * 0.05
clock.unschedule(spawn_coin)
clock.schedule_interval(spawn_coin, coin_interval)
sounds.find_money.play()

# check for game over condition
if lives <= 0 and not over:
game_over()


# called when a keyboard button is released
def on_key_up(key):
# change to forward facing image when left/right keys released
if key == keys.LEFT or key == keys.RIGHT:
robot.image = "robot_idle"


start()
pgzrun.go()

Step 18: More Projects

Project-Based Learning Contest

First Prize in the
Project-Based Learning Contest