Python Arcade Create Window with Postgres

Have a Database Problem? Speak with an Expert for Free
Get Started >>

Introduction

Welcome to the beginning of a multi-part tutorial on building a top-down game. In this game, the player is represented by a penguin on skates in a Winter landscape. Walruses chase the player. The user can navigate the penguin around the screen via either the arrow keys (cursor control keys) and/or pressing “A” for left, “D” for right, “W” for up, and “S” for down. The user can press the space bar to throw a snowball. When the snowball collides with a walrus (enemy), the walrus disappears from the screen. When the player collides with an enemy, the player is “eaten”, loses a life, and respawns in the center of the screen. The user can also press the “-” key to lower game difficulty and the “=” key to increase game difficulty.

In this part we learn to use Python Arcade to create a window with Postgres. The game window will be one of the foundations of our game, where all the activity occurs, including player movement, enemy movement, bullets (“snowballs” in this game), score display, game difficulty, etc. We chose Python’s “Arcade” framework for its graphics-related functions. In this demo, we used Postgres to store game object data, in this case ice blocks.

We chose Arcade for many reasons, including its efficiency and the dedication of the author to making continual improvements. You can see how recent the latest changes have occurred by looking at their github here. Arcade is based on the older Pyglet library and shares much of its syntax.

In future parts of this multi-part tutorial, we will learn to:

  • Create Sprites and Sprite lists for storing and manipulating all the screen objects, including the player, obstacles, bullets, and enemies.
  • Read the keyboard, waiting for and using any keypresses by the user, so we can then…
  • Move the player icon around the screen.
  • Enemy movement. Here we will use the random function to first decide whether an enemy will move randomly or move with intelligence. If the “coin toss” decides to move the enemy with intelligence, we will set their movement direction to be toward the player’s icon.
  • Fire bullets. We will create routines for initializing and moving “bullets”, which in this case are snowballs, from the player.
  • Collision detection. We’ll add detecting collisions between the various objects – Sprites – and reacting to those collisions. This includes the player hitting an ice block, enemies hitting an ice block, bullets hitting an enemy, and enemies hitting the player.
  • Sound effects. In this part, we will increase the depth of immersion of our game by adding a background song as well as cues for when the player collides with an enemy or hits enemies with his bullets.
  • Display score. We’ll track score, based on difficulty level multiplied by enemies hit with bullets, and display it on the bottom of the screen.
  • Game difficulty. We’ll add buttons for the player to adjust game speed on the fly. The difficulty level will also influence points earned for hitting enemies and the intelligence of those enemies, i.e., how likely they are to move randomly vs. intelligently.

Prerequisites

First, you may want to read up on the Arcade framework because many of the game-related functions will depend on this library.

IMPORTANT: We’ve made the full source code and all resources (images and sounds) available for download here.

Next, you will need to install the following frameworks for import into your project:

1
2
3
4
5
import arcade # Game-making-oriented library for Python.
import os # For getting resource files into the game.
import pyautogui # For getting monitor resolution.
import psycopg2 # For Postgres interaction.
from datetime import datetime, timedelta # For random seed.

Create window with Python Arcade

First we’ll merely look at how to open a window using Python Arcade’s open_window function.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Set up constants
SCREEN_TITLE = "Pyngo Skater"
DIFFICULTY = 5
SCALE = 0.25
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 900
#   Get current monitor resolution
MONITOR_RES_WIDTH, MONITOR_RES_HEIGHT = pyautogui.size()
#   Make sure SCREEN_WIDTH is not bigger than monitor width.
if SCREEN_WIDTH > MONITOR_RES_WIDTH:
    SCREEN_WIDTH = MONITOR_RES_WIDTH
#   Make sure SCREEN_HEIGHT is not bigger than monitor width.
if SCREEN_HEIGHT > MONITOR_RES_HEIGHT:
    SCREEN_HEIGHT = MONITOR_RES_HEIGHT
#   Number of ice blocks based on the screen width.
BLOCKS_NUMBER = int(SCREEN_WIDTH/24)
#   Limit enemies to edges of screen.
SCREEN_FROM_DATABASE = True
ID_SCREEN = 1

Analysis of the code above:

  • Constants. Notice how the constants are all uppercase? This is the standard method of naming constants. Most, if not all, Python interpreters will be case sensitive with your variables. Using constants as you see here allows us to most efficiently – when keeping our settings in code vs. a settings file or database – keep the settings we are most likely to change all in one easy-to-find spot. Another benefit is we set up this game so that we only need to change a certain setting one time and the rest of the program will use that setting. For example, as we build our game, there will be multiple spots where we use the constant named “DIFFICULTY”. If we instead used “5” in all those spots, any time we want to change the starting difficulty, we’d need to make changes in many areas of our code instead of just the one near the top we see as “DIFFICULTY = 5”. Finally, this allows us to give descriptive names to the variables that control various game settings.

  • SCALE. This constant controls the scale of every Sprite we’ll later create in relation to the game window width and height. So here a smaller number means smaller Sprites. We created all our Sprites as PNG files with transparent backgrounds and larger than they need to be for the game screen at 1200 x 900, so as to future proof the game for larger resolutions if ever desired.

  • pyautogui.size(). This function from the Python pyautogui library allows us to find out the resolution of the current monitor being used. As you can see, it returned two values, width and height, respectively.

  • BLOCKS_NUMBER. This function gets used if we set SCREEN_FROM_DATABASE = False, so that the random ice block generator knows when to stop. Notice, in order to determine number of blocks, we are dividing screen width with 24. Decrease the 24 to get a higher density of ice blocks on the screen. You could also experiment with turning off collision detection for either the Player with ice blocks or the Enemies with ice blocks to change the flavor of the game some.

