Search code examples
pythonpygamegame-physicscollision

pygame inaccurate collision detection and movement


I have the beginnings of a game developed in pygame with python3, following tutorials on www.teachyourselfpython.com There is a main.py, player.py and walls.py (with player and walls class respectively). The player class contains code for collision detection and movement. Unfortunately, the player does avoid the tile, but on collision, moves to a position on the right hand side of the screen, instead of expected movement (just stopping). Is anyone able to help with the logic of this and correcting the erroneous/undesirable movement on collision. Below, are the three files: main.py, player.py and walls.py. MAIN.PY

#main.py
import pygame
import random
from player import Player
from collectable import Collectable
from walls import Wall

pygame.init()
BLACK=(0,0,0) 
WHITE=(255,255,255) 
RED=(255,0,0) 
GREEN =(0,255,0)

BLUE=(0,0,255) 
GOLD=(255,215,0)
WIDTH=500
HEIGHT=500
size= (WIDTH,HEIGHT) 
screen=pygame.display.set_mode(size)
pygame.display.set_caption("The Life Game")


done = False
clock=pygame.time.Clock()
wall_list=pygame.sprite.Group()
all_sprites = pygame.sprite.Group()
enemy_list  = pygame.sprite.Group() 
player=Player()
player.walls=wall_list

all_sprites.add(player)


for i in range(random.randrange(100,200)):
        whiteStar = Collectable(WHITE, 3, 3, "White Star", "Rect")
        whiteStar.rect.x = random.randrange(size[0])
        whiteStar.rect.y = random.randrange(size[1])
        all_sprites.add(whiteStar)

for i in range(50):

    enemy = Collectable(RED,6, 6,"Enemy","Ellipse")
    enemy.rect.x = random.randrange(300)
    enemy.rect.y = random.randrange(300)
    enemy_list.add(enemy)
    all_sprites.add(enemy)

coin1 = Collectable(GOLD,50,50,"Coin","Ellipse") 
coin1.rect.x=440
coin1.rect.y=0
all_sprites.add(coin1)

coin2 = Collectable(GOLD,50,50,"Coin","Ellipse")
coin2.rect.x=0
coin2.rect.y=440
all_sprites.add(coin2)

enemy = Collectable(RED,100,100,"Enemy","Ellipse")
enemy.rect.x=70
enemy.rect.y=230
all_sprites.add(enemy)

#Make the walls (x_pos,y_pos, width, height,colour)



wall=Wall(0,0,10,600,GREEN)
wall_list.add(wall)
all_sprites.add(wall_list)

wall = Wall(50, 300, 400, 10,RED)
wall_list.add(wall)
all_sprites.add(wall_list)

wall = Wall(10, 200, 100, 10,BLUE)
wall_list.add(wall)
all_sprites.add(wall_list)


score=0
health=100

#- - - - - - - - - - - - - -Main Program Loop - - - - - - - - - - - - - - - -
def main():
        done=False
        score=0
        health=100
        while not done:


                  #- - - - - - Main event loop (this is where code for handling keyboard and mouse clicks will go)
        #Loop until the user clicks the 'x' button (to close program)
                  for event in pygame.event.get(): #User does something
                            if event.type == pygame.QUIT: #If the user clicked close
                                      done = True  #set the done flag to 'true' to exit the loop

                  keys = pygame.key.get_pressed() #checking pressed keys
                  if keys[pygame.K_LEFT]:
                     player.moveLeft(5)
                  if keys[pygame.K_RIGHT]:
                     player.moveRight(5)
                  if keys[pygame.K_UP]:
                     player.moveUp(5)
                  if keys[pygame.K_DOWN]:
                     player.moveDown(5)                


                 #>>----------DRAW SECTION -----------------------------------
                  #Clear the screen to BLACK. Any drawing commands should be put BELOW this or they will be reased with this command
                  screen.fill(BLACK)



                  #Select the font to be used (size, bold, italics, etc)
                  font_score = pygame.font.SysFont('Calibri',20,True,False)
                  font_health = pygame.font.SysFont('Calibri',20,True,False)
                #Printing a variable (score or health) to the screen involves converting the score (if integer) to a string first.score_label = font_score.render("Score: " + str(score),True,BLACK)
                  health_label = font_health.render("Health: "+str(health),True,WHITE)
                  score_label = font_score.render("Score: " + str(score),True, WHITE)
                #Now we can use this line of code to put the image of the text on the screen at a given position
                  screen.blit(score_label,[100,480])
                  screen.blit(health_label,[190,480])


                   #>>---------UPDATE SECTION / Put the logic of your game here (i.e. how objects move, when to fire them, etc)


                  all_sprites.update()

                  if coin1.collision_with(player):
                         score=score+1
                         coin1.kill()
                         coin1.rect.x=-20
                         coin1.rect.y=-330

                  if coin2.collision_with(player):
                         score=score+1
                         coin2.kill()
                         coin2.rect.x=-20
                         coin2.rect.y=-330

                  if enemy.collision_with(player):
                        health=health-25
                        enemy.kill()
                        enemy.rect.x=-20
                        enemy.rect.y=-330

                  enemy.update()





         #-------------PRINTING VARIABLES LIKE SCORE TO SCREEN
                  #Any drawing/graphics code should go here
                  all_sprites.draw(screen)

                  #Update the screen to show whatever you have drawn
                  pygame.display.flip()

                  #Set the frames per second (e.g. 30, 60 etc)
                  clock.tick(120)

