Search code examples
pythonpython-3.xpygamecollision-detection2d-games

Detecting collisions between polygons and rectangles in Pygame


So I am trying to make an among us type game with pygame. I just started, so I don't have much of anything and am working on the map right now. However, one thing I'm struggling with is the collision logic. The map has an elongated octagon shape for now, but I think no matter the shape I will use something like a pygame polygon. When I ran the code I have now, which checks for a collision between my player (pygame rectangle) and the walls (pygame polygon) it says:

TypeError: Argument must be rect style object

I've figured out this is because of the pygame polygon returning a rectangle, but not being classified that way in the collision checker. I have tried a library called collision, and credit to the collision detection for giving a great effort, but the player was still able to glitch through the walls. Sidenote: I saved the code where I used this library if anyone wants to see it and maybe improve upon my faults.

Anyway, to boil it all down:

I need a way to detect collisions (really, really preferably in pygame) between polygons and rectangles

Thank you for any help you can give and if you have a question/request please leave a comment.

Heres my code:

import pygame
pygame.init()
W, H=500, 500
screen = pygame.display.set_mode([500, 500])
running = True

bcg=(200, 200, 200)
red=(255, 0 ,0)
purp=(255, 0, 255)
wall=(100, 100, 100)

class player:
    def bg(self):        
        screen.fill(bcg)
        x,y=self.x,self.y

        self.outer=(
                (x,y),
                (x+800, y),
                (x+1200, y+200),
                (x+1200, y+600),
                (x+800, y+800),
                (x, y+800),
                (x-400, y+600),
                (x-400, y+200),
                
                (x,y),
                
                (x, y+50),
                (x-350, y+225),
                (x-350, y+575),
                (x, y+750),
                (x+800, y+750),
                (x+1150, y+575),
                (x+1150, y+225),
                (x+800, y+50),
                (x, y+50)
            )
        
        pygame.draw.polygon(screen, wall, self.outer)
        
    
    def __init__(self, color, size=20, speed=0.25):
        self.x=0
        self.y=0
        self.col=color
        self.size=size
        self.speed=speed


    def draw(self):
        s=self.size
        self.rect=pygame.Rect(W/2-s/2, H/2-s/2, self.size, self.size)
        pygame.draw.rect(screen, self.col, self.rect)

    def move(self, x, y):
        x*=self.speed
        y*=self.speed
        if not self.rect.colliderect(self.outer):
            self.x+=x
            self.y+=y
        

        

p=player(red)

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

    p.bg()

    keys=pygame.key.get_pressed()
    
    if keys[pygame.K_a]: p.move(1, 0)
    if keys[pygame.K_d]: p.move(-1, 0)
    if keys[pygame.K_w]: p.move(0, 1)
    if keys[pygame.K_s]: p.move(0, -1)

    p.draw()

    pygame.display.update()

pygame.quit()

