Last day we created three useful sprite classes (Brick, Circle, and Label). Here’s an example that uses all three Sprite subclasses from our mySprites.py module together:
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 |
# I - Import and Initialize import pygame, mySprites pygame.init() def main(): '''This function defines the 'mainline logic' for our game.''' # Display screen = pygame.display.set_mode((640, 480)) pygame.display.set_caption("Using the mySprites library") # Entities background = pygame.Surface(screen.get_size()) background.fill((255, 255, 255)) screen.blit(background, (0, 0)) # Instantiate our 3 custom sprites and create an OrderedUpdates Sprite Group label = mySprites.Label("Hi There!", (100,100)) circle = mySprites.Circle() wall = mySprites.Brick(screen) allSprites = pygame.sprite.OrderedUpdates(circle, wall, label) # ACTION # Assign clock = pygame.time.Clock() keepGoing = True # Loop while keepGoing: # Time clock.tick(30) # Events for event in pygame.event.get(): if event.type == pygame.QUIT: keepGoing = False # Refresh screen allSprites.clear(screen, background) allSprites.update() allSprites.draw(screen) pygame.display.flip() # Close the game window pygame.quit() # Call the main function main() |
If you look back at our early IDEA/ALTER framework examples in U4-5 and U4-6, you’ll see that the code above isn’t much more complicated. Most of our custom code is encapsulated within our sprite classes, thus simplifying the main game loop as much as possible. This is the power of OOP!
If you move the Circle sprite around with the mouse you’ll see that it appears to move behind the Label and Brick sprites. The reason for this has to do with the order in which they are added to the OrderedUpdates sprite group. Try adding the circle sprite to the group last and you’ll see that it appears to float above the Label and Brick sprites.
Sprite-on-Sprite Collisions
Arcade games are all about things bonking into each other, so being able to detect collisions is important. There are many ways to handle collisions, but we’ll start with the simplest method that simply checks to see whether the bounding rects of two sprites overlap or not. As mentioned in U4-9, a rect object includes a Boolean colliderect() method that accepts another rect as a parameter. If the two rects are overlapping, the method returns True.
Add the following code to the previous example, just after the for loop in the Event (ALTER) section, and before the Refresh (ALTER) section.
1 2 3 4 5 |
# Sprite-on-Sprite Collision Detection and Reporting if circle.rect.colliderect(wall.rect): label.setText("Collision") else: label.setText("No Collision") |
The code above checks for a collision between the circle and wall sprites by calculating whether their bounding rects are overlapping or not. If you play around with this demo a little you’ll notice that if you move the circle close to (but not visually touching) one of the corners of the wall that it reports a collision. The reason for this is that all sprites are rectangular. Even our Circle sprite is really a rectangular sprite with a circle drawn on it. You’ll recall in our original Circle class that we made the black corners of its bounding rect transparent. Even though you can’t see the “corners” of the Circle sprite, they still matter for collision detection purposes. This basic collision detection is “good enough” for our purposes, but there is a technique called “masking” that you can research on your own to make your collision detection more precise.
Multiple-Sprite Collisions
The more sprites you have on the screen, the more potential collisions need to be checked. To simplify collision detection, you can efficiently check if a single sprite has collided with any sprite in an OrderedUpdates sprite group. Consider the following example:
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 |
# I - Import and Initialize import pygame, mySprites pygame.init() def main(): '''This function defines the 'mainline logic' for our game.''' # Display screen = pygame.display.set_mode((640, 480)) pygame.display.set_caption("Using Groups To Check Collisions") # Entities screen = pygame.display.set_mode((640, 480)) background = pygame.Surface(screen.get_size()) background.fill((255, 255, 255)) screen.blit(background, (0, 0)) # Instantiate our Sprites label = mySprites.Label("Hi There!", (100,100)) circle = mySprites.Circle() walls = [] for i in range(10): wall = mySprites.Brick(screen) walls.append(wall) # The wallGroup will be used for collision detection wallGroup = pygame.sprite.OrderedUpdates(walls) # The allSprites group will be used for Refresh, like usual allSprites = pygame.sprite.OrderedUpdates(circle, label, walls) # ACTION # Assign clock = pygame.time.Clock() keepGoing = True # Loop while keepGoing: # Time clock.tick(30) # Events for event in pygame.event.get(): if event.type == pygame.QUIT: keepGoing = False # Multiple-Sprite Collision Detection and Reporting if pygame.sprite.spritecollide(circle, wallGroup, False): label.setText("Collision") else: label.setText("No Collision") # Refresh screen allSprites.clear(screen, background) allSprites.update() allSprites.draw(screen) pygame.display.flip() # Close the game window pygame.quit() # Call the main function main() |
In the Entities (IDEA) section, we create our Circle and Label sprites, as well as a list of 10 Brick wall sprites (called walls). The list of 10 Brick sprites is put into an OrderedUpdates sprite group called wallGroup which will be used when we check if the circle sprite has collided with any of the wallGroup sprites. All of the sprites are added to the usual allSprites group so that we can keep the Refresh (ALTER) section simple.
In the Event (ALTER) handling section, we use the pygame.sprite.spritecollide() function which takes three parameters: (1) a single, primary sprite to check against a group of other sprites, (2) a sprite group to check for collisions with the primary sprite, and (3) a Boolean value. If a collision has occurred and the Boolean parameter is True, then the object in the sprite group is automatically deleted from memory so that it will no longer be updated or displayed in your game. Try setting this parameter to True to see how this works.
If there was no collision between the sprite and any sprite in the OrderedUpdates group, the spritecollide() function returns an empty list [ ]. If there was a collision, the function returns a list of references to all the sprites in the group that collided with the primary sprite. If you want to know which sprite(s) the primary sprite collided with, you can extract this information from the returned list using a for loop. If you simply want to know that a collision happened, you can treat spritecollide() as a Boolean function (since an empty list [ ] = False, and is True otherwise) as I have done in this example.
You Try!
- Start a new page in your Learning Journal titled “4-11 Managing Collisions”. Carefully read the notes above and in your own words summarize the key ideas from each section.