main()

PLAYER.PY

import pygame
import random
from walls import Wall

class Player(pygame.sprite.Sprite): 
    #-------------------Define Variables here
    speed=0
    #------------------Initialise Constructor
    def __init__(self): 
        pygame.sprite.Sprite.__init__(self)
        self.image=pygame.image.load("player.png") 
        self.rect = self.image.get_rect()

        #SET THE INITIAL SPEED TO ZERO
        self.change_x = 0
        self.change_y = 0

        #--------------Fetch the rectangle object that has the dimensions of the image
        self.rect =self.image.get_rect()
        #---------------Define movement
    def moveRight(self,pixels):
        self.rect.x+=pixels
    def moveLeft(self,pixels):
        self.rect.x-=pixels
    def moveUp(self,pixels):
        self.rect.y-=pixels
    def moveDown(self,pixels):
        self.rect.y+=pixels

    # Make our top-left corner the passed-in location.
    def settopleft():
        self.rect = self.image.get_rect()
        self.rect.y = y
        self.rect.x = x

        # Set speed vector
        self.change_x = 0
        self.change_y = 0
        self.walls = None

    def changespeed(self, x, y):
        """ Change the speed of the player. """
        self.change_x += x
        self.change_y += y


    def update(self):
        # Did this update cause us to hit a wall?
        block_hit_list = pygame.sprite.spritecollide(self, self.walls, False)
        for block in block_hit_list:
            # If we are moving right, set our right side to the left side of
            # the item we hit
            if self.change_x > 0:
                self.rect.right = block.rect.left
            else:
                # Otherwise if we are moving left, do the opposite.
                self.rect.left = block.rect.right

        # Move up/down
                self.rect.y += self.change_y

        # Check and see if we hit anything
        block_hit_list = pygame.sprite.spritecollide(self, self.walls, False)
        for block in block_hit_list:

            # Reset our position based on the top/bottom of the object.
            if self.change_y > 0:
                self.rect.top = block.rect.top
            else:
                self.rect.top = block.rect.bottom

WALLS.PY

import pygame

class Wall(pygame.sprite.Sprite):
    #Wall a player can run into
    def __init__(self, x, y, width, height,colour):
        #Constructor fo rthe wall that the player can run into
        #call the parent's constructor
        super().__init__()

        #Make a green wall, of the size specified in paramenters
        self.image=pygame.Surface([width,height])
        self.image.fill(colour)

        #Make the "passed-in" location ,the top left corner
        self.rect=self.image.get_rect()
        self.rect.y=y
        self.rect.x=x

Attached, area also images for player and bg: playerbackground image


