Python Arcade Game Speed and Postgres

Introduction

This is part 10 of a multi-part set of tutorials showing how to build a 2D game. In this part 10, we amp-up the fun by learning how to use Python Arcade for Game Speed and Postgres for saving users, score, and store the screen configuration. In this part, we learn how to change game difficulty. In the articles following this part, we’ll add user registration and then login. Depending on various other factors, we may then add online multiplayer capability and create a screen editor, also using PostgreSQL.

Prerequisites

  • IMPERATIVE: study part 1 of the set of articles through part 9 where we learned how to create and set up a game screen window with Postgres, how to initialize Sprites with their images and group those Sprites into Sprite Lists using the Arcade library for Python, how to handle when the user presses keys, moving the Player’s Sprite through the game, moving enemies with randomness and mimic intelligence, enabling the player to fire multiple bullets using the spacebar, moving the bullets in the background, handling collisions between Arcade Sprites and Sprite Lists, adding sound effects, and finally, keeping score and storing that score in the database.

  • IMPORTANT: All of the source code for this project, including images and sound files is available for download here.

  • In case you are interested or curious about all the features and functions part of Python Arcade, here are the docs.

Let’s dive in to this lesson on game speed and difficulty, which are tightly related. The goal is to give the user the ability to change the difficulty on the fly, which changes the speed and intelligence of the enemies, which in this game are represented by walruses trying to eat you, represented by a penguin.

We’ll begin by setting up and looking at all the relevant constants:

Initialize Python constants

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
31
32
33
34
# Set up constants
SCREEN_TITLE = "Pyngo Skater"
# --------------------------------------
# Set up difficulty constant as integer
# and start at 5.
DIFFICULTY = 5
# Notice we are basing the initial number
# of enemies on difficulty plus 2.
ENEMY_COUNT_INITIAL = DIFFICULTY + 2
# Notice we are dividing difficulty by 10
# and adding the result to 2 to give us the
# speed that enemies will move.
ENEMY_SPEED = 2 + (DIFFICULTY/10)
# --------------------------------------
SCALE = 0.25
SCREEN_WIDTH = 1200
SCREEN_HEIGHT = 900
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.
SPACE_OFFSCREEN = 1
LIMIT_LEFT = -SPACE_OFFSCREEN
LIMIT_RIGHT = SCREEN_WIDTH + SPACE_OFFSCREEN
LIMIT_BOTTOM = -SPACE_OFFSCREEN
LIMIT_TOP = SCREEN_HEIGHT + SPACE_OFFSCREEN
SCREEN_FROM_DATABASE = False
ID_SCREEN = 1

Since the above difficulty constant can not be changed during game play, we’ll add difficulty to part of the player class, using the constant to initialize it.

Add difficulty as class property

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class PlayerSprite(arcade.Sprite):
    # Set up the player.
    def __init__(self, filename, scale):
        # Call the parent Sprite constructor
        super().__init__(filename, scale)
        # Initialize player movement variables.
        # Angle comes in automatically from the parent class.
        self.thrust = 0
        self.speed = 0
        self.max_speed = 5
        self.drag = 0.045
        # Initialize respawning variable
        self.respawning = 0
        # -------------------------------
        # Initialize difficulty variable
        self.difficulty = DIFFICULTY
        # -------------------------------
        # Set the player to respawn.
        self.respawn()

Next, we added speed as a property to the Enemy Sprite class below using the difficulty constant in our calculation. Note that all enemies will have the same speed. Since each enemy will have its own speed property, it will be a relatively easy task to modify the code so that each enemy has independent speed.

Set speed based on difficulty

1
2
3
4
5
6
7
8
class EnemySprite(arcade.Sprite):
    # Initialize enemy size, movement-type timers, and speed
    def __init__(self, image_file_name, scale):
        super().__init__(image_file_name, scale=scale)
        self.size = 0
        self.timer_rand = 0
        self.timer_smart = 0
        self.speed = 2 + (DIFFICULTY/10)

More enemy initialization where difficulty is used to calculate a speed-related movement factor:

Enemy movement initialization

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
31
# Set up a list of strings to store the URLs to
# our four different colored enemies.
image_list = ("./resources/images/walrus-red.png",
                "./resources/images/walrus-blue.png",
                "./resources/images/walrus-purple.png",
                "./resources/images/walrus-green.png")
for i in range(ENEMY_COUNT_INITIAL):
    # Use the randint function to get a
    # number between (and including) 0 and 3.
    image_no = random.randint(0,3)
    # Use our random number to assign a
    # random colored image to the current
    # enemy being produced.
    enemy_sprite = EnemySprite(image_list[image_no], SCALE)
    enemy_sprite.guid = "Enemy"
    enemy_sprite.timer_rand = 0
    enemy_sprite.timer_smart = 0
    # Set the enemy Sprite's position to
    # random x and y coordinates on the screen.
    enemy_sprite.center_y = random.randint(LIMIT_BOTTOM, LIMIT_TOP+1)
    enemy_sprite.center_x = random.randint(LIMIT_LEFT, LIMIT_RIGHT+1)
    # -------------------------------------
    # Note use of difficulty in the calculations below.
    enemy_sprite.change_x = int(random.random() * 2 + (DIFFICULTY/10) - 1)
    enemy_sprite.change_y = int(random.random() * 2 + (DIFFICULTY/10) - 1)
    # -------------------------------------
    enemy_sprite.size = 4
    # Add the new sprite to our list of enemy
    # Sprites and our list of all Sprites.
    self.list_all_sprites.append(enemy_sprite)
    self.list_enemies.append(enemy_sprite)

