Search code examples
pythonpygamecollision-detection

Add collision detection to a plattformer in pygame


I'm working on a small platformer game in which you place blocks to make a level, then play it.

I got gravity, jumping, and left and right movement.. but I am not sure how to make the player collide with walls when moving left or right.

The way I want it to work is like this-

if key[K_LEFT]:

if not block to the left:

move to the left

How would I go about doing this (relative to this source):

import pygame,random
from pygame.locals import *
import itertools
pygame.init()
screen=pygame.display.set_mode((640,480))
class Block(object):
    sprite = pygame.image.load("texture\\dirt.png").convert_alpha()
    def __init__(self, x, y):
        self.rect = self.sprite.get_rect(centery=y, centerx=x)

class Player(object):
    sprite = pygame.image.load("texture\\playr.png").convert()
    sprite.set_colorkey((0,255,0))
    def __init__(self, x, y):
        self.rect = self.sprite.get_rect(centery=y, centerx=x)

blocklist = []
player = []
colliding = False

while True:
    screen.fill((25,30,90))
    mse = pygame.mouse.get_pos()
    key=pygame.key.get_pressed()

    if key[K_LEFT]:
        p.rect.left-=1
    if key[K_RIGHT]:
        p.rect.left+=1
    if key[K_UP]:
        p.rect.top-=10

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

        if key[K_LSHIFT]:
            if event.type==MOUSEMOTION:
                if not any(block.rect.collidepoint(mse) for block in blocklist):
                    x=(int(mse[0]) / 32)*32
                    y=(int(mse[1]) / 32)*32
                    blocklist.append(Block(x+16,y+16))
        else:
            if event.type == pygame.MOUSEBUTTONUP:
                if event.button == 1:
                    to_remove = [b for b in blocklist if b.rect.collidepoint(mse)]
                    for b in to_remove:
                        blocklist.remove(b)

                    if not to_remove:
                        x=(int(mse[0]) / 32)*32
                        y=(int(mse[1]) / 32)*32
                        blocklist.append(Block(x+16,y+16))

                elif event.button == 3:
                    x=(int(mse[0]) / 32)*32
                    y=(int(mse[1]) / 32)*32
                    player=[]
                    player.append(Player(x+16,y+16))

    for b in blocklist:
        screen.blit(b.sprite, b.rect)
    for p in player:
        if any(p.rect.colliderect(block) for block in blocklist):
            #collide
            pass
        else:
            p.rect.top += 1
        screen.blit(p.sprite, p.rect)
    pygame.display.flip()

