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:
- User input / control of the game (moving, clicking, shooting, etc)
- End states (victory / defeat screens)
- 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:
- 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.
- 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:
- 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
- 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:
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
# movementdx = 0dy = 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.pypygame 1.9.4010203020100
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.
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:
- I have to save both images in a designated folder so that the program can find the image and display them
- I need to layer the Jerry sprite on top of the background
- 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 surfaceSCREEN_SIZE = 800 # sets max screen size at 800 pxGAME_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:
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:
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:
- There’s no end state / objective
- There are no other interactions other than moving side to side
- The visual aesthetic / interest of the game still needs work
- 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:
Scene changes
When players reach the left and right edge of the screen, two things will happen:
- The background will change
- 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 codeSCENE = 0if dx >= 800: # if Jerry goes to right side of the screen dx = 15 SCENE += 1 # change scene backgroundelif 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 cityif 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:
- Alerts players of critical points / items that needs to be interacted with in order to progress the story
- Informs players of the specific keys to trigger interaction
- 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:
- Function checks whether sprite position is within the range
- 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:
- Brittle code that easily breaks on edge cases like holding down two keys at once
- Prompts triggering at the wrong point
- Players walking off the map where there are no scenes left
- 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!