Now to show the user the difficulty level and instructions in the upper left and bottom left corners of the game window:

Show difficulty on the screen

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
def on_draw(self):
    # This command is needed before any drawing
    arcade.start_render()
    # Draw all the sprites.
    self.list_all_sprites.draw()
    # Draw our title image in the center of the screen.
    if self.player_sprite.respawning:
        self.title.center_x = SCREEN_WIDTH/2
        self.title.center_y = SCREEN_HEIGHT/2
        self.title.alpha = 255
        self.title.draw()
    # Remove title image from screen once Player
    # has finished respawning.
    else:
        self.title.alpha = 0
        self.title.draw()

    # Instructions:
    output = f"Throw snowball: press space"
    arcade.draw_text(output, 10, SCREEN_HEIGHT-25, arcade.color.BLACK, 14)
    # -------------------------------------
    # Instructions for changes in difficulty:
    output = f"Harder: press ="
    arcade.draw_text(output, 10, SCREEN_HEIGHT-45, arcade.color.BLACK, 14)
    output = f"Easier: press -"
    arcade.draw_text(output, 10, SCREEN_HEIGHT-65, arcade.color.BLACK, 14)
    # Actual current difficulty level placed near bottom left of the screen.
    output = f"Difficulty: {self.player_sprite.difficulty}"
    arcade.draw_text(output, 10, 60, arcade.color.BLACK, 14)
    # -------------------------------------

Next, let’s look at the code for reading the keyboard and changing the difficulty level:

Key pressed for speed change

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# Increase game difficulty.
#   We chose "equals" key instead of "plus" key here
#   so Player does not have to use the SHIFT key.
elif symbol == arcade.key.EQUAL:
    self.player_sprite.difficulty += 1
    # Making sure difficulty has a ceiling of 15.
    if self.player_sprite.difficulty > 15:
        self.player_sprite.difficulty = 15
# Decrease game difficulty.
elif symbol == arcade.key.MINUS:
    self.player_sprite.difficulty -= 1
    # Making sure difficulty has a floor of 1.
    if self.player_sprite.difficulty < 1:
        self.player_sprite.difficulty = 1

The next thing to add is how difficulty affects the speed of enemy movement.

Enemy speed and difficulty

Finally, intertwined in the enemy movement code below is score calculated using difficulty when a snowball (bullet) hits an enemy.

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
# ----------------------------
# Set up a local variable to track enemy
# speed and base this on difficulty:
enemy_speedy = 2 + (self.player_sprite.difficulty/12)
# ----------------------------
if not self.game_over:
    self.list_all_sprites.update()

    # Cycle through all snowballs currently
    # on the screen.
    for snowball in self.list_snowballs:
        enemies = arcade.check_for_collision_with_list(snowball, self.list_enemies)
        # Cycle through all (if any) enemies hit by
        # current snowball.
        for enemy in enemies:
            arcade.play_sound(self.sound_powerup)
            # ----------------------------
            # Award points for hitting enemy
            # based on difficulty level.
            self.score += int(100 * self.player_sprite.difficulty)
            # ----------------------------
            enemy.remove_from_sprite_lists()
            snowball.remove_from_sprite_lists()

    # If the player is respawning
    # (not invulnerable):
    if not self.player_sprite.respawning:
        # Get player coordinates so later we can have
        # enemies can move toward the player Sprite.
        player_pos_x = self.player_sprite.center_x
        player_pos_y = self.player_sprite.center_y
        # Decide to move enemies
        # randomly or intelligently.
        enemy_number = 0
        for enemy in self.list_enemies:
            enemy_number += 1
            random.seed(datetime.now() + timedelta(0, enemy_number))
            # Decrement enemy movement
            # timers by 1 tic.
            enemy.timer_rand -= 1
            enemy.timer_smart -= 1
            # Initialize local variables for
            # enemy direction of movement.
            dir_x = 0
            dir_y = 0

            # Check to see if BOTH enemy movement
            # direction timers ran out.
            if enemy.timer_rand < 1 and enemy.timer_smart < 1:
                # ----------------------------
                # Random number based on difficulty so below
                # we can decide if the enemy will move randomly
                # or toward the Player.
                random_or_smart = random.randint(1, 20 + (self.player_sprite.difficulty * 2))
                # ----------------------------
            else:
                # Make sure no random
                # movment occurs.
                random_or_smart = 1000

Conclusion

In this part 10, we increased game depth by playing with how to use Python Arcade for game difficulty and PostgreSQL for saving users (later part), score (past part), and storing the screen configuration (part 1). In this part, we learned how to change game difficulty, display it on the screen, and have it affect enemy movement speed and intelligence, and scoring. In the articles following this part 10, we’ll add user registration and then login.

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.