Collision detection doesn't work in pygame

Question:

I am trying to make a top down game and I need functional walls for it to work, i have tried it multiple times but when i hit two directions at once the player phases through the wall. the system i have now is supposed to cancel out any velocity the player has by subtracting/adding it to the current (x, y) coordinates but it doesn’t work, i have also tried making it so there is supposed to be no reaction when you press the button but that breaks the code too.

import pygame
import random
#sets up the screen
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("collision test")

#defines the size and coords for the player
wall_height, wall_width = 200, 20
wall_x, wall_y = 400, 300

last_key = 0
x = 400
y = 300
width = 40
height = 60
vel = 5

#where the main code is run
run = True
while run:
    pygame.time.delay(50)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
    #movement code
    keys = pygame.key.get_pressed()


    
    if keys[pygame.K_LEFT] and x > vel:
        last_key = "left"
        x -= vel
    if keys[pygame.K_RIGHT] and x < 800 - width:
        last_key = "right"
        x += vel
    if keys[pygame.K_UP] and y > vel:
        last_key = "up"
        y -= vel
    if keys[pygame.K_DOWN] and y < 600 - height - vel:
        last_key = "down"
        y += vel
    
    #draws the player
    screen.fill((0,0,0))
    wall = pygame.draw.rect(screen, (244, 247, 30), (wall_x, wall_y, wall_width, wall_height))
    player = pygame.draw.rect(screen, (255, 255, 255), (x, y, width, height))

    #collision code
    if player.colliderect(wall) and last_key == "left":
        x += vel
    elif player.colliderect(wall) and last_key == "right":
        x -= vel
    elif player.colliderect(wall) and last_key == "up":
        y += vel
    elif player.colliderect(wall) and last_key == "down":
        y -= vel
        
        
    pygame.display.update()

pygame.quit()
Asked By: hashdankhog

||

Answers:

so you are basically throwing velocity in the opposite direction if the player collides with the wall which isnt a bad thought

I would recommend either putting a rect (rectangle) around the wall and if you collide with that rect then your velocity is == 0

alternatively you can import math and through a math equation for finding distance between two object like, then when you are within a certain range you set vel ==0

distance = math.sqrt((math.pow(x - wall_x, 2)) + (math.pow(y - wally, 2)))
    if distance < 20:
        vel = 0

naturally the distance of 20 was just a guess that could be tweaked

Answered By: Matthew Swaney

Here is a solution, it uses built in collision detection methods and it is easier to "create/build" the world, just change sth in the matrix (explanation is in code comments (without comments this is around only 80 lines of code)):

# some useless comments too in the sense that they explain what code does
# import pygame
import pygame

# initialise pygame, set display and clock
pygame.init()
screen = pygame.display.set_mode((600, 600))
clock = pygame.time.Clock()

# constant for tile size, this size is because screen is 600x600 pixels,
# there are 12 tiles per column per row so each tile gets displayed
TILE = 50

# world matrix, this is where you create the world layout of tiles,
# if you want to have thinner walls, increase the row and grid count
# and decrease the tile size
world = [
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 1],
    [1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 1, 0, 0, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 1, 1, 1, 1, 0, 1],
    [1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1],
    [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1],
]

# tile list which will contain all the valid tiles (rectangles)
# you could append also the number and then change the number
# in the world matrix to apply some different textures
tile_list = []
for row_num, row in enumerate(world):
    for col_num, col in enumerate(row):
        # if the current tile is 1 in the matrix then calculate
        # its position and append the rectangular area to the tile_list
        if col == 1:
            x, y = col_num * TILE, row_num * TILE
            rect = pygame.Rect(x, y, TILE, TILE)
            tile_list.append(rect)

# player stuff, like player's rectangle to allow for
# easier collision detection
player_rect = pygame.Rect(60, 60, 30, 30)
vel = 3
fps = 60