Solution

  • A common approch is to seperate the horizontal and vertical collision handling into two seperate steps.

    If you do this and also track the velocity of your player, it's easy to know on which side a collision happened.

    First of all, let's give the player some attributes to keep track of his velocity:

    class Player(object):
        ...
        def __init__(self, x, y):
            self.rect = self.sprite.get_rect(centery=y, centerx=x)
            # indicates that we are standing on the ground
            # and thus are "allowed" to jump
            self.on_ground = True 
            self.xvel = 0
            self.yvel = 0
            self.jump_speed = 10
            self.move_speed = 8
    

    Now we need a method to actually check for a collision. As already said, to make things easy, we use our xvel and yvel to know if we collided with our left or right side etc. This goes into the Player class:

    def collide(self, xvel, yvel, blocks):
        # all blocks that we collide with
        for block in [blocks[i] for i in self.rect.collidelistall(blocks)]:
    
            # if xvel is > 0, we know our right side bumped 
            # into the left side of a block etc.
            if xvel > 0: self.rect.right = block.rect.left
            if xvel < 0: self.rect.left = block.rect.right
    
            # if yvel > 0, we are falling, so if a collision happpens 
            # we know we hit the ground (remember, we seperated checking for
            # horizontal and vertical collision, so if yvel != 0, xvel is 0)
            if yvel > 0:
                self.rect.bottom = block.rect.top
                self.on_ground = True
                self.yvel = 0
            # if yvel < 0 and a collision occurs, we bumped our head
            # on a block above us
            if yvel < 0: self.rect.top = block.rect.bottom
    

    Next, we move our movement handling to the Player class. So let's create on object that keeps track of the input. Here, I use a namedtuple, because why not.

    from collections import namedtuple
    ...
    max_gravity = 100
    Move = namedtuple('Move', ['up', 'left', 'right'])
    while True:
        screen.fill((25,30,90))
        mse = pygame.mouse.get_pos()
        key = pygame.key.get_pressed()
    
        for event in pygame.event.get():
           ...
    
        move = Move(key[K_UP], key[K_LEFT], key[K_RIGHT])
        for p in player:
            p.update(move, blocklist)
            screen.blit(p.sprite, p.rect)
    

    We pass the blocklist to the update method of the Player so we can check for collision. Using the move object, we now know where the player should move, so let's implement Player.update:

    def update(self, move, blocks):
    
        # check if we can jump 
        if move.up and self.on_ground: 
            self.yvel -= self.jump_speed
    
        # simple left/right movement
        if move.left: self.xvel = -self.move_speed
        if move.right: self.xvel = self.move_speed
    
        # if in the air, fall down
        if not self.on_ground:
            self.yvel += 0.3
            # but not too fast
            if self.yvel > max_gravity: self.yvel = max_gravity
    
        # if no left/right movement, x speed is 0, of course
        if not (move.left or move.right):
            self.xvel = 0
    
        # move horizontal, and check for horizontal collisions
        self.rect.left += self.xvel
        self.collide(self.xvel, 0, blocks)
    
        # move vertically, and check for vertical collisions
        self.rect.top += self.yvel
        self.on_ground = False;
        self.collide(0, self.yvel, blocks)
    

    The only thing left is to use a Clock to limit the framerate to let the game run at a constant speed. That's it.

    Here's the complete code:

    import pygame,random
    from pygame.locals import *
    from collections import namedtuple
    
    pygame.init()
    clock=pygame.time.Clock()
    screen=pygame.display.set_mode((640,480))
    
    max_gravity = 100
    
    class Block(object):
        sprite = pygame.image.load("dirt.png").convert_alpha()
        def __init__(self, x, y):
            self.rect = self.sprite.get_rect(centery=y, centerx=x)
    
    class Player(object):
        sprite = pygame.image.load("dirt.png").convert()
        sprite.set_colorkey((0,255,0))
        def __init__(self, x, y):
            self.rect = self.sprite.get_rect(centery=y, centerx=x)
            # indicates that we are standing on the ground
            # and thus are "allowed" to jump
            self.on_ground = True
            self.xvel = 0
            self.yvel = 0
            self.jump_speed = 10
            self.move_speed = 8
    
        def update(self, move, blocks):
    
            # check if we can jump 
            if move.up and self.on_ground: 
                self.yvel -= self.jump_speed
    
            # simple left/right movement
            if move.left: self.xvel = -self.move_speed
            if move.right: self.xvel = self.move_speed
    
            # if in the air, fall down
            if not self.on_ground:
                self.yvel += 0.3
                # but not too fast
                if self.yvel > max_gravity: self.yvel = max_gravity
    
            # if no left/right movement, x speed is 0, of course
            if not (move.left or move.right):
                self.xvel = 0
    
            # move horizontal, and check for horizontal collisions
            self.rect.left += self.xvel
            self.collide(self.xvel, 0, blocks)
    
            # move vertically, and check for vertical collisions
            self.rect.top += self.yvel
            self.on_ground = False;
            self.collide(0, self.yvel, blocks)
    
        def collide(self, xvel, yvel, blocks):
            # all blocks that we collide with
            for block in [blocks[i] for i in self.rect.collidelistall(blocks)]:
    
                # if xvel is > 0, we know our right side bumped 
                # into the left side of a block etc.
                if xvel > 0: self.rect.right = block.rect.left
                if xvel < 0: self.rect.left = block.rect.right
    
                # if yvel > 0, we are falling, so if a collision happpens 
                # we know we hit the ground (remember, we seperated checking for
                # horizontal and vertical collision, so if yvel != 0, xvel is 0)
                if yvel > 0:
                    self.rect.bottom = block.rect.top
                    self.on_ground = True
                    self.yvel = 0
                # if yvel < 0 and a collision occurs, we bumped our head
                # on a block above us
                if yvel < 0: self.rect.top = block.rect.bottom
    
    blocklist = []
    player = []
    colliding = False
    Move = namedtuple('Move', ['up', 'left', 'right'])
    while True:
        screen.fill((25,30,90))
        mse = pygame.mouse.get_pos()
        key = pygame.key.get_pressed()
    
        for event in pygame.event.get():
            if event.type == QUIT: exit()
    
            if key[K_LSHIFT]:
                if event.type==MOUSEMOTION:
                    if not any(block.rect.collidepoint(mse) for block in blocklist):
                        x=(int(mse[0]) / 32)*32
                        y=(int(mse[1]) / 32)*32
                        blocklist.append(Block(x+16,y+16))
            else:
                if event.type == pygame.MOUSEBUTTONUP:
                    if event.button == 1:
                        to_remove = [b for b in blocklist if b.rect.collidepoint(mse)]
                        for b in to_remove:
                            blocklist.remove(b)
    
                        if not to_remove:
                            x=(int(mse[0]) / 32)*32
                            y=(int(mse[1]) / 32)*32
                            blocklist.append(Block(x+16,y+16))
    
                    elif event.button == 3:
                        x=(int(mse[0]) / 32)*32
                        y=(int(mse[1]) / 32)*32
                        player=[]
                        player.append(Player(x+16,y+16))
    
        move = Move(key[K_UP], key[K_LEFT], key[K_RIGHT])
    
        for b in blocklist:
            screen.blit(b.sprite, b.rect)
        for p in player:
            p.update(move, blocklist)
            screen.blit(p.sprite, p.rect)
        clock.tick(60)
        pygame.display.flip()
    

    Note that I changed the image names so I just need a single image file for testing this. Also, I don't know why you keep the player in a list, but here's a nice animation of our game in action:

    enter image description here