Python Arcade Collision Detection and Postgres

Introduction

This is part 7 of a multi-part set of articles showing how to create a from-the-top view video game. In this part 7 we add how to use Python Arcade for Collision Detection and Postgres for storing and reading Sprite objects. In this part, we learn all about how to detect collisions between objects; in this case Sprites, using Python’s Arcade library. In the parts (articles) following this one, we’ll learn to create game sounds, keep score, and add a system for tracking and changing game difficulty.

Prerequisites

  • The full Python source code, image files, and mp3 files are all here for download.

  • Online manual for Python Arcade. We use the Arcade framework for quite a bit of the functionality in this Python 8-bit looking game.

  • Be sure to study part 1 through part 6 where we learned to build and set up a game screen with PostgreSQL for storing that data, how to create Sprites from images and Sprite Lists from Sprites, how to respond to key presses, how to move the Player’s Sprite icon on the screen, how to move enemies with some intelligence, and how the player can fire bullets (snowballs) with the space bar and bullet movement.

Python frameworks for the game

Be sure to use PIP to install the following libraries.

1
2
3
4
5
6
7
8
import arcade # Game-oriented library for Python.
from datetime import datetime, timedelta # For seeding random number generation.
import math # For some math / movement physics-related functions.
import os # For accessing the file system to load images and sounds.
import psycopg2 # For Postgres data retrieval for screens.
import pyautogui # For getting monitor resolution to make sure our window is not too large.
import random # For generating random numbers for enemy movement.
import sys # For getting resource files into the game.

An important function we’ll be using in this part of the multi-part series is the check_for_collision_with_list() function. Let’s look at it before going deeper into code that uses it.

Syntax of check_for_collision_with_list

This function allows us to easily find out if a Sprite has collided with any Sprite in a Sprite List.

1
list_of_sprites_hit = arcade.check_for_collision_with_list(individual_sprite, list_of_sprites)

Now that we can see the basics of how the function works, let’s go over the code for creating the Sprites and Sprite Lists we’ll be addressing at the end of this part.

Creating Sprites and Sprite Lists

Initialize all Sprite Lists

1
2
3
4
5
6
# Set up all Sprite lists.
self.list_all_sprites = arcade.SpriteList()
self.list_enemies = arcade.SpriteList()
self.list_snowballs = arcade.SpriteList()
self.list_player_lives = arcade.SpriteList()
self.list_blocks = arcade.SpriteList()

Create Sprite for player

1
2
3
4
5
6
# Initialize a Sprite for the Player and
# set it's image icon from a PNG file.
self.player_sprite = PlayerSprite("./resources/images/penguin.png", SCALE)
# Since there is only one Player Sprite, we don't need a list for "player Sprites".
# but we still do add the Player Sprite to our list of all Sprites.
self.list_all_sprites.append(self.player_sprite)

Create Sprite for bullets

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Create Sprite for bullet and assign it an image and size (scale)
snowball_sprite = SnowballSprite("./resources/images/snowball.png", SCALE)
# Give the bullet Sprite an id
snowball_sprite.guid = "Snowball"
# Set the speed of the bullet
snowball_speed = 12
# Set the angle (direction) the bullet will fly
# based on the angle the Player Sprite is facing
snowball_sprite.change_y = \
    math.cos(math.radians(self.player_sprite.angle)) * snowball_speed
snowball_sprite.change_x = \
    -math.sin(math.radians(self.player_sprite.angle)) \
    * snowball_speed
# Set the beginning coordinates of this new Sprite
# to match the Player Sprite's coordinates
snowball_sprite.center_x = self.player_sprite.center_x
snowball_sprite.center_y = self.player_sprite.center_y
# Update Arcade on all the above settings.
snowball_sprite.update()
# Add the bullet Sprite to our list of all Sprites, which is a sprite list.
self.list_all_sprites.append(snowball_sprite)
# Add the bullet Sprite to our list of bullets
self.list_snowballs.append(snowball_sprite)

Create Sprites for obstacles

Below we will see a Python script pulled from the full project source code that uses the SCREEN_FROM_DATABASE constant to determine if we are getting our screen 1 data from the database or not. If not, we are using the randint() function to randomly place obstacles (ice blocks) on the screen, while creating them.

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
if SCREEN_FROM_DATABASE == False:
    # Place ice blocks on the screen using random.
    for i in range(BLOCKS_NUMBER):
        block = arcade.Sprite("./resources/images/block-ice.png", SCALE)
        block.center_x = random.randint(10, SCREEN_WIDTH)
        block.center_y = random.randint(10, SCREEN_HEIGHT)
        self.list_all_sprites.append(block)
        self.list_blocks.append(block)
