PYGAME: Collision is not working properly. The player and platform are not behaving well. Help, newbie to python here

Viewed 208

this is my first question in stackoverflow. I started learning python 3 months ago, and I'm building a platformer game using sprites. I had to isolate the problem, so I'm only posting the part where the issue is.

Whenever the square is landing on any platform, there is a detection of collision but the square actually goes through the platform approximately 10 pixels down (DURING the collision) and then it goes back up and it stabilizes on top of the platform.

You can SEE the effect better if you adjust the FPS to 2. How can I fix this? I remember building a previous game BUT without using acceleration in "x" axis (friction) and "y" axis (gravity), just fast&stop movements of the square (left/right/up/down) detecting collision and the square never went through the platfom...

Here is the isolated problem:

import pygame as pg
import random

WIDTH = 900
HEIGHT = 600
NAME = "square"
FPS = 60

green = (0, 150, 0)
white = (255, 255, 255)
black = (0, 0, 0)

class Platform(pg.sprite.Sprite):
    def __init__(self, x, y, width, height, color):
        pg.sprite.Sprite.__init__(self)
        self.image = pg.Surface((width, height))
        self.image.fill(color)
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

class Player(pg.sprite.Sprite):
    def __init__(self):
        pg.sprite.Sprite.__init__(self)
        self.image = pg.Surface((40, 40))
        self.image.fill(black)
        self.rect = self.image.get_rect()
        self.rect.center = (WIDTH / 2, HEIGHT / 2)
        self.posx = WIDTH / 2
        self.posy = HEIGHT * 0.10
        self.velx = 0
        self.vely = 0
        self.accx = 0
        self.accy = 0
        self.jumping = False
        self.jump_go = False

    def jump(self):
        if self.jumping == False:
            self.jumping = True
            self.vely = -25

    def update(self):

        self.accx = 0    #FRICTION
        self.accy = 1    #GRAVITY

        keys = pg.key.get_pressed()

        if keys[pg.K_RIGHT]:
            self.accx = 1
        if keys[pg.K_LEFT]:
            self.accx = -1

        if self.vely != 0:
            self.jump_go = False

        #MOVEMENT IN X:

        self.accx = self.accx + (self.velx * (-0.10))
        self.velx = self.velx + self.accx
        self.posx = self.posx + self.velx
        self.rect.x = self.posx

        if abs(self.velx) < 0.5:
            self.velx = 0

        #MOVEMENT IN Y:

        self.accy = self.accy + (self.vely * (-0.05))
        self.vely = self.vely + self.accy
        self.posy = self.posy + self.vely
        self.rect.y = self.posy

        #IF PLAYER FALLS TO EDGE OF SCREEN:

        if self.posy > HEIGHT:
            self.posy = 0
        if self.posx < 0:
            self.posx = WIDTH
        if self.posx > WIDTH:
            self.posx = 0


class Game:
    def __init__(self):
        pg.init()
        pg.mixer.init()
        pg.font.init()
        self.screen = pg.display.set_mode((WIDTH, HEIGHT))
        self.clock = pg.time.Clock()
        self.running = True

    def new(self):
        self.all_sprites = pg.sprite.Group()
        self.all_platforms = pg.sprite.Group()

        for i in range(3):
            self.platform = Platform(random.randint(0, WIDTH), random.randint(HEIGHT * .15,
                                     HEIGHT - 25), random.randint(100, 300), 50, green)
            self.all_platforms.add(self.platform)
            self.all_sprites.add(self.platform)

        self.player = Player()
        self.all_sprites.add(self.player)
        self.run()

    def run(self):
        self.playing = True
        while self.playing:
            self.clock.tick(FPS)
            self.eventos()
            self.update()
            self.draw()

    def eventos(self):
        for event in pg.event.get():
            if event.type == pg.QUIT:
                if self.playing:
                    self.playing = False
                self.running = False
            if event.type == pg.KEYDOWN:
                if self.player.jump_go:
                    if event.key == pg.K_a:
                        self.player.jump()

    def update(self):
        self.all_sprites.update()

        # Check if player hit platfom:
        if self.player.vely > 0:
            hits = pg.sprite.spritecollide(self.player, self.all_platforms, False)
            if hits:
                for hit in hits:
                    if self.player.rect.top < hit.rect.top:
                        self.player.posy = hit.rect.top - self.player.rect.height
                        self.player.vely = 0
                        self.player.jumping = False
                        self.player.jump_go = True

    def draw(self):
        self.screen.fill(white)
        self.all_sprites.draw(self.screen)
        pg.display.flip()

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

