Learning Python

How I built my first game in Python

Introduction

I took an intro course on how to code with Python. As a final project, I got to design and build a simple 2D game with the Pygames module. I’m going to share my process in both designing and executing the game.

Disclaimer: This was an introductory course — the focus was on understanding how Python works and different components (classes and functions), and less on code efficiency / optimization.

Scope

The scope of the project is to build an interactive game within 3 weeks. The game can be a replica of current existing games like connect-four or space invaders, or it could be up to personal interpretation. The design criteria are as follow:

  1. User input / control of the game (moving, clicking, shooting, etc)
  2. End states (victory / defeat screens)
  3. Visual interface / elements / representation of user action

With my timeframe and technical level, I decided to scope the project to a simple side-scroller game with a storyline. This allowed me to focus on designing the theme and visual aesthetics of the game (my strong suites) while fulfilling the project brief.

Concept

The game is called Office Hours. It details the life of an insurance salesman named Jerry working at a shady insurance firm in the city.

People get to move Jerry to the left and right of the screen and interact with different elements like computer terminals, paper, doors, etc. In addition, players can also explore the world that Jerry inhabits.

Players get to decide what kind of ending they wish to Jerry to have:

  1. Sad ending: Jerry goes through the same routine of answering angry customer chat on his computer, going home, waking up, and going back to work. This will continue five times before Jerry is fired from his job.
  2. Good ending: Jerry leaves his job at a whim and finds treasure clues scattered around the city. Following the clues, Jerry goes into the forest and finds a treasure.

Flow Charting

With this concept, the end states of the story were:

  1. Jerry (and the player) continuously going through the same routine of work and sleep over 5 days, ending in Jerry getting fired from his job
  2. Jerry (and the player) breaking the routine and quits his job, and follows clues to find a treasure in the forest

My flowchart for the program looked something like the following:

Flow chart for my game — Black indicate end states

It might look confusing, but the flow of the program is quite simple.

  • If players interact with items within flow 1, or sequence A, then outcome A happens
  • Vice versa, if players interact items in flow 2, or sequence B, then outcome B happens

Simply, the program is just a series of counters and checks for conditions (have players fulfilled the following criteria, if so, mark yes).

Design

Setting up MVP Test

To simulate movement, I did research by looking at online tutorials on how to use Pygame to render objects on the screen.

# Imports
import pygame
# INITIATIONS - DO NOT CHANGE
pygame.init() # initiates pygame module
pygame.font.init() # initiates pygame font
pygame.display.set_caption('Game prototype.exe') # sets the title for the window

This initializes the pygame module and also allows me to import my custom fonts for the game.

Movement

To move right, we are adding numbers to the X axis, and left we are subtracting. Since this is a side scrolling game, I did not have to think of the Y axis. To set this up, we need to create a variable that remembers where Jerry appears

# movement
dx = 0
dy = 0

Now, to provide the user interaction, I used Pygame’s built-in keypresses, which are just functions that detect whether or not a specific keyboard button is pressed. I then used the left and right arrow key to trigger movements by added or subtract 10 pixels every time players press on the arrow key.

if event.type == pygame.KEYDOWN: # detects whether there has been a key press input
  # left arrow - MOVING
  if event.key == pygame.K_LEFT: # if it is the left arrow key
    dx = -10
  # right arrow - MOVING
  elif event.key == pygame.K_RIGHT: # if it is the right arrow key
    dx = 10  # move x to the right by 10 pixels per input

Testing

The way I tested this was to print the variable after each respective keypress. So, if the program functions correctly, by pressing right arrow, dx would be 10 after one press, and 20 after another. The same would apply when pressing left arrow, but the numbers would decrease in increments of 10.

/Users/david/PycharmProjects/Test/venv/bin/python /Users/david/PycharmProjects/Test/Test1.py
pygame 1.9.4
0
10
20
30
20
10
0

Great, looks like the test was a success! Now, I need to figure out how to print a sprite (image of a character) and move it within a window. If I am able to build this, I will have created an MVP for the final project!

Test sprite and background

I jumped into photoshop to create a quick bitmap background for my game, as well as created a small bitmap character of Jerry.

Sprite for MVP
background image found on web for testing

