Search code examples
pythonpygamespritecollision

Check for collision and stop velocity in pygame


I am trying to make a simple platformer in pygame, with simulated gravity and collision. I can't make the collision working. On collision with a sprite, the player slowly falls through the sprite and continues falling at normal speed when reached through.

Main.py:

class Game:
    def __init__(self):
        # initialize pygame library
        pg.init()
        pg.mixer.init()

        # initialize screen
        self.screen =  pg.display.set_mode((WIDTH, HEIGHT))
        pg.display.set_caption(TITLE)
        self.clock = pg.time.Clock()
        self.font = pg.font.match_font(FONT_NAME)

        self.running = True
        self.playing = True

    def new(self):
        # initzialize sprite groups
        self.sprites = pg.sprite.Group()
        self.objects = pg.sprite.Group()

        self.p = Player(self)
        self.sprites.add(self.p)

        self.g = Ground()
        self.sprites.add(self.g)
        self.objects.add(self.g)

        self.o = Object(100, 350, 100, 20)
        self.sprites.add(self.o)
        self.objects.add(self.o)

        self.collide = False

        self.run()

    # constant running functions
    def run(self):
        while self.playing:
            self.clock.tick(FPS)
            self.events()
            self.update()
            self.draw()
        self.running = False


    def events(self):
        for event in pg.event.get():
            if event.type == pg.QUIT:
                self.playing = False
            if event.type == pg.KEYDOWN:
                if event.key == pg.K_UP:
                    self.p.jump() 

    def update(self):
        self.sprites.update()

        hits = pg.sprite.spritecollide(self.p, self.objects, False)
        if hits:
            self.collide = True
            if self.p.vel.y >= 0.0:
                self.p.x = hits[0].rect.top
                print("Collide bottom")
            elif self.p.vel.y < 0: self.p.top  = hits[0].rect.bottom
            elif self.p.vel.x > 0: self.p.rect.right = hits[0].rect.left
            elif self.p.vel.x < 0: self.p.rect.left = hits[0].rect.right
            self.p.vel.y = 0
            #self.p.acc.y = 0
            #print(f"Collision with {hits[0].name}")
        else:
            self.collide = False



    def draw(self):
        self.screen.fill(BLACK)
        self.sprites.draw(self.screen)

        self.drawtext(f"X Pos: = {int(self.p.pos.x)}", 15, WHITE, WIDTH - 5, 20, 3)
        self.drawtext(f"Y Pos: = {int(self.p.pos.y)}", 15, WHITE, WIDTH - 5, 40, 3)
        self.drawtext(f"Y Velocity = {self.p.vel.y}", 15, WHITE, 5, 50, 0)
        self.drawtext(f"Y Accelleration = {self.p.acc.y}", 15, WHITE, 5, 70, 0)
        self.drawtext(f"Collision: = {self.collide}", 15, WHITE, 5, 200, 0)
        #print(self.p.vel.y)

        pg.display.flip()

    # other functions
    def drawtext(self, text, size, color, x, y, align):
        font = pg.font.Font(self.font, size)
        text_surface = font.render(text, True, color)
        text_rect = text_surface.get_rect()
        if align == 0:
            text_rect.midleft = (x, y)
        elif align == 1:
            text_rect.midtop = (x, y)
        elif align == 2:
            text_rect.midbottom = (x, y)
        elif align == 3:
            text_rect.midright = (x, y)
        else:
            text_rect.center = (x, y)
        self.screen.blit(text_surface, text_rect)

    # def checkCollisionY(self):
    #     hits = pg.sprite.spritecollide(self.p, self.objects, False)
    #     if hits:
    #         self.collide = True
    #         return True
    #     else:
    #         self.collide = False
    #         return False


g = Game()
while g.running:
    g.new()

pg.quit()

Sprites.py:

from settings import *
import pygame as pg

vec = pg.math.Vector2

