Pygame Shmup Part 3: Collisions (and shooting!)
Fri, Aug 19, 2016This 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.