The last tutorial ended with the creation of a Sprite class. The class had the functionality to move itself as well as detect when it was about to go out of the game screen, and respond to that by changing its direction of movement. In this part, I'm going to show you how to implement simple collision detection in PyGame.
The first question that needs to be answered is; what exactly is collision detection. Well, as you may have already figured out from the name, it is that part of a game that detects when two (or more) objects are about to collide with each other. Technically, that is where the job of the collision detection part of a game ends. But in most simple games, the part that detects collisions is also responsible for what comes next, responding to those collisions.
Collision detection is one of the few pieces that is present in almost all games that have ever been made. I'm sure you've seen almost realistic physics effects in games like FEAR, GTA 4, or even Crayon Deluxe. Heck, even Prince of Persia and Pac-Man had collision detection. The physics effects are a separate topic, and are also usually handled by a different part of the game, appropriately called the Physics Engine. But before the physics engine can go ahead and do its magic, it needs to be activated by something. That something is usually the collision detection part of a game.
Techniques for Collision Detection
Before we can actually start blasting away PyGame code, we need to understand how to detect collisions. There are a number of techniques for doing so. The simplest are listed here along with brief descriptions. If you want more detailed information, I suggest you check out some of the resources listed at the end of the tutorial.
- Bounding Box Collision
- Bounding Circle Collision
- Pixel Overlap Test
Note that in the bounding box/circle tests, it is very difficult to find the exact point where two sprites have collided. The pixel overlap test fixes this by allowing us to find exactly where the two sprites have collided. This can result in better physics simulation. If the sprites always keep the same orientation (they are not rotated), the bounding box/circle method can give us a good approximation of the angle of collision. If however the sprites have rotated before collision, the pixel overlap test is usually the one to go with.
If you remember the last tutorial, we have already used a simple form of the bounding box collision test when we tested the sprites for collisions against the walls. The piece of code that did that is shown below:
def Update(self, scr=None): # start if self.x < 0: self.x = self.maxX if self.x > self.maxX: self.x = 0 if self.y < 0: self.y = self.maxY if self.y > self.maxY: self.y = 0 # end self.rectangle.move_ip(self.x, self.y) if scr != None: scr.blit(self.image, self.GetPosition())
The important parts are the ones between the start and end comments. While not strictly collision tests, they do provide the most rudimentary form of collision detection. This code actually tests if the sprite has moved beyond the borders of the screen and wraps it around to the other side if it has.
Our Collision Detection Engine
What we are going to do today is to create a simple billiards simulation. We will use both the bounding box and the bounding circle collision tests to see the difference in their results. What we are going to create will look like a couple of balls moving on a billiards table, colliding with the walls and each other. While not all that exciting, it may just be the start of a Snooker Club type game.
The game code is in two parts. Firstly, there is the code for the Ball Sprite class. We create this class as it makes it a lot easier to manage things. The code for the Ball class is given here. I'll explain the entire code line by line so that you get a hang of how things are done in Pygame.
class Ball: def __init__(self, radius=50, init_pos=(0, 0), init_speed=[0, 0], color=pygame.Color('red')): """ This function creates a new Ball object. By default, the new Ball has a radius of 50 pixels, a starting position of (0,0), a speed of (0,0) and a red color. """ # create Surface to hold image for both drawing and erasing the Ball self.img = pygame.Surface((radius * 2, radius * 2)) self.bg = pygame.Surface((radius * 2, radius * 2)) # fill both surfaces with the transparent color self.img.fill(transColor) self.bg.fill(transColor) # draw the Ball shape to both the img and the bg surface pygame.draw.circle(self.img, color, (radius, radius), radius) pygame.draw.circle(self.bg, bgColor, (radius, radius), radius) # set the color key for both surfaces self.img.set_colorkey(transColor) self.bg.set_colorkey(transColor) # convert both Surfaces for faster bliting to the screen self.img.convert() self.bg.convert() # create rectangle for the Ball image # give it the initial position that was passed via init_pos self.rect = self.img.get_rect(init_pos) # set the speed of this Ball object self.speed = init_speed def set_speed(self, new_speed): self.speed = new_speed def move(self, bounding_rect): """ Moves the Ball object according to self.speed Takes a pygame.Rect() object in bounding_rect parameter. When moving, checks if the Ball is within the bounds of this rectangle. If not, moves the Ball to correct this situation """ # check if we have a valid bounding_rect. if not, just crash the game # after giving an error message if not isinstance(bounding_rect, pygame.Rect): sys.exit("ERROR: Invalid type for bounding_rect parameter!\n") # once we have done sanity checking, continue with moving the Ball self.rect.move_ip(self.speed, self.speed) # now, check if the Ball is within the bounds of the bounding_rect if bounding_rect.contains(self.rect): pass # nothing to do here, as the Ball is within bounds else: # if the Ball is outside of bounding_rect # first, we find which the direction we are getting out of bounds if self.rect.top < bounding_rect.top: # from the TOP side. we move the Ball to the max top first self.rect.top = bounding_rect.top # then, we check if the Balls current Y velocity will take # it out of bounds again. if it will, we inverse the Y velocity # by multiplying it with -1 if (self.rect.top + self.speed) < bounding_rect.top: self.speed *= -1 elif self.rect.bottom > bounding_rect.bottom: # likewise for bottom side self.rect.bottom = bounding_rect.bottom if (self.rect.bottom + self.speed) > bounding_rect.bottom: self.speed *= -1 # now, we do the same for the left & right side if self.rect.left < bounding_rect.left: self.rect.left = bounding_rect.left if (self.rect.left + self.speed) < bounding_rect.left: self.speed *= -1 elif self.rect.right > bounding_rect.right: self.rect.right = bounding_rect.right if (self.rect.right + self.speed) > bounding_rect.right: self.speed *= -1 def erase(self, surface): # erase the Ball object from its current location surface.blit(self.bg, self.rect) def draw(self, surface): surface.blit(self.img, self.rect)
Let me explain the code:
- First of all, the lines starting with the # symbol are comments. Comments are totally ignored by the computer when running the program, and are just here to ease the understanding of the program by any one reading it.
In both the
movefunction, the first thing you might have noticed is the explanation for the function given between the triple quotes. This is the DocString for the function. In Python, both classes and functions can have DocStrings. DocStrings are kind of like comments, in the sense that they are ignored by the Python language. However, they provide valuable information about a function/class, and are actually accessible from within a Python program, as opposed to comments which are only seen when a person views the actual code. For example, if you want to see the DocString for the move function, you write:
- The import statements, as you already know from the previous tutorials, imports the pygame library as well as other modules needed in the program.
A class definition is started like so:
Next, we have declared a special function named
init. This is a special function in the sense that it is called by Python automatically every time we create a new object from the Ball class. In Python classes, all functions must be passed a first argument
self, which is sort of like a pointer (variable) to the object from which the function was called. As we'll see later in the code, we can call any method on a Ball object by using the syntax:
As you can see, we never pass any parameter, however, Python automatically passes the
selfparameter, making it point to
ballObject, the object which was used to call the function. The code for the
__init__function is given below:
def __init__(self, radius=50, init_pos=(0, 0), init_speed=[0, 0], color=pygame.Color('red')): """ This function creates a new Ball object. By default, the new Ball has a radius of 50 pixels, a starting position of (0,0), a speed of (0,0) and a red color. """ # create Surface to hold image for both drawing and erasing the Ball self.img = pygame.Surface((radius * 2, radius * 2)) self.bg = pygame.Surface((radius * 2, radius * 2)) # fill both surfaces with the transparent color self.img.fill(transColor) self.bg.fill(transColor) # draw the Ball shape to both the img and the bg surface pygame.draw.circle(self.img, color, (radius, radius), radius) pygame.draw.circle(self.bg, bgColor, (radius, radius), radius) # set the color key for both surfaces self.img.set_colorkey(transColor) self.bg.set_colorkey(transColor) # convert both Surfaces for faster bliting to the screen self.img.convert() self.bg.convert() # create rectangle for the Ball image # give it the initial position that was passed via init_pos self.rect = self.img.get_rect(init_pos) # set the speed of this Ball object self.speed = init_speed
Most of the code should be familiar to you from the previous tutorials. First, we create two surfaces to hold the actual image of the Ball and another image with the background to erase it. In the last tutorial, we loaded an image file from disk and created a surface out of it. Here, we do things differently. Rather than use a predefined image file for the Balls, we create the Ball image in the code using pygames built-in functions. This allows us to control many aspects of the image, including radius and color. To draw a circle, we use the function:
pygame.draw.circle(SURFACE, COLOR, CENTER, RADIUS)
All the parameters are self explanatory. The next line of code however, needs some discussion:
What we are doing here is setting a 'Color Key' for the surface. A color key can be thought of as simply a transparent color. We are telling pygame that the transColor (which is previously defined as pure White) is to be treated as transparent. So, whenever pygame blits the surface, it ignores any pixels that have the same color as the color key. This is needed because surfaces can only be rectangular, while the circle we draw is, well, circular. Thus, there is a portion of the rectangular surface that would not be part of the circle. We thus tell pygame to ignore the extra region by setting the color key equal to white. This concept can take some time to understand, so an image is attached to help you comprehend.
Every thing else in the
__init__function is pretty much what you did in the previous tutorials.
movefunction is quite simple. What it does is to move the Ball objects position by adding the velocity to its current position, while checking that the object remains inside a rectangular area that is passed to the function as the parameter
bounding_rect. The comments are quite explanatory and I don't think require any deep discussion. The reason why I created a separate move function is that it makes things a lot simpler. Say you change the way the Ball class handles collisions with the walls, then all that you need to change is the code inside of the class itself. Nothing outside will change. This is called the concept of encapsulation and is one of the biggest benefits of using classes. The details of how the class does what it does are hidden from the code that actually uses objects of the class.
Now we come to the main function. The place where collisions detection is actually done. Compared to the rest of the code, the collision detection is quite simple. The code for the collision detection is given here:
def collision_detect(ball_list, bounding_rect): bList = list(ball_list) for ballA in bList: # remove the current Ball object, as we do not want to test it again bList.remove(ballA) for ballB in bList: # check if the rectangles of the two calls are overlapping # since we are using bounding box collision detection, this is # how we test for a collision if ballA.rect.colliderect(ballB.rect): # inverse the velocity of one of the Ball objects at random b = random.choice([ballA, ballB]) x = b.speed y = b.speed x *= -1 y *= -1 b.set_speed([x, y]) # now, move the Balls away so they don't collide any more while ballA.rect.colliderect(ballB.rect): ballA.move(bounding_rect) ballB.move(bounding_rect)
As parameters, this function receives a list of Balls that need to be checked against each other for collisions, and a rectangular area bounding the movement of the balls. The first thing we do is to create a variable
bListthat holds a copy of all the Ball objects that the function received. Next, we loop through all the Balls in the list. We check if the rectangles of the two Balls we are checking overlap in the following line of code:
Pygames rectangles have a built-in function for checking if two rectangles are colliding with each other. We are simply using that function to check if the rectangles of the two Ball objects are colliding with each other. If they are, we treat it as a collision of the two Ball objects, since we are using bounding box collision test. Once a collision is detected, we chose a random Ball object from amongst the two that we are checking and reverse its velocity so that it will now move away from the other ball to avoid the collision. Next, we move one of the Ball objects until we reach a point when the two Balls are not colliding with each other anymore. While the results are not very accurate or even pretty, this is a simple way of handling collisions. If better results are required, you could change the code that changes the velocities of the balls once a collision has been detected. Everything else need not change.
The rest of the code just uses the Ball class and the collision detection function to create a small demo of the application. It's quite simple and you have already seen it in the previous tutorials.
Well, that's about it. If you were hoping for (or even dreading) lots of Math, sorry to disappoint. The Math is there if you want it, its just that for demonstrating simple collision detection, we do not need to use it. Hope you find this useful.