Next we’ll open up a game window, giving it the parameters it wants:

1
2
3
4
5
6
7
# Create a game window. Set window title and resolution.
arcade.open_window(SCREEN_WIDTH, SCREEN_HEIGHT, SCREEN_TITLE)

# Set the window background color
# See http://arcade.academy/arcade.color.html for a color list.
arcade.set_background_color(arcade.color.WHITE)
# The above "arcade.color.(0,0,0,0)" could be stated like "arcade.color.black"

Now let’s create a table for our screen data within Postgres, back in Python open a connection to PostgreSQL, query a table, get the position of our ice blocks from the data returned, and draw those blocks to the screen as Sprites.

We’ll begin with creating two tables for storing ice block positioning data:

Create tables for storing game screens in Postgres

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CREATE TABLE public.tbl_screens (
    id serial NOT NULL,
    id_session int4 NULL DEFAULT 0,
    id_user_created int4 NULL DEFAULT 0,
    t_screen_title VARCHAR(255) NULL,
    t_screen_msg VARCHAR(255) NULL,
    t_screen_notes VARCHAR(255) NULL,
    i_points_bonus int4 NOT NULL DEFAULT 0,
    i_screen_order int4 NULL DEFAULT 0,
    i_screen_width int4 NOT NULL DEFAULT 0,
    i_screen_height int4 NOT NULL DEFAULT 0,
    t_screen_bkg_color VARCHAR(24) NULL,
    d_screen_created DATE NULL DEFAULT now(),
    CONSTRAINT tbl_screens_pkey PRIMARY KEY (id)
);

Analysis

  • id serial: This is an auto-incrementing unique index for the table. VERY necessary for the foreign key in the next table we will build!
  • id_session: This column is for when a screen builder adds a new screen to the database and we want to most quickly and easily retrieve the new row id based on session, “id_user_created”, and “t_screen_title”.
  • id_user_created: We may want to keep track of the user who created a given screen or give them credit in some way for their creation.
  • t_screen_title: The name of the game window, which will appear in the title bar for the new window.
  • t_screen_message: A message we may choose to add to the game when the user gets to a new screen.
  • t_screen_notes: The window creator may want to place a comment here.
  • i_points_bonus: Bonus points – if any – to award the player for completion of the screen.
  • i_screen_order: In a game with more than one screen, we’ll want to track the order the screens appear to the player.
  • i_screen_width and i_screen_height: Resolution of the game window.
  • t_screen_bkg_color: Color of the game window background.
  • d_screen_created: Date a given screen was built.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
CREATE TABLE public.tbl_screens_objects (
    id serial NOT NULL,
    id_session int4 NULL DEFAULT 0,
    id_screen int4 NULL DEFAULT 0,
    t_object_name VARCHAR(128) NULL,
    t_object_type VARCHAR(128) NULL,
    t_object_image_URL VARCHAR(255) NULL,
    t_object_sound_URL VARCHAR(255) NULL,
    t_object_sound_movement_URL VARCHAR(256) NULL,
    t_object_sound_collision_URL VARCHAR(256) NULL,
    i_object_order int4 NOT NULL DEFAULT 1,
    i_object_x int4 NULL DEFAULT 0,
    i_object_y int4 NULL DEFAULT 0,
    i_object_speed int4 NULL DEFAULT 100,
    b_collision_possible bool NULL DEFAULT FALSE,
    b_collision_destroys_it bool NULL DEFAULT FALSE,
    n_collision_awards NUMERIC NOT NULL DEFAULT 0,
    n_collision_damage NUMERIC NOT NULL DEFAULT 0,
    CONSTRAINT tbl_screens_objects_pkey PRIMARY KEY (id)
);

Analysis

  • id and id_session: These two columns have same function as same columns in tbl_screens above.
  • id_screen: Via foreign keys we will tie this table to tbl_screens. This field points to the unique id field in tbl_screens. This will be a one to many relationship, where one row in tbl_screens points to a potential of many rows in this table (tbl_screens_objects).
  • t_object_name: Name – may not be used.
  • t_object_type: What kind of object? For now this means shape and we’ll use either “circle” or “rectangle”.
  • t_object_image_URL: Image for the object.
  • t_object_sound_movement_URL: Sound the object makes if/when moving. In this version of the game, the objects – ice blocks – do not move. But it doesn’t hurt to plan some for the future!
  • t_object_sound_collision_URL: Sound for player to hear when they collide with the object.
  • i_object_x and i_object_y: Placement in x/y coordinates of the object.
  • i_object_speed: How fast the object moves.
  • b_collision_possible: Boolean column we used to determine if the player can collide with the object. This is another value we will ignore because in this version of the game, all ice blocks activate collision.
  • b_collision_destroys_it: Does the object disappear if hit by player?
  • n_collision_awards: How many hit points does the player receive when they collide with this object?
  • n_collision_damage: How many hit points damage, if any, does the player receive when they collide with this object?