pg.quit()
3 Answers

In your collision check you get all hits, then you check for each hit

if self.player.rect.top < hit.rect.top:

this is too late.

If your player travels less then its height per frame its bottom will penetrate the hit.rect.top, then be checked, but its top is still above it so nothing happens.

Next frame it travels further, (still not quite fullfilling if self.player.rect.top < hit.rect.top:) and goes a bit further through your hit.

Some frames after it finally fullfills if self.player.rect.top < hit.rect.top: and its position is corrected by

                if self.player.rect.top < hit.rect.top:
                    self.player.posy = hit.rect.top - self.player.rect.height
                    self.player.vely = 0
                    self.player.jumping = False
                    self.player.jump_go = True

leading to the described "pass through and jump up" effect you see.

You can mitigate the effect by changing

if self.player.rect.top < hit.rect.top:

to when the self.player.rect.bottom passes hit.rect.top - if bottom does not exist, use

    if (self.player.rect.top - self.player.rect.height) < hit.rect.top:

to lessen the "pass through" effect.

I think you should move your collision detection to the Player.update() and Player.jump() functions.

The reason behind this is the complexity of collision detection. Each update() your player is moving some amount of pixels in two dimensions. But let's consider just a single dimension to make it simpler...

Say your player is moving 5 pixels "right" (+x) and an obstacle is 3 pixels away. Obviously it will collide with an overlap of 2 pixels. So what should happen in this case? That depends on your game logic, but in most cases the player should stop an only +3 pixels, resting next to the obstacle. This is a simple calculation knowing the direction of movement, and sizes of the sprites. The logic can determine: Want to move 5 right; Can only move 3 -> Move 3.

In your existing game code, the player is already moved before you check the collisions. You don't know where the player moved from, just that it now overlaps. This makes it more difficult to correct. You have to correct the collision after the fact, not before/during.

your input really helped me out in finding a soultion, this is what I did to fix my issue:

  1. I created a new variable (instance attribute) in my "Player" class:

     self.bottom_quart = self.rect.y + (self.rect.height/2)
    
  2. Then, in the "Game" class, inside the "def update(self):" method, this is what it looks like now:

     def update(self):
         self.all_sprites.update()
    
         #Check if player hit platfom:
         if self.player.vely > 0:
             hits = pg.sprite.spritecollide(self.player, self.all_platforms, False)
             if hits:
                 for hit in hits:
    
                     if self.player.rect.bottom > hit.rect.top > self.player.rect.top 
                        and self.player.bottom_quart < hit.rect.top:
    
                             self.player.rect.bottom = hit.rect.top
                             self.player.posy = hit.rect.top - self.player.rect.height
                             self.player.vely = 0
                             self.player.jumping = False
                             self.player.jump_go = True
    

So basically this fixed 2 things:

  1. BEOFRE, when the player object was landing on a platform, it would "go thru" the platform and then placed itself on top of the platform. NOW, when the player lands on the platform, it DOESNT go thru, and sets itself smoothly on top of the platform. This was fixed by setting:

      self.player.rect.bottom = hit.rect.top
      self.player.posy = hit.rect.top - self.player.rect.height
    
  2. The second issue that got fixed was that when the player hit a platform from the side, it would snap really fast back on top of the platform, which is not a normal behavior. That got fixed by creating the instance attribute:

       self.bottom_quart = self.rect.y + (self.rect.height/2)
    

inside the "Player" class. That works as a new boundary that need to be True in the if statement deciding whether or not it sits on top of the platform or just passes thru when hit from the sides.

Thanks for your help!

Related