1

I am new in pygame and I am trying to get the basic knowledge. I want to create the obstacles and detect which side of the player rectangle (that represents the colliding box of the sprite) is colliding with the obstacle rectangle. With this I can create basic physics in a game.

This is what I have done:

import pygame

class Player (pygame.sprite.Sprite):
    def __init__(self, x=0, y=0, s=100):
        super(Player,self).__init__()
        self.image = pygame.transform.scale(pygame.image.load("player.png"), (s, s))
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

class Block (pygame.sprite.Sprite):
    def __init__(self, x=0, y=500, s=100):
        super(Block, self).__init__()
        self.image = pygame.transform.scale(pygame.image.load("wall.png"),(s, s))
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

pygame.init()

display = pygame.display.set_mode((800,600))

player = Player()
block = Block()

sprites = pygame.sprite.Group()
living = pygame.sprite.Group()
noliving = pygame.sprite.Group()

sprites.add(player)
sprites.add(block)
living.add(player)
noliving.add(block)

gravity = 1

xCh = 0
yCh = 0

speed = 1

while True:

    display.fill((159, 159, 159))

    for liv in living:
        for noliv in noliving:
            if(not pygame.sprite.collide_rect(liv, noliv)):
                player.rect.y += gravity

    for event in pygame.event.get():
        if(event.type == pygame.KEYDOWN):
            if(event.key == pygame.K_ESCAPE):
                quit()
            elif(event.key == pygame.K_a or event.key == pygame.K_LEFT):
                xCh = -speed
            elif(event.key == pygame.K_d or event.key == pygame.K_RIGHT):
                xCh = speed
        elif(event.type == pygame.KEYUP):
            xCh = 0
            yCh = 0
        elif(event.type == pygame.QUIT):
            quit()

    player.rect.x += xCh

    sprites.draw(display)

    pygame.display.update()

The player stops falling when it touches the block, but if I go left or right and then into the block, the player goes straight into the block and stops falling from there. I want the player to be unable to go through the block. How to do that?

user193464
  • 380
  • 1
  • 6
  • 25

2 Answers2

2

Here is your code with a suggested fix, along with several other advice for improvement.

First things first: what was wrong is that you never checked if the horizontal movement would make the blocks collide. You had only one check in place, and it actually only did anything after any collision had already happened. And in this case, it would prevent "gravity" from doing anything. Since you were updating your player pixel by pixel, the fact the collision had already happened went by mostly unnoticed. The fix is to create a check that verifies if the moving block can move to a certain place, and only then allowing it. And do it for all of the movement, not only for gravity movement.

So, here I will explain the improvements that can allow this sample code to evolve into a complete game.

  1. Eliminate "floating code" on the module level and put everything inside functions. This is essential for any game that will have even something as simple as a starting screen (and possibly a "play again" screen) in the future. I made two functions, one that creates setup your game environment and creates your objects, and the main game function. You should separate the "setup" stage further in the future, for being able to create more game objects, or different game levels.

  2. Reuse your code. You had the lines in your __init__ method mostly duplicated between your two classes, but for initial position and image-name constants. So you'd actually need just one class, and pass different values for these attributes. Since you hint you will have two distinct game types of objects by using the groups and such, I kept the two classes,but factored out the common code.

  3. Frame delay: your code would simply move "as fast as possible" - which would create different game speeds in different computers, and use 100% of CPU - causing possible machine slowdowns and excessive power usage. I included a hardcoded pause of 30ms - which will allow your game to move at close to 30FPS, keep the speed stable across different devices. The other change needed is that your hardcoded "1" values for gravity and speed, that would make the player move 1px at a time had to be changed to larger values.

  4. Last but not least, the "can I move there" logic: it can actually seem extensive - but perceive that the plain English description of the checking of the "can this object move to that position" is lengthy. The method essentially does: "Store the current position". "Fake a position on with the given x and y displacements". "check if I am colliding with something". "if not, restore the previous position, and say the movement is allowed". "otherwise, restore the y position, check if there is a collision in the X direction, if so, restrict horizontal movement". "Restore the x position, move the y position by desired amount, check if collision happens. If so, restrict movement in the y direction". "If movement is not restricted in either x or y, it is just the combined movement that causes the collision: restrict movement in both directions to be fair". "Restore original position, return allowed changes to x and y positions, and the object we'd collide with". All this logic is put inside the "living" objects class itself, so it can be used at any time, with a single line of code in the main game logic.