After finishing the illustration, I needed to map them onto the screen, or “blit” the sprites. In order to do so, I have to do the following:

  1. I have to save both images in a designated folder so that the program can find the image and display them
  2. I need to layer the Jerry sprite on top of the background
  3. I need to change the coordinates of Jerry’s sprite based on the arrow key inputs

Setting up MVP test

To upload the background, I need to first define how big the game window will be. I designated it as 800 pixels by 800 pixels.

# game surface
SCREEN_SIZE = 800 # sets max screen size at 800 px
GAME_DISPLAY = pygame.display.set_mode((SCREEN_SIZE, SCREEN_SIZE)) 
# defines GAME_DISPLAY as a surface

Now, to render the background image, I used a built in pygames feature that “blits” the image at (0,0). I designated it at this coordinate because the background image is exactly 800 tall and wide, and pygame renders image starting from the upper left corner.

I created a function to perform this action so that in the future, if I decided to change backgrounds, I can reuse and manipulate the code.

def bg_render(BG_NAME):
# this function renders backgrounds based on PNG names
  png_img = pygame.image.load(BG_NAME)
  GAME_DISPLAY.blit(png_img, BG_POSITION)

Similarly, this function can be reused for rendering our character Jery on the screen as well!

def jerry_render(GAME_DISPLAY, url):
  character_png = pygame.image.load(url)
  GAME_DISPLAY.blit(character_png, (20, 570))

In order to layer them onto the screen, I needed to put them into a main loop, which is the main set of instructions that would execute the program.

while not GAME_EXIT:
# movement inputs here
  bg_render(GAME_DISPLAY, city_bg) # bottom - background
  jerry_render(GAME_DISPLAY, jerry_char) # top layer- Jerry
  pygame.display.update() # update the game

Running this loop would result in the following static image:

Static image produced by the loop

Incorporating movement

Now, to make Jerry move based on user input, I simply need to update Jerry’s sprite x coordinate every time users press left or right arrow key. To do so, I needed to incorporate the keypress functions from my first test, and update Jerry’s x coordinates.

while not GAME_EXIT:
for event in pygame.event.get():
  if event.type == pygame.KEYDOWN:
# detects whether any key is pressed
  if event.key == pygame.K_LEFT: # if left arrow key is pressed
    dx -= 10
  if event.key == pygame.K_RIGHT: # if right arrow key is pressed
    dx += 10

Now that the variable dx is being updated, I need to take that variable and update Jerry’s sprite so that it is re-rendered in the new X coordinate. To do so, I need to put dx into Jerry’s blit function

def jerry_render(GAME_DISPLAY, url, dx):
  character_png = pygame.image.load(url)
  GAME_DISPLAY.blit(character_png, (dx, 570))

The final loop looks something like this, without the global variables in the beginning. I used a for loop because the game is updating on every event, or keypress.

while not GAME_EXIT:
  for event in pygame.event.get():
    if event.type == pygame.KEYDOWN: 
# detects whether any key is pressed
      if event.key == pygame.K_LEFT: # if left arrow key is pressed
        dx -= 10
    if event.key == pygame.K_RIGHT: # if right arrow key is pressed
      dx += 10
  bg_render(GAME_DISPLAY, city_bg) # bottom - background
  jerry_render(GAME_DISPLAY, jerry_char, dx) # top layer- Jerry
  pygame.display.update() # update the game

And the actual prototype looks like this:

Moving around the sprite with left and right key

Execution

After showing the prototype and reviewing with Alex, my instructor, I then started thinking about executing the final project. While the MVP worked and provided user input, it still had unmet criteria:

  1. There’s no end state / objective
  2. There are no other interactions other than moving side to side
  3. The visual aesthetic / interest of the game still needs work
  4. Scene / background changes after players reach the left and right edge of the screen

So, with the skeleton of the game in place, I started designing the assets for the final game.

Building the world

I used Illustrator to build my characters, assets, and backgrounds. I then exported all the different assets as PNG and stored them in different folders under the master folder Assets

Below are just a few of the assets I created for the game:

One of the characters in the game
Final sprite for Jerry
one of the interactive elements

Scene changes

When players reach the left and right edge of the screen, two things will happen:

  1. The background will change
  2. Jerry will be rendered at the opposite edge of the screen