For the purposes of expediency, we’ll assume you added a certain amount of ice block location data to this table so we can write SQL to get data from a screen and draw that data as Sprites using Python’s Arcade functions.

Query PostgreSQL for screen data

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#   Log into the database.
t_host = "database host address"
t_port = "5432"
t_dbname = "database name"
t_user = "database user name"
t_pw = "password"
db_conn = psycopg2.connect(host=t_host, port=t_port, dbname=t_dbname, user=t_user, password=t_pw)
db_cursor = db_conn.cursor()

# First we get a screen ID and title from tbl_screens from FIRST screen
s = ""
s += "SELECT TOP 1"
s += " id"
s += ", t_screen_title"
s += ", i_screen_width"
s += ", i_screen_height"
s += ", t_screen_bkg_color"
s += " FROM tbl_screens"
s += " ORDER BY i_screen_order"
try:
    db_cursor.execute(s)
    id_screen = db_cursor.fetch("id")
    t_screen_title = db_cursor.fetch("t_screen_title")
    i_screen_width = db_cursor.fetch("i_screen_width")
    i_screen_height = db_cursor.fetch("i_screen_height")
    t_screen_bkg_color = db_cursor.fetch("t_screen_bkg_color")
except psycopg2.Error as e:
    t_message = "Database error: " + e + "/n SQL: " + s
    return render_template("error.html", t_message = t_message)
db_cursor.close

Analysis: Acquire the screen data from PostgreSQL, including screen id, window title, window dimensions, and window background color. Note that we used “SELECT TOP 1” to tell Postgres to get only the first record determined by the “ORDER BY” clause, which sorts by i_screen_order ascending.

Now that we have the basic high level data for the current screen, we query tbl_screens_objects to get all the objects associated with that specific game screen.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
s = ""
s += "SELECT"
s += " id"
s += ", t_object_name"
s += ", t_object_image_URL"
s += ", t_object_sound_URL"
s += ", t_object_sound_movement_URL"
s += ", t_object_sound_collision_URL"
s += ", i_object_x"
s += ", i_object_y"
s += " FROM tbl_screens_objects"
s += " WHERE"
s += " ("
s += " id_screen = " + id_screen
s += " AND"
s += " t_object_type = 'ice block'"
s += " )"
s += " ORDER BY i_object_order"
try:
    db_cursor.execute(s)
except psycopg2.Error as e:
    t_message = "Database error: " + e + "/n SQL: " + s
    return render_template("error.html", t_message = t_message)

Analysis: This SQL gets all object data from PostgreSQL for a specific game screen. Next we’ll loop through the returned recordset and draw Sprites to the window!

Draw Sprites from recordset

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# Initialize rendering. Necessary before we "draw".
arcade.start_render()

# Loop through each row in the recordset we created above,
#   create a Sprite for each row, and draw that Sprite to the game window.
#   Note: We are only grabbing the data we'll be using right now in this part 1.
for each db_row in db_cursor:
    id_screen = db_row[0]
    t_object_image_URL = db_row[2]
    i_object_x = db_row[6]
    i_object_y = db_row[7]

    # Create a Sprite.
    # Draw the current ice block as a sprite
    sprite_block = arcade.Sprite("./resources/images/block-ice.png", SCALE)
    sprite_block.center_x = i_x
    sprite_block.center_y = i_y
    sprite_block.draw
db_cursor.close

# Commit and show the results of the draw commands above
arcade.finish_render()

# Keep the window alive until the user clicks close
arcade.run()

NOTE: In the Python code above, we drew each block immediately to the window. Since we want these objects to be detectable for collisions, etc., in later parts of this series, we are going to add those Sprites to a Python Arcade Sprite List so we can utilize group functions like “detect any collisions between the player and ANY ice block.”

Conclusion

This was part 1 of a multi-part tutorial lesson on building a top-down 1980’s style arcade style game. In this part we learned to use Python Arcade to create a game window with Postgres. The window will be one of the primary foundations of our game, where all the play occurs, including player Sprite movement, enemy Sprite movement, bullets, score display, game difficulty, etc. We used Postgres to store screen object data.

Pilot the ObjectRocket Platform Free!

Try Fully-Managed CockroachDB, Elasticsearch, MongoDB, PostgreSQL (Beta) or Redis.

Get Started

Keep in the know!

Subscribe to our emails and we’ll let you know what’s going on at ObjectRocket. We hate spam and make it easy to unsubscribe.