Python Arcade Enemy Movement and Postgres

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

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

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.