Lesson 086: Pygame: Collision Detection | Learning Records

Posted by TreeNode on Sun, 05 May 2019 15:48:04 +0200

Today we are going to learn collision detection. Most games need to do collision detection because you need to know whether the ball collided, whether the bullet hit the target, whether the protagonist stepped on the shit.

How should that be achieved?

To put it bluntly, the principle is simple, that is, to detect whether there is overlap between the two elves, such as the ball in our last lesson, in the case of Figure 1, they do not overlap, that is, there is no collision.

Figure 1

When the collision occurs, width = r1 + r2, as shown in Figure 2.

Figure 2

When they overlap and intersect, width < R1 + r2, as shown in Figure 3:

Figure 3

So we can judge whether the two balls collide or not. We just need to check whether the distance between the centers of the two balls is less than or equal to the sum of their radii.

Let's first try to write our own collision detection function:

(Of course, I know that there are ready-made sprite modules, but I suggest that before you learn anything, we should understand its principles, understand its principles, and then you will learn other languages and other knowledge with half the effort.)

The function name is collide_check().

from pygame.locals import *
from random import *
import pygame
import math
import sys

class Ball(pygame.sprite.Sprite) :  #Inheritance of animation wizard base class
    def __init__ (self,imgae,position,speed,bg_size) :
        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.image.load(imgae).convert_alpha()   
        self.rect = self.image.get_rect()   #Get the size of the ball
        self.rect.left , self.rect.top = position   #Place the ball where it appears
        self.speed = speed  #set speed
        self.width , self.height = bg_size[0] , bg_size[1]  #Getting the active boundary is the boundary of the background.

    def move(self):
        self.rect = self.rect.move(self.speed)  #Move at your own speed

        if self.rect.right < 0:    #The right side of the picture has gone beyond the left side of the boundary, that is, the whole ball has gone out of bounds.
            self.rect.left = self.width    #Let him come back from the right border
        if self.rect.bottom < 0:    #The bottom of the picture is beyond the top of the boundary.
            self.rect.top = self.height   #Let him come back from the bottom
        if self.rect.left > self.width:   #The left side of the picture is beyond the right side of the boundary.
            self.rect.right = 0     #Let him come back from the left
        if self.rect.top > self.height:  #If the top of the picture has gone beyond the bottom of the boundary
            self.rect.bottom = 0    #Let him come back from the top

#Judging Collision Detection Function
def collide_check(item,target):
    col_balls = []      #Adding collision balls
    for each in target:     #Detection of all target balls in target
        #Distance between two spherical centers
        distance = math.sqrt( math.pow( (item.rect.center[0] - each.rect.center[0]) , 2 )  + \
                                        math.pow( (item.rect.center[1] - each.rect.center[1]) , 2) )
        if distance <= ( item.rect.width + each.rect.width ) / 2:   #If the distance is less than or equal to the sum of the radii between the two, that is half of the sum of the two diameters.
            col_balls.append(each)  #Add the collision ball to the list
    
    return col_balls


def main() :
    pygame.init()

    bg_image = r"D:\Code\Python\Pygame\pygame6: animated sprites\background.png"
    ball_image = r"D:\Code\Python\Pygame\pygame6: animated sprites\gray_ball.png" 
    
    running = True  #There are many ways to exit the program for the sake of the future.
    
    bg_size = width , height = 1024 , 681       #Background size
    screen = pygame.display.set_mode(bg_size) # Set the background size
    background = pygame.image.load(bg_image).convert_alpha()       #Background of painting

    balls = []

    # Create five balls
    BALL_NUM = 5

    for i in range (BALL_NUM) :    #Generate 5 balls
        position = randint (0,width-100) ,  randint(0,height-100)   #The reason for subtracting 100 is that the size of the sphere image is 100 and the position is randomly generated.
        speed  = [ randint (-10,10) , randint(-10,10) ]
        ball  = Ball(ball_image,position,speed,bg_size)  #Objects that generate balls
        balls.append(ball)  #Add all sphere objects to the class table for easy management
        
    clock = pygame.time.Clock()    #Generating refresh frame rate controller

    while running :
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()

        screen.blit(background, (0, 0)) #Draw the background onto screen

        for each in balls:  #Each ball is moved and redrawn
            each.move()
            screen.blit(each.image, each.rect)
        
        for i in range (BALL_NUM) : #Cycle 5 balls to determine if the ball has collided with the other four balls.
            item = balls.pop(i)    #Because it's judgment and the other four balls, you need to take the ball out first.
            if collide_check( item , balls ):  #Call the collision detection function. If the result is true, there is a collision ball.
                item.speed[0] = - item.speed[0]     #Backward motion after collision
                item.speed[1] = - item.speed[1]
            balls.insert(i , item)  #Finally, don't forget to put the ball back in place.

        pygame.display.flip()
        clock.tick(30)

if __name__ == "__main__":
    main()