Solution

  • Okay, let's talk about the movement problem first. The code in the Players update method should move the player along the x-axis first, check if he collides with a wall and if he collides, set his position to the block edge. Afterwards you do the same with the y-axis. It has to be done in this way, because we wouldn't know the direction of the player otherwise and couldn't reset his position to the correct edge. You seem to have changed the code so that the self.change_x and change_y attributes aren't used anymore and move the player with pygame.key.get_pressed instead. I'd delete the pygame.key.get_pressed block and do the movement changes in the event loop. A few things in the update method had to be fixed as well, e.g.:

    if self.change_y > 0:
        self.rect.bottom = block.rect.top
    

    For the collision detection, you can use pygame.sprite.spritecollide and pass the player and the sprite group that you want to check. Then iterate over the returned list and do something for every collided sprite.

    Here's your updated code:

    import sys
    import random
    import pygame as pg
    
    
    pg.init()
    
    
    class Wall(pg.sprite.Sprite):
        """Wall a player can run into."""
        def __init__(self, x, y, width, height, colour):
            super().__init__()
            self.image = pg.Surface([width, height])
            self.image.fill(colour)
            # Make the "passed-in" location the top left corner.
            self.rect = self.image.get_rect(topleft=(x, y))
    
    
    class Collectable(pg.sprite.Sprite):
        """A collectable item."""
    
        def __init__(self, colour, x, y, image, rect):
            super().__init__()
            self.image = pg.Surface((5, 5))
            self.image.fill(colour)
            self.rect = self.image.get_rect(topleft=(x, y))
    
    
    class Player(pg.sprite.Sprite):
    
        def __init__(self):
            pg.sprite.Sprite.__init__(self)
            self.image = pg.Surface((30, 30))
            self.image.fill((50, 150, 250))
            self.rect = self.image.get_rect()
            #SET THE INITIAL SPEED TO ZERO
            self.change_x = 0
            self.change_y = 0
            self.health = 100
    
        def update(self):
            # Move left/right.
            self.rect.x += self.change_x
            # Did this update cause us to hit a wall?
            block_hit_list = pg.sprite.spritecollide(self, self.walls, False)
            for block in block_hit_list:
                # If we are moving right, set our right side to the left side of
                # the item we hit
                if self.change_x > 0:
                    self.rect.right = block.rect.left
                else:
                    # Otherwise if we are moving left, do the opposite.
                    self.rect.left = block.rect.right
    
            # Move up/down.
            self.rect.y += self.change_y
            # Check and see if we hit anything.
            block_hit_list = pg.sprite.spritecollide(self, self.walls, False)
            for block in block_hit_list:
                # Reset our position based on the top/bottom of the object.
                if self.change_y > 0:
                    self.rect.bottom = block.rect.top
                else:
                    self.rect.top = block.rect.bottom
    
    
    BLACK = (0,0,0)
    WHITE = (255,255,255)
    GREEN = (0,255,0)
    RED = (255,0,0)
    BLUE = (0,0,255)
    GOLD = (255,215,0)
    
    size = (500, 500)
    screen = pg.display.set_mode(size)
    pg.display.set_caption("The Life Game")
    
    wall_list = pg.sprite.Group()
    all_sprites = pg.sprite.Group()
    enemy_list = pg.sprite.Group()
    coins = pg.sprite.Group()
    
    player = Player()
    player.walls = wall_list
    all_sprites.add(player)
    
    for i in range(random.randrange(100,200)):
        x = random.randrange(size[0])
        y = random.randrange(size[1])
        whiteStar = Collectable(WHITE, x, y, "White Star", "Rect")
        all_sprites.add(whiteStar)
    
    for i in range(50):
        x = random.randrange(size[0])
        y = random.randrange(size[1])
        enemy = Collectable(RED, x, y, "Enemy","Ellipse")
        enemy_list.add(enemy)
        all_sprites.add(enemy)
    
    coin1 = Collectable(GOLD,240,200,"Coin","Ellipse")
    coin2 = Collectable(GOLD,100,340,"Coin","Ellipse")
    all_sprites.add(coin1, coin2)
    coins.add(coin1, coin2)
    
    # Make the walls.
    walls = [Wall(0,0,10,600,GREEN), Wall(50, 300, 400, 10,RED),
             Wall(10, 200, 100, 10,BLUE)]
    wall_list.add(walls)
    all_sprites.add(walls)
    
    
    def main():
        clock = pg.time.Clock()
        done = False
        score = 0
        font_score = pg.font.SysFont('Calibri',20,True,False)
        font_health = pg.font.SysFont('Calibri',20,True,False)
        while not done:
            for event in pg.event.get():
                if event.type == pg.QUIT:
                    done = True
                # Do the movement in the event loop by setting
                # the player's change_x and y attributes.
                elif event.type == pg.KEYDOWN:
                    if event.key == pg.K_LEFT:
                        player.change_x = -3
                    elif event.key == pg.K_RIGHT:
                        player.change_x = 3
                    elif event.key == pg.K_UP:
                        player.change_y = -3
                    elif event.key == pg.K_DOWN:
                        player.change_y = 3
                elif event.type == pg.KEYUP:
                    if event.key == pg.K_LEFT and player.change_x < 0:
                        player.change_x = 0
                    elif event.key == pg.K_RIGHT and player.change_x > 0:
                        player.change_x = 0
                    elif event.key == pg.K_UP and player.change_y < 0:
                        player.change_y = 0
                    elif event.key == pg.K_DOWN and player.change_y > 0:
                        player.change_y = 0
    
            # UPDATE SECTION / Put the logic of your game here (i.e. how
            # objects move, when to fire them, etc).
            all_sprites.update()
    
            # spritecollide returns a list of the collided sprites in the
            # passed group. Iterate over this list to do something per
            # collided sprite. Set dokill argument to True to kill the sprite.
            collided_enemies = pg.sprite.spritecollide(player, enemy_list, True)
            for enemy in collided_enemies:
                player.health -= 25
    
            collided_coins = pg.sprite.spritecollide(player, coins, True)
            for coin in collided_coins:
                score += 1
    
            # DRAW SECTION
            screen.fill(BLACK)
            all_sprites.draw(screen)
    
            health_label = font_health.render("Health: "+str(player.health),True,WHITE)
            score_label = font_score.render("Score: " + str(score),True, WHITE)
            screen.blit(score_label,[100,480])
            screen.blit(health_label,[190,480])
    
            pg.display.flip()
            clock.tick(60)
    
    if __name__ == '__main__':
        main()
        pg.quit()
        sys.exit()