简体   繁体   中英

How do I avoid an glitchy collision between circle and rectangle in PyGame?

The pong paddle moves so fast that the ball winds up inside the paddle before the collision is detected. The problem is the user input moves the paddle by a single pixel so I don't know how to slow it down. What is the fix? Here is the code:

import pygame, sys, os
from pygame import*
from pygame.locals import*

WHITE = (255, 255, 255)
GREEN = (0, 255, 0)
BLUE = (0, 0, 128)
RED = (255,0,0)
os.environ["SDL_VIDEO_CENTERED"]="1"
displaysize=600
DISPLAYSURF = pygame.display.set_mode((displaysize,displaysize))
rectwidth = 50
rectheight= 50
rectposx =0
rectposy =0

class Player(object):
    def __init__(self):
        self.rect = pygame.rect.Rect((rectposx, rectposy, rectwidth, rectheight))
    def handle_keys(self):
        key = pygame.key.get_pressed()
        dist = 1
        if key[pygame.K_LEFT] and (player.rect.x>0):
            self.rect.move_ip(-1, 0)
        if key[pygame.K_RIGHT] and (player.rect.x<600-rectwidth):
            self.rect.move_ip(1, 0)
        if key[pygame.K_UP] and (player.rect.y>0):
            self.rect.move_ip(0, -1)
        if key[pygame.K_DOWN] and (player.rect.y<600-rectheight):
            self.rect.move_ip(0, 1)
    def draw(self, DISPLAYSURF):
        pygame.draw.rect(DISPLAYSURF, BLUE, self.rect)
    def postext(self):
        pygame.image.load(self.rect).convert_alpha()

pygame.init()
player =Player()
pygame.display.set_caption('Hello World!')

clock=pygame.time.Clock()
fontObj = pygame.font.Font(None,32)
textSurfaceObj = fontObj.render('Hello World!', True, GREEN, BLUE)
#textPosition =
dt=0.1
v = pygame.math.Vector2(5,5)
ballposx=200
ballposy=200
ballrad=10
#DISPLAYSURF.fill(WHITE)
#x=10
#y=10
#dx=5
#rectpos = pygame.Rect(x,y,50,50)
#rect = pygame.draw.rect(DISPLAYSURF, BLUE, rectpos)
pygame.display.update()
running = True
n=0
while running:
    for event in pygame.event.get():
        if event.type == KEYDOWN:
            if event.key == K_ESCAPE:
                running = False
        if event.type==QUIT:
            pygame.quit()
            sys.exit
    player.handle_keys()
    ballposx=ballposx+v[0]*dt
    ballposy=ballposy+v[1]*dt
    DISPLAYSURF.fill(WHITE)
    DISPLAYSURF.blit(textSurfaceObj,(0,0))
    player.draw(DISPLAYSURF)
    ball=pygame.draw.circle(DISPLAYSURF, GREEN, (int(ballposx),int(ballposy)), ballrad)
    rectposx1=player.rect.x
    rectposy1=player.rect.y
    rectvelx=-(rectposx-rectposx1)/dt
    rectvely=-(rectposy-rectposy1)/dt
    if ballposx-ballrad<0:
        v[0]=-v[0]
    if ballposy-ballrad<0:
        v[1]=-v[1]
    if ballposx+ballrad>600:
        v[0]=-v[0]
    if ballposy+ballrad>600:
        v[1]=-v[1]
    if player.rect.colliderect(ball):
        pygame.math.Vector2.reflect_ip(v,-v+5*pygame.math.Vector2(rectvelx,rectvely)) 

    #print (player.rect.x, rectposy, ball.x, ball.y)
    ballmass=1
    rectmass=5
    rectposx=rectposx1
    rectposy=rectposy1
    print (v)
            #raise SystemExit("You win!")
    pygame.display.update()
    clock.tick(120)

One option would be to reduce the fps with something like: clock.tick(30) . I doubt you need 120 FPS for pong

BUT if you do you will need to account for inter-pixel movement (and even if you drop the framerate you should do this anyway). That will involve some change, here is what I noticed will need changing:

