Search code examples
python-2.7pygamecollision-detectionspriterect

Sprites not aligning to boundaries correctly


I am creating a basic Pong-style game using Pygame, and I have gotten as far as moving the paddles. I have code that should prevent the paddles from moving beyond the edges of the screen, but if a player holds down a movement key, the paddle moves just slightly past the edge of the screen. Once the player releases the key, the paddle snaps back to where it ought to stop.

I am somewhat new to Python and coding in general, so my code may not be the tidiest or most efficient. While this problem may not affect gameplay at all, I would like to know why it behaves this way and how to change it to how I want it.

My code is:

import random
import pygame
from pygame.locals import *
from sys import exit

screen_size = 600, 600
w,h = screen_size
screen = pygame.display.set_mode(screen_size)
clock = pygame.time.Clock()
pic = pygame.image.load

class paddle(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)

        self.pos = pos
        self.posx, self.posy = self.pos
        self.width = 10
        self.height = 100
        self.image = pygame.Surface((self.width, self.height))
        self.image.fill((255,255,255))
        self.rect = pygame.Rect((0,0), self.image.get_size())
        self.speed = 150

    def update(self, mov, tp):
        self.posy += mov * self.speed * tp
        self.rect.center = self.posx, self.posy

class box(pygame.sprite.Sprite):
    def __init__(self, pos):
        pygame.sprite.Sprite.__init__(self)

        self.pos = pos
        self.posx, self.posy = self.pos
        self.width = 10
        self.height = 10
        self.image = pygame.Surface((self.width, self.height))
        self.image.fill((255,255,255))
        self.rect = pygame.Rect((0,0), self.image.get_size())
        self.speed = 300        

        self.directionx = 0
        self.directiony = 0

    def update(self, mov, tp):
        self.posx += mov[0] * self.speed * tp
        self.posy += mov[1] * self.speed * tp
        self.rect.center = self.posx, self.posy

reset = True

done = False
while done == False:

    if reset == True:
        p1 = paddle((0,0))
        p1 = paddle((20,(h-p1.height)/2))
        p2 = paddle((0,0))
        p2 = paddle((w-20,(h-p2.height)/2))
        paddle_group = pygame.sprite.Group()
        paddle_group.add(p1)
        paddle_group.add(p2)

        ball = box(((w/2)-10,h/2))
        ball_group = pygame.sprite.Group(ball)

        reset = False
    else:
        pass

    time_passed = clock.tick(60)
    time_passed_seconds = time_passed/1000.0

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

    p1_movey = 0
    p2_movey = 0

    pressed_keys = pygame.key.get_pressed()
    pressed_mb = pygame.mouse.get_pressed()

    if pressed_keys[K_ESCAPE]:
        done = True

    if pressed_keys[K_w]:
        p1_movey = -2
    elif pressed_keys[K_s]:
        p1_movey = +2

    if pressed_keys[K_UP]:
        p2_movey = -2
    elif pressed_keys[K_DOWN]:
        p2_movey = +2

    p1.update(p1_movey, time_passed_seconds)
    p2.update(p2_movey, time_passed_seconds)


# This is where the border check is
    for PADDLE in paddle_group.sprites():

        if PADDLE.posy > h - (PADDLE.height/2):
            PADDLE.posy = h - (PADDLE.height/2)
        elif PADDLE.posy < (PADDLE.height/2):
            PADDLE.posy = (PADDLE.height/2)

    screen.fill((0,0,0))

    paddle_group.draw(screen)

    pygame.display.update()

pygame.quit()

The boundary part is marked with a comment.


Solution

  • The problem is that you're correcting each paddle's posy without adjusting its rect at the same time. posx and posy store the location of your sprite-- in this case, it's center-- but the position of what you see on-screen is determined by the rect. Because you add p#_movey, then update (which adjusts the rect), then, finally, make the posy correction for out-of-bounds values, rect remains at it's invalid location. Because you have adjusted posy, though, future p#_movey changes effect the correct location, not the invalid one (which is why your sprite remains O.B. until the movement key is released).

    In short, this is what's going on:

    1. Determine p#_movey for each player.
    2. Apply p#_movey to player #'s paddle's posy and rect via update.
    3. Adjust PADDLE.posy to keep it in-bounds for each PADDLE in paddle_group, but do not update PADDLE.rect to reflect the adjustment. (And rect does need to be updated.)
    4. Conclude the frame.

    There are two solutions I can think of:

    1) Modify your for PADDLE in paddle_group.sprites() loop so that PADDLE.rect.center is refreshed, that it looks something like this:

      # on the main loop...
    for PADDLE in paddle_group.sprites():
        if PADDLE.posy > h - (PADDLE.height/2):
            PADDLE.posy = h - (PADDLE.height/2)
        elif PADDLE.posy < (PADDLE.height/2):
            PADDLE.posy = (PADDLE.height/2)
    
        PADDLE.rect.center = PADDLE.posx, PADDLE.posy
            ## ^ adding this line, which recenters the rect.
    

    2) Alternatively, you could run the boundary check in paddle's update method and remove the for PADDLE in paddle_group.sprites() loop from the main loop all together. paddle.update(..) would then look something like this:

      # in class paddle(..) ...
    def update(self, mov, tp):
        self.posy += mov * self.speed * tp
        self.posy  = max(self.height / 2, min(self.posy, h - (self.height / 2)))
            ## ^ Adding this line, which checks boundaries for posy.
            ## because you're doing it here, there's no need to do it again on the main loop.
        self.rect.center = self.posx, self.posy
    

    ... as long as your boundaries are defined by h (screen height; the bottom of your window) and 0 (the top of the window) and the height of the paddle.

    Either solution should work, though there are almost certainly third ways, too. Hope that helps!