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