code:

import pygame


class GameObject(pygame.sprite.Sprite):
    def __init__(self, x=0, y=0, s=100, image=""):
        super(GameObject, self).__init__()
        if not image:
            image = self.image
        self.image = pygame.transform.scale(pygame.image.load(image), (s, s))
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y


class Player(GameObject):
    image = "player.png"

    def can_move(self, xCh, yCh, group):
        old_pos = self.rect.x, self.rect.y
        self.rect.x += xCh
        self.rect.y += yCh
        collided_with  = pygame.sprite.spritecollideany(self, group)
        if not collided_with:
            # No Collisions at all - allow movement
            self.rect.x = old_pos[0]
            self.rect.y = old_pos[1]
            return True, xCh, yCh, None
        # Check if the colision was due to horizontal movement:
        self.rect.y = old_pos[1]
        if pygame.sprite.spritecollideany(self, group):
            # Yes, then indicate horizontal movement should be 0
            xCh = 0

        # check if collision was due to vertical movement:
        self.rect.y += yCh
        self.rect.x = old_pos[0]

        if pygame.sprite.spritecollideany(self, group):
            # Yes - indicate vertical movemnt should be 0
            yCh = 0

        # if only diagonal movement causes collision, then
        # cancel movrment in both axes:
        # (i.e. just the diagonal movement would hit the
        #  obstacle )
        if not xCh == 0 and not yCh == 0:
            xCh = 0
            yCh = 0

        self.rect.x = old_pos[0]
        self.rect.y = old_pos[1]
        return False, xCh, yCh, collided_with



class Block(GameObject):
    image = "wall.png"


def setup():
    global display, player, living, noliving, gravity, speed, sprites
    pygame.init()

    display = pygame.display.set_mode((800,600))

    player = Player()
    block = Block(y=500)

    sprites = pygame.sprite.Group()
    living = pygame.sprite.Group()
    noliving = pygame.sprite.Group()

    sprites.add(player)
    sprites.add(block)
    living.add(player)
    noliving.add(block)

    gravity = 10
    speed = 10


def main():
    xCh = 0
    yCh = 0
    while True:

        display.fill((159, 159, 159))

        for event in pygame.event.get():
            if(event.type == pygame.KEYDOWN):
                if(event.key == pygame.K_ESCAPE):
                    quit()
                elif(event.key == pygame.K_a or event.key == pygame.K_LEFT):
                    xCh = -speed
                elif(event.key == pygame.K_d or event.key == pygame.K_RIGHT):
                    xCh = speed
            elif(event.type == pygame.KEYUP):
                xCh = 0
                yCh = 0
            elif(event.type == pygame.QUIT):
                quit()

        yCh = gravity

        for liv in living:
            if liv == player:
                check_x_ch = xCh
            else:
                check_x_ch = 0
            can_move, xCh, yCh, obstacle = liv.can_move(xCh, yCh, noliving)

            liv.rect.x += xCh
            liv.rect.y += yCh

            # Do other actions if "can_move" indicates a block was hit...


        sprites.draw(display)

        pygame.display.update()
        pygame.time.delay(30)


setup()
main()
jsbueno
  • 99,910
  • 10
  • 151
  • 209
0

The last version of it is this:

import pygame

class GameObj(pygame.sprite.Sprite):
    def __init__(self, image, x, y, s):
        super(GameObj, self).__init__()
        self.image = pygame.transform.scale(pygame.image.load("img/"+ image), (s, s))
        self.rect = self.image.get_rect()
        self.rect.x = x
        self.rect.y = y

class Player(GameObj):

    leftUnable = False
    rightUnable = False

    jumpHeight = 200
    jumpSpeed = 5

    image = "player.png"
    inAir = True
    def __init__(self, x, y, s=100):
        super(Player,self).__init__(self.image, x, y, s)

class Block(GameObj):
    image = "wall.png"
    def __init__(self, x, y, s=100):
        super(Block, self).__init__(self.image, x, y, s)

def collideNum(sprite, group):
    total = 0
    for member in group:
        if(pygame.sprite.collide_rect(sprite, member)):
            total += 1

    return total