The way I approached scene changes is to detect whether the x coordinate of Jerry’s sprite has hit the right or left edge of the screen. If so, render the background to the correct image. To do so, I need a variable called SCENE that can identify the right background to render.

The function looks something like this:

def near_item(dx, object_left, object_right):
  if object_left <= dx <= object_right: 
# if player is near an object
    return True
  else:
    return False
# shortened code
SCENE = 0
if dx >= 800: # if Jerry goes to right side of the screen
  dx = 15
  SCENE += 1 # change scene background
elif dx < 0:  # if Jerry comes from the left side of the screen
  dx = 780
  SCENE -= 1 # change scene background

This loop allows me to store different backgrounds in different numbers. Upon initiation, the scene number starts at 0, so every time Jerry’s x exceeds the right side of the screen, SCENE would be increase by 1.

So, now that there can be different scenes, I created a function that blits the correct background correlating to the scene number.

def background_change():
# within the city
if SCENE == 0:
  bg_render(OFFICE_EX_BG)
elif SCENE == 1:
  bg_render(BAR_EX_BG)
elif SCENE == 2:
  bg_render(APARTMENT_EX_BG)
elif SCENE == 3:
  bg_render(CITY_END)
...

What results is the following:

Similar to changing scenes by moving left and right, I can also make Jerry enter buildings. When players are within specific positions in specific SCENE number, using the SPACE key detection:

elif event.key == pygame.K_SPACE:
  door_interaction = near_object(dx, item_left_edge, item_right_edge)
if SCENE == 0 and door_interaction == True:
  open_door = True

While this loop is testing whether the SPACE key was pressed, the next if loop is detecting which door is being interacted with. Since specific doors are located in specific SCENE numbers, this check makes sure that the right background loads in to the background_change function.

What the near_object function is doing is checking, at the moment of keypress, whether the player’s sprite is within a certain area of the screen, or virtually “near” an object.

This changes the SCENE variable. This creates the effect of Jerry entering buildings.

Interactive elements

The cool thing about using the near_object function is that it allows me to sprinkle in interactive items. When players are near a specific x coordinate and press the SPACE key, I would blit an image of the interactive item on the screen using the bg_render function.

if SCENE == 3:
if near_item(dx, item_left, item_right) == True:
bg_render(ITEM_PNG)

I would put this function under the pygame.SPACE loop so that every time players are within an item’s “range” and press the space key, the image of the item would be rendered on the screen.

User prompts

The last component I needed to add were texts that prompted user interaction. These prompts were essential because it accomplishes the following:

  1. Alerts players of critical points / items that needs to be interacted with in order to progress the story
  2. Informs players of the specific keys to trigger interaction
  3. Clues players of hidden interactions throughout the game

In pygame, fonts are treated like image in that it needs to be rendered as an image and then displayed at a (X,Y) position. Therefore, the design criteria for this function are as the following:

  1. Function checks whether sprite position is within the range
  2. While the position is within the X ranges, render a specific message at a designated position on the screen

To accomplish this, I combined the background_change and near_object functions. The first function renders an the text onto the screen, and the latter checks whether the sprite position is near an object that is interactive.

prompt_text = SUBTITLE_DISPLAY.render(‘press E to enter’, False, (WHITE)) 
# pygame font rendering ‘press E to enter’
def show_prompt(window, text): # function that renders text into window
window.blit(text)
while note GAME_EXIT:
...
if near_item(dx, item_left, item_right) == True:
show_prompt(display, prompt_text)

And all these components come together as the following:

Demo Day

Overall, the reception for the game was great! I was not only able to execute a game with different interaction types, but also create a somewhat quirky game that deviated from the examples provided. I was able to join my interest in illustrating and hacking into a single project. Furthermore, my classmates also had a lot of fun playing with the game.

Challenges and Next Steps

Because the time frame for the project was so short, I had to reuse a lot of the functions that I’ve created to save time and “cheat” the effects. This led to bugs like:

  1. Brittle code that easily breaks on edge cases like holding down two keys at once
  2. Prompts triggering at the wrong point
  3. Players walking off the map where there are no scenes left
  4. A lot of redundant code

If I had an extra week to work on the project, I would organize my code into classes so that my loops are handled more efficiently, readable, and malleable. A special thanks to Alex Seropian for all the guidance and reference material, and all the SWE’s on Youtube with robust examples!