The lines:

ball=pygame.draw.circle(DISPLAYSURF, GREEN, (int(ballposx),int(ballposy)), ballrad)
        rectposx1=player.rect.x
        rectposy1=player.rect.y

Ensures that you cast your position [ballposx,ballposy] to integers for the drawing AND keep them as integers for the calculation. This will give less control than the following:

ball=pygame.draw.circle(DISPLAYSURF, GREEN, (int(ballposx),int(ballposy)), ballrad)
        rectposx1=ballposx
        rectposy1=ballposy

Here we still cast to integers to draw the rect BUT for the calculation we keep a more precise value of the ball's position. This way, if your speed is 1/2 pixel per frame you will move once every 2 frames, instead of either not at all or 1 pixel per frame.

Indeed, your collision detection expects the ball to touch the paddle, but your ball moves 5 pixels a frame, so it can jump through the edge of the paddle in a single move.

That's how I would do collision detection:

  • Track the current and the previous ball position, at least the vertical position.
  • On every frame, check if the ball's previous position was above the paddle, and current position as at or below the paddle.
  • If the ball is at the paddle, the reflection is trivial: just reverse the vertical speed.
  • If the ball is below the paddle, the reflection is somehow less trivial, but still easy: reverse the vertical speed and add 5 (or whatever vertical unit speed is) to the vertical position.

There are 2 strategies to a void that.

  1. Move the ball in the way, that it is touching the player but not intersecting the player once a collision is detected. eg:

     dx = ballposx - player.rect.centerx dy = ballposy - player.rect.centery if abs(dx) > abs(dy): ballposx = player.rect.left-ballrad if dx < 0 else player.rect.right+ballrad else: ballposy = player.rect.top-ballrad if dy < 0 else player.rect.bottom+ballrad
  2. Reflect the movement of the ball only if its movement vector points in a direction "against" the ball. eg:

     if abs(dx) > abs(dy): if (dx < 0 and v[0] > 0) or (dx > 0 and v[0] < 0): v.reflect_ip(pygame.math.Vector2(1, 0)) else: if (dy < 0 and v[1] > 0) or (dy > 0 and v[1] < 0): v.reflect_ip(pygame.math.Vector2(0, 1))

See also Sometimes the ball doesn't bounce off the paddle in pong game

Applying these 2 fixes to your code the ball will reflect properly on the player. eg:

ball = pygame.Rect((0,0), (ballrad*2, ballrad*2))
ball.center = int(ballposx),int(ballposy)
if player.rect.colliderect(ball):
    dx = ballposx - player.rect.centerx
    dy = ballposy - player.rect.centery
    if abs(dx) > abs(dy):
        ballposx = player.rect.left-ballrad if dx < 0 else player.rect.right+ballrad
        if (dx < 0 and v[0] > 0) or (dx > 0 and v[0] < 0):
            v.reflect_ip(pygame.math.Vector2(1, 0))
    else:
        ballposy = player.rect.top-ballrad if dy < 0 else player.rect.bottom+ballrad
        if (dy < 0 and v[1] > 0) or (dy > 0 and v[1] < 0):
            v.reflect_ip(pygame.math.Vector2(0, 1))

If you want to avoid the player pushing the ball out of the window, you need to restrict the ball to the window area and reflect the ball off the edges of the window like a pool ball:

min_x, min_y, max_x, max_y = 0, 0, window.get_width(), window.get_height()

ballposx = ballposx + v[0]*dt
ballposy = ballposy + v[1]*dt
if ballposx-ballrad < min_x:
    ballposx = ballrad+min_x
    v[0]=-v[0]
if ballposy-ballrad < min_y:
    ballposy = ballrad+min_y
    v[1]=-v[1]
if ballposx + ballrad > max_x:
    ballposx = max_x-ballrad
    v[0]=-v[0]
if ballposy + ballrad > max_y:
    ballposy = max_y-ballrad
    v[1]=-v[1]

See also Use vector2 in pygame. Collide with the window frame and restrict the ball to the rectangular area respectively How to make ball bounce off wall with Pygame? .