else:
    # Place ice blocks on the screen
    # using Postgres. Log in to
    # the database.
    t_host = "db host address"
    t_port = "5432" # default port number.
    t_dbname = "database name"
    t_user = "database user name"
    t_pw = "password"
    # Create connection object.
    db_conn = psycopg2.connect(host=t_host, port=t_port, dbname=t_dbname, user=t_user, password=t_pw)
    # Create cursor object.
    db_cursor = db_conn.cursor()
    #   SQL to get ice blocks configuration for the screen from the database.
    s = ""
    s += "SELECT"
    s += " i_y"
    s += ", i_x"
    s += " FROM tbl_screens_objects"
    s += " WHERE ("
    s += " id_screen = " + ID_SCREEN
    s += " AND t_object_type = 'ice block'"
    s += ")"
    try:
        db_cursor.execute(s)
        # return db_cursor
    except psycopg2.Error as e:
        t_msg = "SQL error: " + e + "/n SQL: " + s
        return render_template("error.html", t_msg = t_msg)
    #  Iterate through the recordset rows returned,
    #  place a block for each row according to the
    #  x and y coordinates provided in that row.
    for each row in db_cursor:
        i_y = row[0]
        i_x = row[1]
        # Create a Sprite for the ice block and give it an image.
        block = arcade.Sprite("./resources/images/block-ice.png", SCALE)
        # Set the block's x and y coordinates.
        block.center_x = i_x
        block.center_y = i_y
        # Add this block to our list of all Sprites.
        self.list_all_sprites.append(block)
        # Add this block to our Sprite list of blocks.
        self.list_blocks.append(block)

Next, we’ll create the Sprites to represent the enemies:

Create Sprites for enemies

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
image_list = ("./resources/images/walrus-red.png",
                "./resources/images/walrus-blue.png",
                "./resources/images/walrus-purple.png",
                "./resources/images/walrus-green.png")
# Make the enemy Sprites
for i in range(6):
    # For random colors
    image_no = random.randint(0,3)
    enemy_sprite = EnemySprite(image_list[image_no], SCALE)
    enemy_sprite.guid = "Enemy"
    # timer_rand initialized for later use in movement
    # which was explored in part 5 of this series.
    enemy_sprite.timer_rand = 0
    # timer_smart initialized for later use...
    enemy_sprite.timer_smart = 0
    # Set this Sprite's coordinates to be random
    enemy_sprite.center_y = random.randint(LIMIT_BOTTOM, LIMIT_TOP+1)
    enemy_sprite.center_x = random.randint(LIMIT_LEFT, LIMIT_RIGHT+1)
    # Set this Sprite's movement direction to be random
    enemy_sprite.change_x = int(random.random() * 2 + (DIFFICULTY/10) - 1)
    enemy_sprite.change_y = int(random.random() * 2 + (DIFFICULTY/10) - 1)
    # Set the size of this Sprite.
    enemy_sprite.size = 4
    # Add this Sprite to both applicable Sprite lists
    self.list_all_sprites.append(enemy_sprite)
    self.list_enemies.append(enemy_sprite)

Now we’ll finish with some more actual game code where we look at how to detect and handle collisions for all of the Sprites and Sprite Lists we created above.

Sprite collision detection

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
# Cycle through all snowballs currently on 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:
        # A sound will eventually go in this spot.
        # 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()

for enemy in self.list_enemies:
    # Check for collision between current enemy and all ice blocks.
    any_collisions = arcade.check_for_collision_with_list(enemy, self.list_blocks)
    if len(any_collisions) > 0:
        # THIS enemy hit a block.
        # Reverse direction with added random factor.
        # The added random factor helps the enemy not
        # get stuck at 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_speedy)
        enemy.change_y = dir_y * (enemy_speedy)
        # 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 ~2 seconds of this direction so the
        # enemy for sure gets away from the ice block they hit.
        enemy.timer_smart = int(fps * 2)
    enemy.update()

# Check for collision between player and all blocks.
# NOTE: You can remove this section if you want the player to NOT
# be impeded by ice blocks while only the enemies are. But this
# will reduce challenge.
any_collisions = arcade.check_for_collision_with_list(self.player_sprite, self.list_blocks)
if len(any_collisions) > 0:
    # Player hit a block, so change facing
    # angle to opposite of what it was.
    self.player_sprite.angle *= -1
    self.player_sprite.change_x = -math.sin(math.radians(self.player_sprite.angle)) * self.player_sprite.speed
    self.player_sprite.change_y = math.cos(math.radians(self.player_sprite.angle)) * self.player_sprite.speed
    self.player_sprite.center_x += self.player_sprite.change_x
    self.player_sprite.center_y += self.player_sprite.change_y

# Check for collision between player and all enemies
enemies = arcade.check_for_collision_with_list(self.player_sprite, self.list_enemies)
#   If list of enemies hit created above has at least one enemy:
if len(enemies) > 0:
    # Sound will eventually go in this spot
    # to represent player getting killed and eaten.
    self.eaten = True
    # Remove a life from count
    if self.lives > 0:
        self.lives -= 1

        self.player_sprite.respawn()
        # If you want the collision to destroy the enemy, add this code:
        #   enemies[0].remove_from_sprite_lists()
        self.list_player_lives.pop().remove_from_sprite_lists()
    else:
        self.game_over = True
else:
    self.eaten = False

Conclusion

In this part 7 of the multi-part set of articles showing how to create a video game of a skating Penguin in Python, we added how to use Python’s Arcade library for Collision Detection between Sprites and PostgreSQL for storing and reading Sprite objects such as ice blocks in this case. In this part, we learn all about how to detect collisions between objects. In the articles following this one, we’ll learn to set up and play game sounds, keep score, and add a system for managing game difficulty.

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.