Python Arcade Enemy Movement and Postgres
Introduction
Welcome to part 5 of a multi-part set of tutorials on creating a 2D video game from the 80’s. In this part 5 we learn how to use Python Arcade for enemy movement and Postgres for reading and writing Sprite objects for the screen.
In this part, we focus on initializing the enemy Sprites and relying heavily on Python’s “random” library to move the enemies around the screen in two different manners; (1) randomly; and (2) with “intelligence”, meaning movement toward the Player.
In the remaining articles following this one, we’ll work on bullet firing, sounds, detecting collisions, player score keeping, and incorporating a game difficulty subsystem.
Prerequisites
Be sure to study part 1 through part 4 where we learned to create and populate a game window with Postgres as our back-end for storing that data, how to create Sprites and Sprite Lists, how to read key presses, and various aspects of moving the Player around the screen.
The working source code, images, and sound files are free here.
Python Arcade documentation. We use the Arcade framework for a large amount of the game-related functionality of this 2D game.
Use PIP to install the arcade, datetime, flask, math, os, psycopg2, pyautogui, and random libraries. As we move through each lesson, we’ll use more functions dependent upon these frameworks.
Necessary Python libraries
1 2 3 4 5 6 7 | import arcade # Game-making-oriented library for Python. import math # For various math functions. import random # For various random number generation. 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. |
Set up necessary constants
Here, as usual for these subarticles, we are including only the parts of the primary game source code that are relevant to this lesson and leaving out the rest. This means if you want to see the full list of constants we set up near the beginning, pull up the source code we linked to above.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 | # Set up constants DIFFICULTY = 5 ENEMY_COUNT_INITIAL = DIFFICULTY + 2 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 |
Python Arcade set up enemy Sprites
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 | # MAKE THE ENEMIES # Create a Python list of strings for storing four image URLs: image_list = ("./resources/images/walrus-red.png", "./resources/images/walrus-blue.png", "./resources/images/walrus-purple.png", "./resources/images/walrus-green.png") # Iterate through a number of enemies, in this case, # we are using the ENEMY_COUNT_INITIAL constant we set # up above. for i in range(ENEMY_COUNT_INITIAL): # For random colors: image_no = random.randint(0,3) # For non-random, use "image_no = i" in this spot. # The "EnemySprite" call you see below is a class we will # create down below. enemy_sprite = EnemySprite(image_list[image_no], SCALE) # Initialize our "timers", which are more like counters for # making sure the Sprite continues for some time in the # direction/manner we'll determine later. enemy_sprite.timer_rand = 0 enemy_sprite.timer_smart = 0 # Set random starting positions for each enemy Sprite. enemy_sprite.center_y = random.randint(LIMIT_BOTTOM, LIMIT_TOP+1) enemy_sprite.center_x = random.randint(LIMIT_LEFT, LIMIT_RIGHT+1) # Set a random movement direction for each enemy Sprite. enemy_sprite.change_x = int(random.random() * 2 + (DIFFICULTY/10) - 1) enemy_sprite.change_y = int(random.random() * 2 + (DIFFICULTY/10) - 1) # Set enemy Sprite size. enemy_sprite.size = 4 # Add current Sprite to the list of all Sprites. self.list_all_sprites.append(enemy_sprite) # Add current Sprite to the list of all enemy Sprites. self.list_enemies.append(enemy_sprite) |
Here’s the class we created to represent an enemy Sprite:
1 2 3 4 5 6 7 8 9 | # Class for the 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.size = 0 self.timer_rand = 0 self.timer_smart = 0 self.speed = 2 + (DIFFICULTY/10) |
Notice how in the above code, we initilize our timers (counters) and set the enemy Sprites’ speed to be 2 plus difficulty divided by 10. So yes, speed can be a “real number” type, not needing to be an integer.
Handle enemy movement
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 | # Move the enemy. def update(self): # If enemy is at a screen boundary, # cause a "bounce" toward opposite direction. super().update() if self.center_x < LIMIT_LEFT: self.center_x = LIMIT_LEFT self.change_x *= -1 if self.center_x > LIMIT_RIGHT: self.center_x = LIMIT_RIGHT self.change_x *= -1 if self.center_y > LIMIT_TOP: self.center_y = LIMIT_TOP self.change_y *= -1 if self.center_y < LIMIT_BOTTOM: self.center_y = LIMIT_BOTTOM self.change_y *= -1 |
Random enemy movement
Now for the challenging and potentially most fun part! Here is where we will first do a random check to decide whether to use random movement vs. “intelligent” movement, then we’ll go into each of those movement types and set them up.
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 | def on_update(self, x_delta): # Calculate frames per second and call the variable "fps". fps = x_delta * 3600 # print("fps: " + str(fps)) # Set up enemy_speedy variable for local-to-this-function use. enemy_speedy = 2 + (self.player_sprite.difficulty/12) if not self.game_over: # Update all Sprites for Arcade. self.list_all_sprites.update() # If the player not respawning (invulnerable): if not self.player_sprite.respawning: # Get the player Sprite's position so enemies can move toward player, if # intelligent mode is picked. player_pos_x = self.player_sprite.center_x player_pos_y = self.player_sprite.center_y # ENEMY MOVEMENT # Cycle through each enemy in the enemy Sprite list. for enemy in self.list_enemies: # Reset the random seed using the current time, so # we get more truly random numbers when we use the randint() # function below. random.seed(datetime.now() + timedelta(0, enemy_number)) # Update the two enemy movement timers in a countdown fashion. 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 whether to move enemy randomly or "intellligently". # 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 * 6) # ~ 6 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_speedy - 2) enemy.change_y = dir_y * (enemy_speedy - 2) elif enemy.timer_rand < 1: enemy.timer_rand = 0 # If the movement timer for smart movement runs out, # reset it here. 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 * 3)) y_pos = enemy.center_y x_pos = enemy.center_x # If Player Sprite is above enemy, set y direction to up. if player_pos_y > y_pos: dir_y = 1 # If Player Sprite is to the right of enemy, set x direction to right. if player_pos_x > x_pos: dir_x = 1 # If Player Sprite is below enemy, set y direction to down. if player_pos_y < y_pos: dir_y = -1 # If Player Sprite is to the left of enemy, set x direction to left. if player_pos_x < x_pos: dir_x = -1 # Set the current enemy Sprite's x and y directions based on above # four tests, modified with speed. enemy.change_x = dir_x * (enemy_speedy - 2) enemy.change_y = dir_y * (enemy_speedy - 2) # Set a new x/y position on the screen for THIS enemy. enemy.center_x += enemy.change_x enemy.center_y += enemy.change_y |
Conclusion
In this part 5 of a multi-part series of articles for creating our own 2D top-down game, we studied how to use the Python Arcade library to move the enemies around the screen in both random and “smart” ways. We also looked at how to initialize the enemy Sprites; give them images, positions, and speeds; and add them to Arcade Sprite Lists.
In the next part 6, we will look at firing bullets (snowballs in this game), followed by collision detection, sound effects, the ways game difficulty changes various game dynamics, and tracking/displaying score.
Pilot the ObjectRocket Platform Free!
Try Fully-Managed CockroachDB, Elasticsearch, MongoDB, PostgreSQL (Beta) or Redis.
Get Started