Pygame Shmup Part 5: Improved Collisions

Tags: python tutorial gamedev pygame

This is part 5 of our “Shmup” project. If you haven’t already read through the previous parts, please start with Part 1. In this lesson we’ll discuss how to change the way Pygame handles collisions between sprites.

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:

What’s going on with our collisions?

In the previous video we added graphics, changing our sprites from plain rectangles to nicer-looking PNG images. However, this has introduced a problem: sometimes the game counts a collision between the player and a meteor when it doesn’t look like they hit at all. To understand what’s going on, let’s look at a diagram:

The default type of collision in Pygame is using the collide_rect() function, which uses the two sprites’ rect attributes to calculate whether they overlap. This is called AABB collision and it’s very fast and reliable. However, if the sprite images are not rectangles then you get a situation like in the picture. The rectangles overlap, so collide_rect() is True, but the player will feel frustrated, because they feel like they should have successfully dodged the meteor.

If you find yourself in this situation, there are a few things you can try:

By using the collide_rect_ratio() function instead, you can use a smaller rectangle, reducing the amount of “empty” space that could count as an overlap. Depending on the shape of the sprite, this can work quite well. Notice in the image above how the tips of the ship’s wings are outside the rect. This means that in some cases the meteor will appear to go through the wing without counting as a hit. This is actually a good situation! At the speed things are moving in the game, the player won’t notice this directly, but will just feel like they “got away with” a really close dodge. Instead of feeling frustrated, they will feel like they are doing really well.

Another option is to use a circular bounding box. In the case of the meteor, this is a really good fit. It doesn’t fit the ship quite as well, but again the wings being outside the collision won’t be a negative thing.

Setting the sprite’s radius

Based on the options above, we’re going to choose circles for our meteor vs. player collisions. Pygame makes this easy to do - we just need to set a new attribute on each of our sprites: self.radius.

Let’s start with the player. How big should be make the collision circle? It can take a little bit of experimentation to get the right value. Here’s how to do that, in the player sprite’s __init()__:

	self.rect = self.image.get_rect()
	self.radius = 25
	pygame.draw.circle(self.image, RED, self.rect.center, self.radius)

We are drawing a red circle on top of the player image so that we can see how it looks. Let’s do the same for the meteor:

	self.rect = self.image.get_rect()
	self.radius = int(self.rect.width / 2)
	pygame.draw.circle(self.image, RED, self.rect.center, self.radius)

In this case, we are planning ahead a little bit. We may decide to use different sized meteor images. By setting the radius to 12 of the width of the image, we can do that without having to adjust the code later.

Here is what we end up with:

You can see we probably have too large a radius for the player sprite - it’s actually bigger in the y axis than the size of the ship. To get closer to the example above, let’s set self.radius = 20 on the player.

For the meteor, we’d like a little bit of the corners to stick out, so let’s scale the circle to 85% of the size:

	self.radius = int(self.rect.width * .85 / 2)

Changing the collision type

To have the game start using those circles for the collision tests, we just need to change the spritecollide command to use the circle function instead of the AABB one:

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

Once you have tried it out and you’re happy with how the collisions are working, you can remove the red circles. I recommend you just comment the commands out rather than deleting them, in case you ever want to use them again in the future.

Wrapping up

Deciding on the right collision style can make a big difference in how your game feels. We have a much better meteor vs. player collision now, but notice that we did not change the bullet vs. meteor collision style. Circles would be a bad choice for the shape of the bullets, so it’s better to leave them as rectangles.

In the next part we’ll liven things up by learning how to add animation to our sprites.

Full code for this part

Part 6: Sprite Animation

Comments