class Player(pg.sprite.Sprite):
    def __init__(self, game):
        pg.sprite.Sprite.__init__(self)
        self.game = game
        self.width = 30
        self.height = 30
        self.image = pg.Surface((self.width, self.height))
        self.image.fill(YELLOW)
        self.rect = self.image.get_rect()
        self.rect.center = vec(150, 100)

        self.pos = vec(150, 100)
        self.vel = vec(0, 0)
        self.acc = vec(0, 0)

    def update(self):
        self.acc = vec(0, PLAYER_GRAV)

        #input
        keys = pg.key.get_pressed()
        if keys[pg.K_LEFT]:
            self.acc.x = -PLAYER_ACC
        if keys[pg.K_RIGHT]:
            self.acc.x = PLAYER_ACC



        self.acc.x += self.vel.x * PLAYER_FRICTION

        self.vel += self.acc   
        self.pos += self.vel + 0.5 * self.acc
        print(f"{self.vel.y} + 0.5 * {self.acc.y} = {self.vel.y + 0.5 * self.acc.y}")


        self.rect.topleft = self.pos
    def jump(self):
        hits = pg.sprite.spritecollide(self, self.game.objects, False)
        if hits:
            self.vel.y = -20

class Object(pg.sprite.Sprite):
    def __init__(self, x, y, w, h):
        pg.sprite.Sprite.__init__(self)
        self.image = pg.Surface((w, h))
        self.image.fill((255, 0, 144))
        self.name = "Object"
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y



class Ground(pg.sprite.Sprite):
    def __init__(self):
        pg.sprite.Sprite.__init__(self)
        self.name = "Ground"
        self.image = pg.image.load("ground.png")    
        self.rect = self.image.get_rect()
        self.rect.x = -100
        self.rect.y = 550

Settings.py:

# game settings
TITLE = "My Game"
WIDTH = 480
HEIGHT = 800
FPS = 60
FONT_NAME = 'impact'

#Player properties
PLAYER_ACC = 0.7
PLAYER_FRICTION = -0.12
PLAYER_GRAV = 0.7


# define colors
WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 0, 255)

YELLOW = (255, 255, 0)
CYAN = (0, 255, 255)
PURPLE = (255, 0, 255)

The important code here is the update() in main.py and update() in sprites.py. Help?

EDIT

hits = pg.sprite.spritecollide(self.p, self.objects, False)
        for hit in hits:
            self.collide = True
            if self.p.vel.x > 0.0: 
                self.p.rect.right = hit.rect.left
                self.p.pos.x = self.p.rect.centerx
                self.p.vel.x = 0

            elif self.p.vel.x < 0.0: 
                self.p.rect.left = hit.rect.right
                self.p.pos.x = self.p.rect.centerx

            self.p.vel.x = 0
            self.p.pos.x = self.p.rect.x
        else:
            self.collide = False

        hits = pg.sprite.spritecollide(self.p, self.objects, False)
        for hit in hits:
            self.collide = True
            if self.p.vel.y >= 0.0:
                self.p.rect.bottom = hit.rect.top
                self.p.pos.y = self.p.rect.centery
                self.p.vel.y = 0

            elif self.p.vel.y < 0.0:
                self.p.rect.top = hit.rect.bottom
                self.p.pos.y = self.p.rect.centery
                self.p.vel.y = 0
            self.p.pos.y = self.p.rect.y
        else:
            self.collide = False