When a collision is detected, the player's position must be restricted so that the ball can take place between the widow boundary and the player:

if abs(dx) > abs(dy):
    if dx < 0:
        ballposx = max(player.rect.left-ballrad, ballrad+min_x)
        player.rect.left = int(ballposx)+ballrad
    else:
        ballposx = min(player.rect.right+ballrad, max_x-ballrad)
        player.rect.right = int(ballposx)-ballrad

With these changes, the ball can even be "squeezed" between the edge of the window and the player:

Minimal example:

import pygame

class Player(object):
    def __init__(self, x, y, w, h):
        self.rect = pygame.rect.Rect(x, y, w, h)
    def handle_keys(self):
        key = pygame.key.get_pressed()
        if key[pygame.K_LEFT]:
            self.rect.left = max(20, self.rect.left - 1)
        if key[pygame.K_RIGHT]:
            self.rect.right = min(window.get_height() - 20, self.rect.right + 1)
        if key[pygame.K_UP]:
            self.rect.top = max(20, self.rect.top - 1)
        if key[pygame.K_DOWN]:
            self.rect.bottom = min(window.get_width() - 20, self.rect.bottom + 1)
    def draw(self, surface):
        pygame.draw.rect(surface, (0, 0, 128), self.rect)

pygame.init()
window = pygame.display.set_mode((240, 240))
clock=pygame.time.Clock()

player = Player(20, 20, 50, 50)
v, vel = pygame.math.Vector2(1, 1), 0.5
ballPosX, ballPosY, ballRadius = 120, 120, 10

run = True
while run:
    clock.tick(120)
    for event in pygame.event.get():
        if event.type==pygame.QUIT:
            run = False
    player.handle_keys()  

    min_x, min_y, max_x, max_y = 20, 20, window.get_width()-20, window.get_height()-20
    ballPosX += v[0] * vel
    ballPosY += v[1] * vel
    if ballPosX - ballRadius < min_x:
        ballPosX = ballRadius + min_x
        v[0] = -v[0]
    if ballPosY - ballRadius < min_y:
        ballPosY = ballRadius + min_y
        v[1] = -v[1]
    if ballPosX + ballRadius > max_x:
        ballPosX = max_x - ballRadius
        v[0] = -v[0]
    if ballPosY + ballRadius > max_y:
        ballPosY = max_y - ballRadius
        v[1] = -v[1]

    ball = pygame.Rect((0,0), (ballRadius*2, ballRadius*2))
    ball.center = int(ballPosX),int(ballPosY)
    if player.rect.colliderect(ball):
        dx = ballPosX - player.rect.centerx
        dy = ballPosY - player.rect.centery
        if abs(dx) > abs(dy):
            if dx < 0:
                ballPosX = max(player.rect.left-ballRadius, ballRadius+min_x) 
                player.rect.left = int(ballPosX)+ballRadius
            else:
                ballPosX = min(player.rect.right+ballRadius, max_x-ballRadius)
                player.rect.right = int(ballPosX)-ballRadius
            if (dx < 0 and v[0] > 0) or (dx > 0 and v[0] < 0):
                v.reflect_ip(pygame.math.Vector2(1, 0))
        else:
            if dy < 0:
                ballPosY = max(player.rect.top-ballRadius, ballRadius+min_y) 
                player.rect.top = int(ballPosY)+ballRadius
            else:
                ballPosY = min(player.rect.bottom+ballRadius, max_y-ballRadius)
                player.rect.bottom = int(ballPosY)-ballRadius
            ballPosY = player.rect.top-ballRadius if dy < 0 else player.rect.bottom+ballRadius
            if (dy < 0 and v[1] > 0) or (dy > 0 and v[1] < 0):
                v.reflect_ip(pygame.math.Vector2(0, 1))


    window.fill((255, 255, 255))
    pygame.draw.rect(window, (255,0,0), (18, 18, 203, 203), 2)
    player.draw(window)
    pygame.draw.circle(window, (0, 255, 0), (round(ballPosX), round(ballPosY)), ballRadius)
    pygame.display.update()

pygame.quit()
exit()

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM