GUI Speed in Python and Cockroach

Introduction

This is part nine of a multi-article set of articles exploring how to build a 2D zombie shooter game with the Arcade library. In this part, we will learn how to change GUI Speed in Python and Cockroach is used for saving GUI configuration, score, and other settings. In this article, we will learn how to change game speed and difficulty. In the articles following this part, we’ll add mouse support, score keeping, and image swapping.

Prerequisites

  • REQUIRED: see part 1 of the set through part eight where we learned how to create and set up a GUI with the CockroachDB database; create Sprites with images, as well as group together those Sprites into Sprite Lists using the Arcade library for Python; handle when the player presses keys; moving the Player around the GUI; moving enemies with randomness and some intelligence; enabling the player to throw multiple brains (bullets) with the space bar, moving those bullets; dealing with any collisions between Sprites and other Sprites; music and sound effects; and finally, tracking the Player’s score and storing that score in the Cockroach database.

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

  • Here is the documentation for Python Arcade.

Now to begin learning how to add Speed and difficulty to our game play, which are closely related. Our primary goal is to provide the user with the ability to change the game difficulty without interrupting game play. Changing difficulty changes the intelligence and movement speed of the zombies.

In this article, we will also be adding “enemy immunity” and “frustration” which are linked and influence game play. Immunity here means immunity from the blocks. Without immunity, the zombies will bounce off the blocks and have to go around them. Frustration is a counter that initiates once a zombie has become “enraged”. The counter increments whenever the enemy bounces against a block. If they bounce too many times, i.e., their frustration counter got to a certain level, they then become immune to the blocks. I added this feature in because enemies sufficiently enraged, being huge, can too easily get stuck forever between blocks, making it too easy for the Player.

We’ll start off by initializing and examining 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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
SCREEN_TITLE = "Zombie Feeder"
# Beginning difficulty level, which can be
# changed via the "-" and "+(=)" buttons.
DIFFICULTY = 4
# How many enemies?
ENEMY_COUNT_INITIAL = 8
ENEMY_SPEED = 2 + (DIFFICULTY/12)
# Set the following to true for
# harder game play where enraged
# enemies are immune to obstacles.
ENEMY_ENRAGED_IMMUNE_TO_BLOCKS = True
# How many bounces before an
# enemy becomes immune to blocks?
ENEMY_FRUSTRATION_THRESHOLD = 160
# Allow enemies to "teleport"
# from any edge of the screen
# to opposite edge.
# Set to False for easier, where
# the enemies will "bounce" from
# the screen edge.
ENEMY_SCREEN_EDGE_TRAVERSE = True
# Beginning size of all the Sprites
# on the screen.
SCALE = 0.25
PLAYER_STARTING_LIVES = 4
PLAYER_IMMUNE_TO_BLOCKS = True
MONITOR_RES_WIDTH, MONITOR_RES_HEIGHT = pyautogui.size()
SCREEN_WIDTH = 1920
SCREEN_HEIGHT = 1080
# Make sure GUI is not bigger than monitor width.
if SCREEN_WIDTH > MONITOR_RES_WIDTH:
    SCREEN_WIDTH = MONITOR_RES_WIDTH
# Make sure GUI is not bigger than monitor width.
if SCREEN_HEIGHT > MONITOR_RES_HEIGHT:
    SCREEN_HEIGHT = MONITOR_RES_HEIGHT
# Number of obstacles are based on the screen width.
BLOCKS_NUMBER = int(SCREEN_WIDTH/20)
# Set up screen edges.
SPACE_OFFSCREEN = 1
LIMIT_LEFT = -SPACE_OFFSCREEN
LIMIT_RIGHT = SCREEN_WIDTH + SPACE_OFFSCREEN
LIMIT_BOTTOM = -SPACE_OFFSCREEN
LIMIT_TOP = SCREEN_HEIGHT + SPACE_OFFSCREEN
# Draw screens from database.
# If set to False, screens will
# be drawn using the random function
# to place obstacles.
SCREEN_FROM_DATABASE = False

Since the difficulty constant we created above can not be modified during game play, we will add difficulty as a property in the player class object, using the constant to initialize that property’s value.

Add property to class

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# Sprite that represents the player.
# derives from arcade.Sprite.
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 variables
        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 (later using the difficulty constant to calculate speed). Please notice that all enemies will have the same speed in this version of the game.

Initialize speed

1
2
3
4
5
6
7
8
9
10
# Sprite that represents an enemy.
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.timer_rand = 0
        self.timer_smart = 0
        self.speed = 0
        self.url_image = image_file_name
        self.immunity = False

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

Enemy movement initialization

Notice here below we are giving the enemy sprites a speed based on difficulty level and frustration is initialized to zero.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
for i in range(ENEMY_COUNT_INITIAL):
    image_no = i
    enemy_sprite = EnemySprite(image_list[image_no], SCALE)
    enemy_sprite.guid = name_list[image_no]
    enemy_sprite.speed = 2 + (DIFFICULTY/12)
    enemy_sprite.immunity = False
    enemy_sprite.enraged = False
    enemy_sprite.frustration = 0
    enemy_sprite.which = "self.sound_char_" + str(i).zfill(2) + "_enraged"
    enemy_sprite.scale = SCALE
    enemy_sprite.timer_rand = 0
    enemy_sprite.timer_smart = 0
    enemy_sprite.url_image = image_list[image_no]
    enemy_sprite.center_y = 800
    enemy_sprite.center_x = int((SCREEN_WIDTH/8 * (i+1))-(SCREEN_WIDTH/13.9))
    enemy_sprite.change_x = 0
    enemy_sprite.change_y = 0
    self.list_all_sprites.append(enemy_sprite)
    self.list_enemies.append(enemy_sprite)

Now we will display (and update often) the difficulty level and instructions in the upper and lower left corners of the GUI:

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
31
def on_draw(self):
    # The following command must
    # be before we begin drawing.
    arcade.start_render()
    # Draw all sprites to the screen.
    self.list_all_sprites.draw()
    # Draw the title image Sprite in
    # the middle of the GUI.
    if self.player_sprite.respawning:
        self.title.center_x = SCREEN_WIDTH/2
        self.title.center_y = SCREEN_HEIGHT/2
        # Fully visible. Below, alpha (opacity)
        # will gradually reduce to zero over time.
        self.title.alpha = 255
        self.title.draw()
    # Remove the title image from GUI once
    # the Player has finished respawning.
    else:
        # For the 1st spawn, show the title box longer.
        if self.lives < PLAYER_STARTING_LIVES:
            self.title.alpha = 0
        else:
            self.title.alpha -= 0.4
            if self.title.alpha < 0:
                self.title.alpha = 0
        self.title.draw()
    # Show the stats, etc on the GUI.
    output = f"Difficulty: {self.player_sprite.difficulty}"
    arcade.draw_text(output, 10, 60, arcade.color.WHITE, 14)
    output = f"Score: {self.score}"
    arcade.draw_text(output, 10, 40, arcade.color.WHITE, 14)

Now we’ll see the code for reading the keys to change speed and difficulty level:

Keys for speed change

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# Increase game speed and difficulty.
# We chose the "equal" key instead of the "plus"
# key here so the Player does not have to use the
# SHIFT along with "equal" key to get a "+" result.
elif symbol == arcade.key.EQUAL:
    self.player_sprite.difficulty += 1
    # To be sure difficulty has a top potential
    # ceiling of 15.
    if self.player_sprite.difficulty > 15:
        self.player_sprite.difficulty = 15
# Decrease game speed and difficulty.
elif symbol == arcade.key.MINUS:
    self.player_sprite.difficulty -= 1
    # Being sure difficulty has a bottom floor of 1.
    if self.player_sprite.difficulty < 1:
        self.player_sprite.difficulty = 1

Next, we’ll use difficulty to affect enemy movement speed.

Sprite speed