while True:
    clock.tick(fps)
    screen.fill((0, 0, 0))
    
    # get pressed keys
    keys = pygame.key.get_pressed()
    # delta x and delta y values that allow to move
    # player in either direction, they are like
    # a helper variable since
    # they don't directly affect player movement thus
    # allowing for additional checks like collision detection
    dx, dy = 0, 0
    # the basic key stuff, the reason to reduce instead of set the 
    # value is in case both keys are pressed so that the player stays in place
    # basically makes both keys equivalent
    if keys[pygame.K_UP]:
        dy -= vel
    if keys[pygame.K_DOWN]:
        dy += vel
    if keys[pygame.K_RIGHT]:
        dx += vel
    if keys[pygame.K_LEFT]:
        dx -= vel
    
    # this is where collision detection starts
    # first I create two projections of where the player might be
    # depending on what keys they pressed, `.move()` returns a new
    # rectangle allowing to easily apply `.colliderect()  method
    x_projection = player_rect.move(dx, 0)
    y_projection = player_rect.move(0, dy)
    # now for each placed tile check against collision
    for tile in tile_list:
        # also draw the rectangle here too
        pygame.draw.rect(screen, (255, 255, 0), tile)
        # can add the border
        # pygame.draw.rect(screen, (255, 255, 255), tile, width=1)
        
        # `pygame.Rect` has a built-in method for checking the collision
        if x_projection.colliderect(tile):
            # depending on player movement calculate delta x
            # so that the player can touch the wall and there
            # wouldn't be any gaps between player and wall
            # the same logic for checking
            # y collision
            if dx > 0:
                dx = tile.left - player_rect.right
            elif dx < 0:
                dx = tile.right - player_rect.left

        elif y_projection.colliderect(tile):
            if dy > 0:
                dy = tile.top - player_rect.bottom
            elif dy < 0:
                dy = tile.bottom - player_rect.top
    
    # move the player's rectangle in place meaning
    # it moves the actual rectangle instead of returning
    # an new one as `.move` would do
    player_rect.move_ip(dx, dy)
    # draw the player cube on screen
    pygame.draw.rect(screen, (255, 0, 0), player_rect)
    # can also draw border
    # pygame.draw.rect(screen, (0, 0, 255), player_rect, width=1)

    # event checking
    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            exit()
    # remember to update the display
    pygame.display.update()

Sources:

If you have any questions, be sure to ask them!

Answered By: Matiiss

I’ll give you an complete answer with minimal changes to your code.

Change the order. Do the collision detection before the drawing. Create pygame.Rect objects:

wall = pygame.Rect(wall_x, wall_y, wall_width, wall_height)
player = pygame.Rect(x, y, width, height)

Restrict the position of the player to the boundaries of the wall:

if player.colliderect(wall) and last_key == "left":
    x += vel
    x = wall.right
elif player.colliderect(wall) and last_key == "right":
    x -= vel
    x = wall.left - width
elif player.colliderect(wall) and last_key == "up":
    y += vel
    y = wall.bottom
elif player.colliderect(wall) and last_key == "down":
    y -= vel
    y = wall.top - height

Complete example:

import pygame

#sets up the screen
pygame.init()
screen = pygame.display.set_mode((800, 600))
pygame.display.set_caption("collision test")

#defines the size and coords for the player
wall_height, wall_width = 200, 20
wall_x, wall_y = 400, 300

last_key = 0
x = 400
y = 240
width = 40
height = 60
vel = 5

#where the main code is run
run = True
while run:
    pygame.time.delay(50)

    for event in pygame.event.get():
        if event.type == pygame.QUIT:
            run = False
    #movement code
    keys = pygame.key.get_pressed() 
    if keys[pygame.K_LEFT] and x > vel:
        last_key = "left"
        x -= vel
    if keys[pygame.K_RIGHT] and x < 800 - width:
        last_key = "right"
        x += vel
    if keys[pygame.K_UP] and y > vel:
        last_key = "up"
        y -= vel
    if keys[pygame.K_DOWN] and y < 600 - height - vel:
        last_key = "down"
        y += vel

    wall = pygame.Rect(wall_x, wall_y, wall_width, wall_height)
    player = pygame.Rect(x, y, width, height)

    #collision code
    if player.colliderect(wall) and last_key == "left":
        x += vel
        x = wall.right
    elif player.colliderect(wall) and last_key == "right":
        x -= vel
        x = wall.left - width
    elif player.colliderect(wall) and last_key == "up":
        y += vel
        y = wall.bottom
    elif player.colliderect(wall) and last_key == "down":
        y -= vel
        y = wall.top - height

    #draws the player
    screen.fill((0,0,0))
    pygame.draw.rect(screen, (244, 247, 30), (wall_x, wall_y, wall_width, wall_height))
    pygame.draw.rect(screen, (255, 255, 255), (x, y, width, height))    
    pygame.display.update()

pygame.quit()
Answered By: Rabbid76
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.