Here we can see that there are still some shortcomings.

The two balls will collide constantly, and then they can't get away from each other. The reason is explained in the video. It's not difficult to understand, so it's no longer described.
The solution is also simple: a collision detection is carried out when a small ball is generated, and if it collides, a small ball can be generated again.

from pygame.locals import *
from random import *
import pygame
import math
import sys

class Ball(pygame.sprite.Sprite) :  #Inheritance of animation wizard base class
    def __init__ (self,imgae,position,speed,bg_size) :
        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.image.load(imgae).convert_alpha()   
        self.rect = self.image.get_rect()   #Get the size of the ball
        self.rect.left , self.rect.top = position   #Place the ball where it appears
        self.speed = speed  #set speed
        self.width , self.height = bg_size[0] , bg_size[1]  #Getting the active boundary is the boundary of the background.

    def move(self):
        self.rect = self.rect.move(self.speed)  #Move at your own speed

        if self.rect.right < 0:    #The right side of the picture has gone beyond the left side of the boundary, that is, the whole ball has gone out of bounds.
            self.rect.left = self.width    #Let him come back from the right border.
        if self.rect.bottom < 0:    #The bottom of the picture is beyond the top of the boundary.
            self.rect.top = self.height   #Let him come back from the bottom
        if self.rect.left > self.width:   #The left side of the picture is beyond the right side of the boundary.
            self.rect.right = 0     #Let him come back from the left
        if self.rect.top > self.height:  #If the top of the picture has gone beyond the bottom of the boundary
            self.rect.bottom = 0    #Get him back from the top

#Judging Collision Detection Function
def collide_check(item,target):
    col_balls = []      #Adding collision balls
    for each in target:     #Detection of all target balls in target
        #Distance between two spherical centers
        distance = math.sqrt( math.pow( (item.rect.center[0] - each.rect.center[0]) , 2 )  + \
                                        math.pow( (item.rect.center[1] - each.rect.center[1]) , 2) )
        if distance <= ( item.rect.width + each.rect.width ) / 2:   #If the distance is less than or equal to the sum of the radii between the two, that is half of the sum of the two diameters.
            col_balls.append(each)  #Add the collision ball to the list
    
    return col_balls


def main() :
    pygame.init()

    bg_image = r"D:\Code\Python\Pygame\pygame6: animated sprites\background.png"
    ball_image = r"D:\Code\Python\Pygame\pygame6: animated sprites\gray_ball.png" 
    
    running = True  #There are many ways to exit the program for the sake of the future.
    
    bg_size = width , height = 1024 , 681       #Background size
    screen = pygame.display.set_mode(bg_size) # Set the background size
    background = pygame.image.load(bg_image).convert_alpha()       #Background of painting

    balls = []

    # Create five balls
    BALL_NUM = 5

    for i in range (BALL_NUM) :    #Generate 5 balls
        position = randint (0,width-100) ,  randint(0,height-100)   #The reason for subtracting 100 is that the size of the sphere image is 100 and the position is randomly generated.
        speed  = [ randint (-10,10) , randint(-10,10) ]
        ball  = Ball(ball_image,position,speed,bg_size)  #Objects that generate balls
        while collide_check(ball,balls):    #If the generated ball collides with the previous ball, the ball is regenerated.
            ball.rect.left , ball.rect.top = randint (0,width-100) ,  randint(0,height-100)     
        balls.append(ball)  #Add all sphere objects to the class table for easy management
        
    clock = pygame.time.Clock()    #Generating refresh frame rate controller

    while running :
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()

        screen.blit(background, (0, 0)) #Draw the background onto screen

        for each in balls:  #Each ball is moved and redrawn
            each.move()
            screen.blit(each.image, each.rect)
        
        for i in range (BALL_NUM) : #Cycle 5 balls to determine if the ball has collided with the other four balls.
            item = balls.pop(i)    #Because it's judgment and the other four balls, you need to take the ball out first.
            if collide_check( item , balls ):  #Call the collision detection function. If the result is true, there is a collision ball.
                item.speed[0] = - item.speed[0]     #Backward motion after collision
                item.speed[1] = - item.speed[1]
            balls.insert(i , item)  #Finally, don't forget to put the ball back in place.

        pygame.display.flip()
        clock.tick(30)

if __name__ == "__main__":
    main()

It's collision detection written by myself. Next, let's talk about ready-made ones.

Why do we talk about the existing collision detection functions provided by sprite? I wonder if you have noticed that our collide_check() function is only applicable to collision detection between circles, other polygons (such as rectangles, triangles) and some irregular polygons, so what to do, we will not get the corresponding results, we can try to write a collision detection function for each case, nor can we.

But Pygame's Sprite module has actually provided a mature collision detection function for us to use, which is why we want to inherit our class from Sprite base class of Sprite module.