def setup():
    pygame.init()

    global display, player, block, sprites, living, noliving, clock

    display = pygame.display.set_mode((800, 600))
    pygame.display.set_caption("Test")

    clock = pygame.time.Clock()

    player = Player(100, 0)
    block = Block(100, 300)
    block1 = Block(200, 400)
    block2 = Block(400, 400)

    sprites = pygame.sprite.Group()
    living = pygame.sprite.Group()
    noliving = pygame.sprite.Group()

    noliving.add(block)
    noliving.add(block1)
    noliving.add(block2)

    living.add(player)

    for liv in living:
        sprites.add(liv)

    for noliv in noliving:
        sprites.add(noliv)

    main()

def main():
    speed = 5
    gravity = 5

    xCh, yCh = 0, 0

    player.leftUnable = False
    player.rightUnable = False

    while True:
        clock.tick(60)

        display.fill((184, 184, 184))

        yCh = gravity

        for event in pygame.event.get():
            if(event.type == pygame.QUIT):
                quit()
            elif(event.type == pygame.KEYDOWN):
                if(event.key == pygame.K_ESCAPE):
                    quit()
                elif((event.key == pygame.K_a or event.key == pygame.K_LEFT) and not player.leftUnable):
                    for noliv in noliving:
                        if(pygame.sprite.collide_rect(player, noliv)):
                            if(noliv.rect.left < player.rect.left < noliv.rect.right and player.rect.bottom > noliv.rect.top + 5):
                                player.leftUnable = True
                                break
                            else:
                                xCh = -speed
                                player.leftUnable = False

                        else:
                            xCh = -speed
                            player.leftUnable = False

                elif((event.key == pygame.K_d or event.key == pygame.K_RIGHT) and not player.rightUnable):
                    for noliv in noliving:
                        if(pygame.sprite.collide_rect(player, noliv)):
                            if(noliv.rect.left < player.rect.right < noliv.rect.right and player.rect.bottom > noliv.rect.top + 5):
                                player.rightUnable = True
                                break
                            else:
                                xCh = speed
                                player.rightUnable = False

                        else:
                            xCh = speed
                            player.rightUnable = False
                elif(event.key == pygame.K_SPACE or event.key == pygame.K_w or event.key == pygame.K_UP):
                    oldPos = player.rect.bottom
                    xCh = 0
                    if(not player.inAir):
                        while player.rect.bottom > oldPos - player.jumpHeight:
                            clock.tick(60)
                            display.fill((184, 184, 184))

                            for ev in pygame.event.get():
                                if(ev.type == pygame.KEYDOWN):
                                    if(ev.key == pygame.K_d or ev.key == pygame.K_RIGHT):
                                        xCh = speed
                                    elif(ev.key == pygame.K_a or ev.key == pygame.K_LEFT):
                                        xCh = -speed
                                elif(ev.type == pygame.KEYUP):
                                    xCh = 0

                            player.rect.x += xCh

                            player.rect.y -= player.jumpSpeed
                            player.inAir = True

                            sprites.draw(display)
                            pygame.display.update()
            elif(event.type == pygame.KEYUP):
                xCh = 0

        for liv in living:
            for noliv in noliving:
                if(pygame.sprite.collide_rect(liv, noliv)):
                    liv.inAir = False
                    break
                else:
                    liv.inAir = True



        for noliv in noliving:
            if(pygame.sprite.collide_rect(player, noliv)):
                if(noliv.rect.left < player.rect.left < noliv.rect.right and player.rect.bottom > noliv.rect.top + 5):
                    player.leftUnable = True
                    if(collideNum(player, noliving) == 1):
                        player.inAir = True
                    if(xCh < 0):
                        xCh = 0
                elif(noliv.rect.left < player.rect.right < noliv.rect.right and player.rect.bottom > noliv.rect.top + 5):
                    player.rightUnable = True
                    if(collideNum(player, noliving) == 1):
                        player.inAir = True
                    if(xCh > 0):
                        xCh = 0
                else:
                    player.leftUnable = False
                    player.rightUnable = False
            else:
                player.leftUnable = False
                player.rightUnable = False

        if(not player.inAir):
            yCh = 0

        if(player.rect.top > display.get_size()[1]):
            setup()

        player.rect.x += xCh
        player.rect.y += yCh

        sprites.draw(display)
        pygame.display.update()

setup()

It is working perfectly.

user193464
  • 380
  • 1
  • 6
  • 25
  • But in this code you change xCh to 0 before its value is changed by a key being pressed. It may "make things a little better" - but it actually does not prevent the overlap to occur. This approach is essentially incorrect. – jsbueno Aug 30 '17 at 00:56