Solution

  • Your Player class doesn't have x and y attributes but a pos attribute which you need to change after a collision. The rect of the object needs to be updated as well and it's better to do that first and then set the pos.y coordinate to the rect.centery coordinate afterwards.

    if self.p.vel.y >= 0.0:
        self.p.rect.bottom = hits[0].rect.top
        self.p.pos.y = self.p.rect.centery
        self.p.vel.y = 0
    

    Do the same for the other directions.

    Also, the horizontal and vertical movement should be handled separately, otherwise you'll see odd jumps for example from the side to the top of a platform. Take a look at the first part of this platformer example.


    And in the jump method you need to move the rect down by 1 pixel so that it's able to collide with the platform sprites.

    def jump(self):
        self.rect.y += 1
        # ...
    

    Here's a complete, runnable example:

    import pygame as pg
    
    
    # game settings
    TITLE = "My Game"
    WIDTH = 480
    HEIGHT = 800
    FPS = 60
    FONT_NAME = 'impact'
    
    #Player properties
    PLAYER_ACC = 0.7
    PLAYER_FRICTION = -0.12
    PLAYER_GRAV = 0.7
    
    # define colors
    WHITE = (255, 255, 255)
    BLACK = (0, 0, 0)
    YELLOW = (255, 255, 0)
    
    vec = pg.math.Vector2
    
    
    class Player(pg.sprite.Sprite):
        def __init__(self, game):
            pg.sprite.Sprite.__init__(self)
            self.game = game
            self.image = pg.Surface((30, 30))
            self.image.fill(YELLOW)
            self.rect = self.image.get_rect(center=(150, 100))
    
            self.pos = vec(150, 100)
            self.vel = vec(0, 0)
            self.acc = vec(0, 0)
            self.objects = game.objects
    
        def update(self):
            self.acc = vec(0, PLAYER_GRAV)
    
            #input
            keys = pg.key.get_pressed()
            if keys[pg.K_LEFT]:
                self.acc.x = -PLAYER_ACC
            if keys[pg.K_RIGHT]:
                self.acc.x = PLAYER_ACC
    
            self.acc.x += self.vel.x * PLAYER_FRICTION
            self.vel += self.acc
    
            # Move along the x-axis first.
            self.pos.x += self.vel.x + 0.5 * self.acc.x
            # print(f"{self.vel.y} + 0.5 * {self.acc.y} = {self.vel.y + 0.5 * self.acc.y}")
            self.rect.centerx = self.pos.x
            # Check if the sprite collides with a platform.
            hits = pg.sprite.spritecollide(self, self.objects, False)
            if hits:
                # Reset the x position.
                if self.vel.x > 0:
                    self.rect.right = hits[0].rect.left
                    self.pos.x = self.rect.centerx
                    self.vel.x = 0
                elif self.vel.x < 0:
                    self.rect.left = hits[0].rect.right
                    self.pos.x = self.rect.centerx
                    self.vel.x = 0
    
            # Move along the y-axis.
            self.pos.y += self.vel.y + 0.5 * self.acc.y
            self.rect.centery = self.pos.y
            # Check if the sprite collides with a platform.
            hits = pg.sprite.spritecollide(self, self.objects, False)
            if hits:
                # Reset the y position.
                if self.vel.y >= 0.0:
                    self.rect.bottom = hits[0].rect.top
                    self.pos.y = self.rect.centery
                    self.vel.y = 0
                elif self.vel.y < 0:
                    self.rect.top = hits[0].rect.bottom
                    self.pos.y = self.rect.centery
                    self.vel.y = 0
    
        def jump(self):
            self.rect.y += 1  # Move it down to check if it collides with a platform.
            hits = pg.sprite.spritecollide(self, self.game.objects, False)
            if hits:
                self.vel.y = -20
            self.rect.y -= 1  # Move it up again after the collision check.
    
    
    class Object(pg.sprite.Sprite):
        def __init__(self, x, y, w, h):
            pg.sprite.Sprite.__init__(self)
            self.image = pg.Surface((w, h))
            self.image.fill((255, 0, 144))
            self.name = "Object"
            self.rect = self.image.get_rect()
            self.rect.x = x
            self.rect.y = y
    
    
    class Ground(pg.sprite.Sprite):
        def __init__(self):
            pg.sprite.Sprite.__init__(self)
            self.name = "Ground"
            self.image = pg.Surface((500, 300))
            self.image.fill((90, 30, 30))
            self.rect = self.image.get_rect()
            self.rect.x = -100
            self.rect.y = 550
    
    
    class Game:
        def __init__(self):
            pg.init()
            pg.mixer.init()
            self.screen =  pg.display.set_mode((WIDTH, HEIGHT))
            pg.display.set_caption(TITLE)
            self.clock = pg.time.Clock()
            self.font = pg.font.match_font(FONT_NAME)
    
            self.running = True
            self.playing = True
    
        def new(self):
            self.sprites = pg.sprite.Group()
            self.objects = pg.sprite.Group()
    
            self.p = Player(self)
            self.sprites.add(self.p)
    
            self.g = Ground()
            self.sprites.add(self.g)
            self.objects.add(self.g)
    
            rects = [(100, 350, 100, 20), (50, 380, 100, 20),
                     (200, 450, 100, 100)]
            for x, y, w, h in rects:
                obj = Object(x, y, w, h)
                self.sprites.add(obj)
                self.objects.add(obj)
    
            self.collide = False
    
            self.run()
    
        def run(self):
            while self.playing:
                self.clock.tick(FPS)
                self.events()
                self.update()
                self.draw()
            self.running = False
    
        def events(self):
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    self.playing = False
                if event.type == pg.KEYDOWN:
                    if event.key == pg.K_UP:
                        self.p.jump()
    
        def update(self):
            self.sprites.update()
    
        def draw(self):
            self.screen.fill(BLACK)
            self.sprites.draw(self.screen)
            pg.display.flip()
    
    
    g = Game()
    while g.running:
        g.new()
    
    pg.quit()