Finally, within the enemy movement code section below is score, which is calculated using difficulty when a brain (bullet) hits the enemy zombie; many uses of difficulty; speed; frustration; and enraged state. We have heavily commented the code for your ease of understanding.

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
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
if not self.game_over:
    self.list_all_sprites.update()
    # Cycle through all bullets currently on screen.
    for bullet in self.list_bullets:
        # Check for bullet collision with enemy
        enemies = arcade.check_for_collision_with_list(bullet, self.list_enemies)

        # Cycle through all (if any) enemies hit by current bullet.
        i = -1
        for enemy in enemies:
            i += 1
            # Give Player points for hitting enemy based on difficulty level.
            self.score += int(100 * self.player_sprite.difficulty)
            # Stop enemy that was hit
            enemy.change_x = 0
            enemy.change_y = 0
            url_image = enemy.url_image
            # Grow the enemy size.
            new_scale = enemy.scale + 0.07
            # When the enemy gets to a certain size,
            # they become "enraged".
            if new_scale > 0.59:
                new_scale = 0.6
                enemy.enraged = True
                # enraged sound enabled for THIS enemy?
                if url_image.find(enemy.guid) > -1:
                    # following builds this: "self.sound_char_02_enraged"
                    build_sound_name = enemy.which
                    arcade.play_sound(eval(build_sound_name))
                else:
                    arcade.play_sound(self.sound_powerup)
                # Change image for "enraged" version with red eyes.
                url_image = url_image.replace("front","enraged")
            else:
                arcade.play_sound(self.sound_powerup)
                # arcade.play_sound(self.sound_munch)
                # arcade.play_sound(self.sound_)

            new_enemy_sprite = EnemySprite(url_image, new_scale)
            if new_scale > 0.59:
                new_enemy_sprite.immunity = ENEMY_ENRAGED_IMMUNE_TO_BLOCKS
            new_enemy_sprite.guid = enemy.guid
            new_enemy_sprite.which = enemy.which
            new_enemy_sprite.center_x = enemy.center_x
            new_enemy_sprite.center_y = enemy.center_y
            new_enemy_sprite.speed = enemy.speed
            new_enemy_sprite.frustration = enemy.frustration
            new_enemy_sprite.enraged = enemy.enraged
            # If you want enemy speed to increase when they
            # are hit, uncommment the following line
            # new_enemy_sprite.speed = enemy.speed + 0.1
            new_enemy_sprite.change_x = 0
            new_enemy_sprite.change_y = 0
            new_enemy_sprite.timer_rand = int(fps * (12 - self.player_sprite.difficulty))
            new_enemy_sprite.timer_smart = 0
            # print(enemy.url_image)
            # print("enemy size: " + str(enemy.size))
            enemy.update()
            new_enemy_sprite.update()
            # Remove the enemy we replaced.
            enemy.remove_from_sprite_lists()
            # Add our new enemy sprite, the replacement,
            # to our Sprite lists.
            self.list_all_sprites.append(new_enemy_sprite)
            self.list_enemies.append(new_enemy_sprite)
            # Delete the bullet that collided with an enemy.
            bullet.remove_from_sprite_lists()

    # if player not invulnerable (respawning):
    if not self.player_sprite.respawning:
        # get player position so enemies can move toward player
        player_pos_x = self.player_sprite.center_x
        player_pos_y = self.player_sprite.center_y

        # Enemy movement
        enemy_number = 0
        for enemy in self.list_enemies:
            enemy_number += 1
            # enemy_speedy = enemy.speed
            enemy.speed = 2 + (self.player_sprite.difficulty/12) + enemy.scale/2.2
            random.seed(datetime.now() + timedelta(0, enemy_number))
            # Update enemy movement timers.
            enemy.timer_rand -= 1
            enemy.timer_smart -= 1

            # Set up/reset variables for enemy direction of movement.
            dir_x = 0
            dir_y = 0

            # Did both enemy movement direction timers run 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 happens.
                random_or_smart = 1000

            # Decide to move the enemy either randomly or "intelligently".
            # Lower the "20" if you want random movement more often.
            if random_or_smart < 20:
                # How long to continue in the random direction?
                enemy.timer_rand = int(fps * 7) # ~ 7 seconds
                enemy.timer_smart = 0
                # Random 8 directions N, S, E, W, NE, SE, NW, SW
                direction = random.randint(1, 8)
                if direction == 1:
                    dir_y = 1
                elif direction == 2:
                    dir_x = 1
                    dir_y = 1
                elif direction == 3:
                    dir_x = 1
                elif direction == 4:
                    dir_x = 1
                    dir_y = 1
                elif direction == 5:
                    dir_y = 1
                elif direction == 6:
                    dir_y = 1
                    dir_x = 1
                elif direction == 7:
                    dir_x = 1
                elif direction == 8:
                    dir_x = 1
                    dir_y = 1
                enemy.change_x = dir_x * (enemy.speed - 2)
                enemy.change_y = dir_y * (enemy.speed - 2)
            elif enemy.timer_rand < 1:
                enemy.timer_rand = 0
                # If smart movement timer runs out,
                # reset it.
                if enemy.timer_smart < 1:
                    # Set smart movement timer to random number between
                    # 1 second and 3 seconds.
                    enemy.timer_smart = random.randint(int(fps * 1), int(fps * 4))
                y_pos = enemy.center_y
                x_pos = enemy.center_x
                # If Player is above enemy, set y direction to up.
                if player_pos_y > y_pos:
                    dir_y = 1
                # If Player is to the right of enemy, set x direction to right.
                if player_pos_x > x_pos:
                    dir_x = 1
                # If Player is below enemy, set y direction to down.
                if player_pos_y < y_pos:
                    dir_y = -1
                # If Player is to the left of enemy, set x direction to left.
                if player_pos_x < x_pos:
                    dir_x = -1
                # Set enemy x and y directions based on above four tests, modified with speed.
                enemy.change_x = dir_x * (enemy.speed - 2)
                enemy.change_y = dir_y * (enemy.speed - 2)
            # Set new position for THIS enemy.
            enemy.center_x += enemy.change_x
            enemy.center_y += enemy.change_y

            # Check for collision between this enemy and all legos.
            if enemy.immunity == False:
                any_collisions = arcade.check_for_collision_with_list(enemy, self.list_blocks)
                if len(any_collisions) > 0:
                    # THIS enemy collided with an obstruction.
                    if enemy.enraged == True:
                        enemy.frustration += 1
                        if enemy.frustration > ENEMY_FRUSTRATION_THRESHOLD - (self.player_sprite.difficulty * 15):
                            enemy.immunity = True
                    # Reverse direction with added random factor.
                    # The added random factor helps the enemy
                    # get around the block they hit.
                    random.seed(datetime.now() + timedelta(0,enemy_number))
                    x_rand = random.randint(1, 50)
                    if x_rand < 25:
                        dir_x *= -2
                    else:
                        dir_y *= -2
                    # Change enemy direction based on above and enemy speed.
                    enemy.change_x = dir_x * (enemy.speed)
                    enemy.change_y = dir_y * (enemy.speed)
                    # Change enemy position based on the above direction change.
                    enemy.center_x += enemy.change_x
                    enemy.center_y += enemy.change_y
                    enemy.timer_rand = 0
                    # Set smart timer to ~4 seconds of this direction so the
                    # enemy for sure gets away from the ice block they hit.
                    enemy.timer_smart = int(fps * 4)
            enemy.update()

Conclusion

In this part nine, we amped up game depth by modifying how to use for game difficulty and Cockroach (though not in this part of the many-part series of articles) for saving users, scoring, and storing/reading the screen configuration in part one. In this part, we learned how to change game speed and difficulty, display it on the screen, and have it affect enemy movement speed, intelligence, and score-keeping. In the articles following this part, we will add mouse detection and score tracking and display.

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.