SundyAgo
SundyAgo

Reputation: 73

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()

Upvotes: 6

Views: 4028

Answers (2)

Luka Cronqvist
Luka Cronqvist

Reputation: 1

Alternatively define the rect with 4 lines and detect if you are above or bellow those lines, and in the 2 frames where the rect is perfectly aligned you just use normal rect collision.

Upvotes: 0

Rabbid76
Rabbid76

Reputation: 211277

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()

Upvotes: 8

Related Questions