Solution

  • Write a function collideLineLine that test if to line segments are intersecting. The algorithm to this function is explained in detail in the answer to the question pygame, detecting collision of a rotating rectangle:

    def collideLineLine(l1_p1, l1_p2, l2_p1, l2_p2):
    
        # normalized direction of the lines and start of the lines
        P  = pygame.math.Vector2(*l1_p1)
        line1_vec = pygame.math.Vector2(*l1_p2) - P
        R = line1_vec.normalize()
        Q  = pygame.math.Vector2(*l2_p1)
        line2_vec = pygame.math.Vector2(*l2_p2) - Q
        S = line2_vec.normalize()
    
        # normal vectors to the lines
        RNV = pygame.math.Vector2(R[1], -R[0])
        SNV = pygame.math.Vector2(S[1], -S[0])
        RdotSVN = R.dot(SNV)
        if RdotSVN == 0:
            return False
    
        # distance to the intersection point
        QP  = Q - P
        t = QP.dot(SNV) / RdotSVN
        u = QP.dot(RNV) / RdotSVN
    
        return t > 0 and u > 0 and t*t < line1_vec.magnitude_squared() and u*u < line2_vec.magnitude_squared()
    

    Write the function colideRectLine that test if a rectangle and a line segment is intersecting. To test if a line segment intersects a rectangle, you have to test if it intersect any of the 4 sides of the rectangle:

    def colideRectLine(rect, p1, p2):
        return (collideLineLine(p1, p2, rect.topleft, rect.bottomleft) or
                collideLineLine(p1, p2, rect.bottomleft, rect.bottomright) or
                collideLineLine(p1, p2, rect.bottomright, rect.topright) or
                collideLineLine(p1, p2, rect.topright, rect.topleft))
    

    The next function collideRectPolygon tests if a polygon and a rectangle are intersecting. This can be achieved by testing each line segment on the polygon against the rectangle in a loop:

    def collideRectPolygon(rect, polygon):
        for i in range(len(polygon)-1):
            if colideRectLine(rect, polygon[i], polygon[i+1]):
                return True
        return False
    

    Finally you can use collideRectPolygon for the collision test. Note, however, that for the test you need to use the polygon as if the player were moving:

    class player:
        def bg(self):        
            screen.fill(bcg)
            self.outer = self.createPolygon(self.x, self.y)
            pygame.draw.polygon(screen, wall, self.outer)
      
        def createPolygon(self, x, y):
            return [
                (x,y), (x+800, y), (x+1200, y+200), (x+1200, y+600), 
                (x+800, y+800), (x, y+800), (x-400, y+600), (x-400, y+200),           
                (x,y), (x, y+50), (x-350, y+225), (x-350, y+575), 
                (x, y+750), (x+800, y+750), (x+1150, y+575), (x+1150, y+225),
                (x+800, y+50),(x, y+50)]
        
        # [...]
    
        def move(self, x, y):
            
            x *= self.speed
            y *= self.speed
            polygon = self.createPolygon(self.x + x, self.y + y)
            if not collideRectPolygon(self.rect, polygon):
                self.x += x
                self.y += y
    

    See also Collision and Intersection - Rectangle and polygon


    Minimal example:

    repl.it/@Rabbid76/PyGame-CollisionPolygonRectangle


    Complete example:

    import pygame
    pygame.init()
    W, H=500, 500
    screen = pygame.display.set_mode([500, 500])
    running = True
    
    bcg=(200, 200, 200)
    red=(255, 0 ,0)
    purp=(255, 0, 255)
    wall=(100, 100, 100)
    
    def collideLineLine(l1_p1, l1_p2, l2_p1, l2_p2):
    
        # normalized direction of the lines and start of the lines
        P  = pygame.math.Vector2(*l1_p1)
        line1_vec = pygame.math.Vector2(*l1_p2) - P
        R = line1_vec.normalize()
        Q  = pygame.math.Vector2(*l2_p1)
        line2_vec = pygame.math.Vector2(*l2_p2) - Q
        S = line2_vec.normalize()
    
        # normal vectors to the lines
        RNV = pygame.math.Vector2(R[1], -R[0])
        SNV = pygame.math.Vector2(S[1], -S[0])
        RdotSVN = R.dot(SNV)
        if RdotSVN == 0:
            return False
    
        # distance to the intersection point
        QP  = Q - P
        t = QP.dot(SNV) / RdotSVN
        u = QP.dot(RNV) / RdotSVN
    
        return t > 0 and u > 0 and t*t < line1_vec.magnitude_squared() and u*u < line2_vec.magnitude_squared()
    
    def colideRectLine(rect, p1, p2):
        return (collideLineLine(p1, p2, rect.topleft, rect.bottomleft) or
                collideLineLine(p1, p2, rect.bottomleft, rect.bottomright) or
                collideLineLine(p1, p2, rect.bottomright, rect.topright) or
                collideLineLine(p1, p2, rect.topright, rect.topleft))
    
    def collideRectPolygon(rect, polygon):
        for i in range(len(polygon)-1):
            if colideRectLine(rect, polygon[i], polygon[i+1]):
                return True
        return False
    
    class player:
        def bg(self):        
            screen.fill(bcg)
            self.outer = self.createPolygon(self.x, self.y)
            pygame.draw.polygon(screen, wall, self.outer)
      
        def createPolygon(self, x, y):
            return [
                (x,y), (x+800, y), (x+1200, y+200), (x+1200, y+600), 
                (x+800, y+800), (x, y+800), (x-400, y+600), (x-400, y+200),           
                (x,y), (x, y+50), (x-350, y+225), (x-350, y+575), 
                (x, y+750), (x+800, y+750), (x+1150, y+575), (x+1150, y+225),
                (x+800, y+50),(x, y+50)]
        
        def __init__(self, color, size=20, speed=0.25):
            self.x=0
            self.y=0
            self.col=color
            self.size=size
            self.speed=speed
    
        def draw(self):
            s=self.size
            self.rect=pygame.Rect(W/2-s/2, H/2-s/2, self.size, self.size)
            pygame.draw.rect(screen, self.col, self.rect)
    
        def move(self, x, y):
            
            x *= self.speed
            y *= self.speed
            polygon = self.createPolygon(self.x + x, self.y + y)
            if not collideRectPolygon(self.rect, polygon):
                self.x += x
                self.y += y
                
    p=player(red)
    while running:
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                running = False
    
        p.bg()
    
        keys=pygame.key.get_pressed()
        
        if keys[pygame.K_a]: p.move(1, 0)
        if keys[pygame.K_d]: p.move(-1, 0)
        if keys[pygame.K_w]: p.move(0, 1)
        if keys[pygame.K_s]: p.move(0, -1)
    
        p.draw()
        pygame.display.update()
    
    pygame.quit()