Pygame Shmup Part 3: Collisions (and shooting!)

Tags: python tutorial gamedev pygame

This is part 3 of our “Shmup” project! In this lesson we’ll add collisions between the player and the enemies, as well as adding bullets for the player to shoot.

About this series

In this series of lessons we’ll build a complete game using Python and Pygame. It’s intended for beginning programmers who already understand the basics of Python and are looking to deepen their Python understanding and learn the fundamentals of programming games.

You can watch a video version of this lesson here:

Collisions

Collisions are a fundamental part of game development. Collision detection means that you want to detect whether one object in the game world is touching another object. Collision response is deciding what you want to happen when the collision occurs - does Mario pick up the coin, does Link’s sword damage the enemy, etc.

In our game we currently have a number of enemy sprites flying down the screen towards our player, and we’d like to know when one of those sprites hits. For this stage of our game, we’ll just say that an enemy hitting the player means the game is over.

Bounding boxes

Remember, each sprite in Pygame has a rect attribute that defines its coordinates and its size. A Rect object in Pygame is in the format [x, y, width, height], where x and y represent the upper left corner of the rectangle. Another word for this rectangle is bounding box, because it represents the bounds of the object.

This kind of collision detection is called AABB, which stands for “Axis Aligned Bounding Box”, because the rectangles are aligned with the screen axes - they’re not tilted at an angle. AABB collision is very popular because it’s fast - the computer can compare the coordinates of rectangles very quickly, which is very helpful if you have a large number of objects to compare.

To detect a collision we need to look at the rect of the player and compare it with the rect of each of the mobs. Now we could do this by looping through the mobs and for each one performing this comparison:

In this picture, you can see that only rectangle #3 is colliding with the large black rectangle. #1 is overlapping in the x axis, but not the y; #2 is overlapping in y, but not in x. In order for two rectangles to be overlapping, their bounds must overlap in each axis. Writing this in code:

if mob.rect.right > player.rect.left and \
   mob.rect.left < player.rect.right and \
   mob.rect.bottom > player.rect.top and \
   mob.rect.top < player.rect.bottom:
       collide = True

Fortunately for us, Pygame has a built-in way of doing the above, by using the spritecollide() function.

Colliding mobs with player

We’re going to add this command to the “update” section of our game loop:

#Update
all_sprites.update()

#check to see if a mob hit the player
hits = pygame.sprite.spritecollide(player, mobs, False)
if hits:
    running = False

The spritecollide() function takes 3 arguments: the name of the sprite you want to check, the name of a group you want to compare against, and a True/False parameter called dokill. The dokill parameter lets you set whether the object should be deleted when it gets hit. If, for example, we were trying to see if the player picked up a coin, we would want to set this to True so the coin would disappear.

The result of the spritecollide() command is a list of the sprites that were hit (remember, it’s possible the player collided with more than one mob at a time). We’re assigning that list to the variable hits.

If the hits list is not empty, the if statement will be True, and we set running to False so the game will end.

Shooting back

Bullet sprite

Now we’re ready to add a new sprite: the bullet. This will be a sprite that is spawned when we shoot, appears at the top of the player sprite, and moves upwards at some fairly high speed. Defining a sprite should be starting to look familiar to you by now, so here’s the complete Bullet class:

class Bullet(pygame.sprite.Sprite):
    def __init__(self, x, y):
        pygame.sprite.Sprite.__init__(self)
        self.image = pygame.Surface((10, 20))
        self.image.fill(YELLOW)
        self.rect = self.image.get_rect()
        self.rect.bottom = y
        self.rect.centerx = x
        self.speedy = -10

    def update(self):
        self.rect.y += self.speedy
        # kill if it moves off the top of the screen
        if self.rect.bottom < 0:
            self.kill()

In the __init__() method of the bullet sprite, we’re passing x and y values, so that we can tell the sprite where to appear. Since the player sprite can move, this will be set to where the player is at the time the player shoots. We set speedy to a negative value, so that it will be going upward.

Finally, we check to see if the bullet has gone off the top of the screen, and if so, we delete it.

Keypress event

To keep things simple at first, we’re going to make it so that each time the player presses the spacebar, a bullet will be fired. We need to add that to the events checking:

for event in pygame.event.get():
    # check for closing window
    if event.type == pygame.QUIT:
        running = False
    elif event.type == pygame.KEYDOWN:
        if event.key == pygame.K_SPACE:
            player.shoot()

Our new code checks for a KEYDOWN event, and if there is one, checks to see if it was the K_SPACE key. If it was, we’re going to run the player sprite’s shoot() method.

Spawning the bullet

First we need to add a new group to hold all of the bullets:

bullets = pygame.sprite.Group()

Now, we can add the following method to the Player class:

def shoot(self):
    bullet = Bullet(self.rect.centerx, self.rect.top)
    all_sprites.add(bullet)
    bullets.add(bullet)

All the shoot() method does is spawn a bullet, using the top center of the player as the spawn point. Then we make sure to add the bullet to all_sprites (so it will be drawn and updated) and to bullets, which we’re going to use for the collisions.

Bullet collisions

Now we need to check whether a bullet hits a mob. The difference here is we have multiple bullets (in the bullets group) and multiple mobs (in the mobs group), so we can’t use spritecollide() like before because that only compares one sprite against a group. Instead, we’re going to use groupcollide():

# Update
all_sprites.update()

# check to see if a bullet hit a mob
hits = pygame.sprite.groupcollide(mobs, bullets, True, True)
for hit in hits:
    m = Mob()
    all_sprites.add(m)
    mobs.add(m)

The groupcollide() function is similar to spritecollide(), except that you name two groups to compare, and what you will get back is a list of mobs that were hit. There are two dokill options, one for each of the groups.

If we just delete the mobs, we’ll have a problem: running out of mobs! So what we do is loop through hits and for each mob that we destroy, another new one will be spawned.

Now it’s actually starting to feel like a game:

In the next lesson, we’ll learn how to add graphics to our game instead of using those plain colored rectangles.

Full code for this part

Part 4: Adding Graphics

Comments