The sprite module provides a spritecollide() method to detect whether a sprite collides with other sprites in the specified group. (Basically similar to the collide_check() principle we wrote)

spritecollide(sprite, group, dokill, collided = None)

The first parameter, sprite, is to specify the detected wizard; (that is, the item we wrote in it)

The second parameter group is to specify a group (that is, the target list we wrote), which is the group of sprite, so sprite.Group() is used to generate it.

The third parameter, dokill, is to set whether the collision detection wizard is deleted from the group, and to True, then delete.

The fourth parameter collided is to specify a callback function to customize a particular detection method. If the fourth parameter is ignored, the default is to detect rect attributes between wizards.

(We can't use the default detection method here. Look at the picture below. The situation in the picture will be detected as a collision, but actually the two balls haven't collided yet.)

The collide_circle(left, right) method is suitable for detecting collisions between two circles. The parameters of left and right are two wizards respectively, so we use it directly.

collided = pygame.sprite.collide_circle

In addition, the collide_circle() method requires the wizard to have a radius attribute, so we add a radius attribute to the Ball class.

The code is as follows:

from pygame.locals import *
from random import *
import pygame
import math
import sys

class Ball(pygame.sprite.Sprite) :  #Inheritance of animation wizard base class
    def __init__ (self,imgae,position,speed,bg_size) :
        pygame.sprite.Sprite.__init__(self)

        self.image = pygame.image.load(imgae).convert_alpha()   
        self.rect = self.image.get_rect()   #Get the size of the ball
        self.rect.left , self.rect.top = position   #Place the ball where it appears
        self.speed = speed  #set speed
        self.width , self.height = bg_size[0] , bg_size[1]  #Getting the active boundary is the boundary of the background.
        self.radius = self.rect.width / 2

    def move(self):
        self.rect = self.rect.move(self.speed)  #Move at your own speed

        if self.rect.right < 0:    #The right side of the picture has gone beyond the left side of the boundary, that is, the whole ball has gone out of bounds.
            self.rect.left = self.width    #Let him come back from the right border.
        if self.rect.bottom < 0:    #The bottom of the picture is beyond the top of the boundary.
            self.rect.top = self.height   #Let him come back from the bottom
        if self.rect.left > self.width:   #The left side of the picture is beyond the right side of the boundary.
            self.rect.right = 0     #Let him come back from the left
        if self.rect.top > self.height:  #If the top of the picture has gone beyond the bottom of the boundary
            self.rect.bottom = 0    #Let him come back from the top

def main() :
    pygame.init()

    bg_image = r"D:\Code\Python\Pygame\pygame6: animated sprites\background.png"
    ball_image = r"D:\Code\Python\Pygame\pygame6: animated sprites\gray_ball.png" 
    
    running = True  #There are many ways to exit the program for the sake of the future.
    
    bg_size = width , height = 1024 , 681       #Background size
    screen = pygame.display.set_mode(bg_size) # Set the background size
    background = pygame.image.load(bg_image).convert_alpha()       #Background of painting

    balls = []
    group = pygame.sprite.Group()   #Because you need to use your own group to use your own functions, here we create a

    # Create five balls
    BALL_NUM = 5

    for i in range (BALL_NUM) :    #Generate 5 balls
        position = randint (0,width-100) ,  randint(0,height-100)   #The reason for subtracting 100 is that the size of the sphere image is 100 and the position is randomly generated.
        speed  = [ randint (-10,10) , randint(-10,10) ]
        ball  = Ball(ball_image,position,speed,bg_size)  #Objects that generate balls
        while pygame.sprite.spritecollide(ball , group , False , pygame.sprite.collide_circle):    #If the generated ball collides with the previous ball, the ball is regenerated.
            ball.rect.left , ball.rect.top = randint (0,width-100) ,  randint(0,height-100)     
        balls.append(ball)  #Add all sphere objects to the class table for easy management
        group.add(ball) #Group has add () and remove () methods

    clock = pygame.time.Clock()    #Generating refresh frame rate controller

    while running :
        for event in pygame.event.get():
            if event.type == QUIT:
                sys.exit()

        screen.blit(background, (0, 0)) #Draw background aaa onto screen

        for each in balls:  #Each ball is moved and redrawn
            each.move()
            screen.blit(each.image, each.rect)
        
        for each in group : #Cycle 5 balls to determine if the ball has collided with the other four balls.
            group.remove(each)    #Because it's judgment and the other four balls, you need to take the ball out first.

            if pygame.sprite.spritecollide(each,group,False,pygame.sprite.collide_circle):  #Call the collision detection function. If the result is true, there is a collision ball.
                each.speed[0] = - each.speed[0]     #Backward motion after collision
                each.speed[1] = - each.speed[1]
            group.add(each)  #Finally, don't forget to put the ball back in place.

        pygame.display.flip()
        clock.tick(30)

if __name__ == "__main__":
    main()

Topics: Python less Attribute