如何使用 pygame 对乒乓球中的弹跳运动进行编码

How to code bounce movement in pong using pygame

我是 python 的菜鸟,我正在尝试重新创建 Pong 游戏,并且我正在尝试尽可能自己做。

这些是我目前的代码存在的问题:

  1. 我基本上对球在边缘反弹时的每一个可能运动进行了编码。我花了几个小时研究它,我让它开始工作,我只是想知道是否有更有效的方法来用我的代码生成类似的输出(因为我知道这应该是一个初学者项目)?
  2. 每次球弹过边缘时,分数都会瞬间增加,每次球重生时都会回到 0。
  3. 如何让小球随机生成?在每一轮的开始(和重新开始)?

这是我的 class 代码和程序的主要功能:

import pygame
import random
from rect_button import Button

pygame.init()

WIDTH = 800
HEIGHT = 500
window = pygame.display.set_mode((WIDTH, HEIGHT))

TITLE = "Pong"
pygame.display.set_caption(TITLE)

# COLORS
black = (0, 0, 0)
white = (255, 255, 255)
red = (255, 0, 0)
green = (0, 255, 0)

# FONTS
small_font = pygame.font.SysFont("courier", 20)
large_font = pygame.font.SysFont("courier", 60, True)


class Paddle:
    def __init__(self, x, y, width, height, vel, color):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.vel = vel
        self.color = color

    def draw(self, window):
        pygame.draw.rect(window, self.color, (self.x, self.y, self.width, self.height), 0)


class Ball:
    def __init__(self, x, y, side, vel, color):
        self.x = x
        self.y = y
        self.side = side
        self.vel = vel
        self.color = color
        self.lower_right = False
        self.lower_left = True
        self.upper_right = False
        self.upper_left = False
        self.ball_bag = []
        self.last_movement = 'ball.lower_right'

    def draw(self, window):
        pygame.draw.rect(window, self.color, (self.x, self.y, self.side, self.side), 0)

    def move_lower_right(self):
        self.x += self.vel
        self.y += self.vel

    def move_upper_right(self):
        self.x += self.vel
        self.y -= self.vel

    def move_upper_left(self):
        self.x -= self.vel
        self.y -= self.vel

    def move_lower_left(self):
        self.x -= self.vel
        self.y += self.vel

    def start(self):
        self.lower_right = True
        self.lower_left = False
        self.upper_right = False
        self.upper_left = False

        self.last_movement = 'ball.lower_left'

        # return random.choice([self.lower_right, self.lower_left, self.upper_left, self.upper_right]) is True

def main():
    run = True
    fps = 60
    clock = pygame.time.Clock()

    # Initializing Paddles

    left_paddle = Paddle(20, 100, 10, 50, 5, white)
    right_paddle = Paddle(770, 350, 10, 50, 5, white)

    balls = []

    def redraw_window():
        window.fill(black)

        left_paddle.draw(window)
        right_paddle.draw(window)
        for ball in balls:
            ball.draw(window)

        player_A_text = small_font.render("Player A: " + str(score_A), 1, white)
        player_B_text = small_font.render("Player B: " + str(score_B), 1, white)
        window.blit(player_A_text, (320 - int(player_A_text.get_width() / 2), 10))
        window.blit(player_B_text, (480 - int(player_B_text.get_width() / 2), 10))

        pygame.draw.rect(window, white, (20, 450, 760, 1), 0)
        pygame.draw.rect(window, white, (20, 49, 760, 1), 0)
        pygame.draw.rect(window, white, (19, 50, 1, 400), 0)
        pygame.draw.rect(window, white, (780, 50, 1, 400), 0)

        pygame.display.update()

    while run:
        score_A = 0
        score_B = 0
        clock.tick(fps)

        if len(balls) == 0:
            ball = Ball(random.randrange(320, 465), random.randrange(200, 285), 15, 3, white)
            balls.append(ball)
        if ball.lower_left:
            ball.move_lower_left()

            if ball.last_movement == 'ball.lower_right':
                if ball.y + ball.side > HEIGHT - 50:
                    ball.lower_left = False
                    ball.last_movement = 'ball.lower_left'
                    ball.upper_left = True

            if ball.last_movement == 'ball.upper_left':
                if ball.x < 30:
                    if left_paddle.x < ball.x < left_paddle.x + left_paddle.width:
                        if left_paddle.y < ball.y + ball.side < left_paddle.y + left_paddle.height:
                            ball.lower_left = False
                            ball.last_movement = 'ball.lower_left'
                            ball.lower_right = True
                    else:
                        score_B += 1
                        balls.remove(ball)
                        #ball.start()

                if ball.y + ball.side > HEIGHT - 50:
                    ball.lower_left = False
                    ball.last_movement = 'ball.lower_left'
                    ball.upper_left = True

        if ball.upper_left:
            ball.move_upper_left()

            if ball.last_movement == 'ball.lower_left':
                if ball.x < 30:
                    if left_paddle.x < ball.x < left_paddle.x + left_paddle.width:
                        if left_paddle.y < ball.y + ball.side < left_paddle.y + left_paddle.height:
                            ball.upper_left = False
                            ball.last_movement = 'ball.upper_left'
                            ball.upper_right = True
                    else:
                        score_B += 1
                        balls.remove(ball)
                        #ball.start()

                if ball.y < 50:
                    ball.upper_left = False
                    ball.last_movement = 'ball.upper_left'
                    ball.lower_left = True

            if ball.last_movement == 'ball.upper_right':
                if ball.y < 50:
                    ball.upper_left = False
                    ball.last_movement = 'ball.upper_left'
                    ball.lower_left = True

        if ball.upper_right:
            ball.move_upper_right()

            if ball.last_movement == 'ball.upper_left':
                if ball.y < 50:
                    ball.upper_right = False
                    ball.last_movement = 'ball.upper_right'
                    ball.lower_right = True

            if ball.last_movement == 'ball.lower_right':
                if ball.x + ball.side > WIDTH - 30:
                    if right_paddle.x + right_paddle.width > ball.x + ball.side > right_paddle.x:
                        if right_paddle.y < ball.y + ball.side < right_paddle.y + right_paddle.height:
                            ball.upper_right = False
                            ball.last_movement = 'ball.upper_right'
                            ball.upper_left = True
                    else:
                        score_A += 1
                        balls.remove(ball)
                        #ball.start()

                if ball.y < 50:
                    ball.upper_right = False
                    ball.last_movement = 'ball.upper_right'
                    ball.lower_right = True

        if ball.lower_right:
            ball.move_lower_right()

            if ball.last_movement == 'ball.upper_right':
                if ball.y + ball.side > HEIGHT - 50:
                    ball.lower_right = False
                    ball.last_movement = 'ball.lower_right'
                    ball.upper_right = True

                if ball.x + ball.side > WIDTH - 30:
                    if right_paddle.x + right_paddle.width > ball.x + ball.side > right_paddle.x:
                        if right_paddle.y < ball.y + ball.side < right_paddle.y + right_paddle.height:
                            ball.lower_right = False
                            ball.last_movement = 'ball.lower_right'
                            ball.lower_left = True
                    else:
                        score_A += 1
                        balls.remove(ball)
                        #ball.start()

            if ball.last_movement == 'ball.lower_left':
                if ball.y + ball.side > HEIGHT - 50:
                    ball.lower_right = False
                    ball.last_movement = 'ball.lower_right'
                    ball.upper_right = True

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
                quit()

        keys = pygame.key.get_pressed()

        if keys[pygame.K_UP] and right_paddle.y > 50:
            right_paddle.y -= right_paddle.vel
        if keys[pygame.K_w] and left_paddle.y > 50:
            left_paddle.y -= left_paddle.vel
        if keys[pygame.K_DOWN] and right_paddle.y + right_paddle.height < HEIGHT - 50:
            right_paddle.y += right_paddle.vel
        if keys[pygame.K_s] and left_paddle.y + left_paddle.height < HEIGHT - 50:
            left_paddle.y += left_paddle.vel
        if keys[pygame.K_SPACE]:
            pass

        redraw_window()

    quit()


def main_menu():
    run = True

    play_button = Button(green, 100, 350, 150, 75, "Play Pong")
    quit_button = Button(red, 550, 350, 150, 75, "Quit")

    pong_text = large_font.render("Let's Play Pong!!!", 1, black)

    while run:
        window.fill(white)

        play_button.draw(window, black)
        quit_button.draw(window, black)

        window.blit(pong_text, (int(WIDTH / 2 - pong_text.get_width() / 2), 100))

        pygame.display.update()

        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                run = False
                quit()

            if event.type == pygame.MOUSEMOTION:
                if play_button.hover(pygame.mouse.get_pos()):
                    play_button.color = (0, 200, 0)
                else:
                    play_button.color = green
                if quit_button.hover(pygame.mouse.get_pos()):
                    quit_button.color = (200, 0, 0)
                else:
                    quit_button.color = red

            if event.type == pygame.MOUSEBUTTONDOWN:
                if play_button.hover(pygame.mouse.get_pos()):
                    main()
                if quit_button.hover(pygame.mouse.get_pos()):
                    run = False
                    quit()


main_menu()

谢谢!!!

回答

  1. Every time the ball bounces past the edge, the score increments for a split second, and it goes back to 0 every time the ball respawns.

分数在主循环中不断初始化。你必须在循环之前初始化分数:

def main():
    # [...]

    score_A = 0 # <--- INSERT
    score_B = 0
    
    while run:
        # score_A = 0 <--- DELETE
        # score_B = 0

I basically coded every possible movement of the ball when it bounces on the edge. I spent hours working on it, and I got it to work, I was just wondering if there was a more efficient way to produce a similar output with my code (coz I know this is supposed to be a beginner project)?

How can I make the ball spawn at random directions? at the start (and restart) of every round?

您可以使用浮动坐标,而不是实现球的“每个”方向。这些变量通常称为 dx 和 dy。 通过这种方式为您的球获得随机或反向方向很简单,只需为 dx 和 dy 使用随机或反向值即可。

您的球的更新应该如下所示:

def update(self. dt):
    self.x += self.dx * self.speed * time_elapsed
    self.y += self.dy * self.speed * time_elapsed # Time elasped is often called dt.

Every time the ball bounces past the edge, the score increments for a split second, and it goes back to 0 every time the ball respawns.

查看 Rabid76 的回答。理想情况下,您应该有一个 GameState 对象,其中包含分数、生命和其他内容作为属性。

问题 1 的答案: 是的。肯定有很多更有效的方法来做事。没有必要为方向做那么多变数。简单地说direction = [True, False],其中direction[0]表示左,not direction[0]表示x-axis中的右。同样,direction[1]代表y轴。这也解决了您在开始时随机化方向的问题。你可以简单地在你的 init 方法中做 direction = [random.randint(0, 1), random.randint(0, 1)] 来随机化方向。以同样的方式,也列出​​速度。 self.speed = [0.5, random.uniform(0.1, 1)]。这样,左右方向的速度将始终相同,但 y 将根据所选的随机数而变化,因此具有适当的随机性,您也不必硬编码随机方向。这样,移动变得非常简单。

class Ball:
def __init__(self, x, y, color, size):
    self.x = x
    self.y = y
    self.color = color
    self.size = size
    self.direction = [random.randint(0, 1), random.randint(0, 1)]
    self.speed = [0.5, random.uniform(0.1, 1)]
    
def draw(self, display):
    pygame.draw.rect(display, self.color, (self.x, self.y, self.size, self.size))

def move(self):
    if self.direction[0]:
        self.x += self.speed[0]
    else:
        self.x -= self.speed[0]
    if self.direction[1]:
        self.y += self.speed[0]
    else:
        self.y -= self.speed[0]

因为我们定义的方向是布尔值,改变状态也变得非常容易。如果球击中球拍,您只需切换 x 中的布尔值 direction[0] = not direction[0] 并为 y 选择一个新的随机数,而不是手动分配布尔值。

def switchDirection(self):
    self.direction = not self.direction
    self.speed[1] = random.uniform(0.1, 1)

Paddle 也可以稍微改进,通过给 Paddle class 一个 move 函数而不是在主循环中移动。这只是意味着您必须编写更少的代码。

def move(self, vel, up=pygame.K_UP, down=pygame.K_DOWN):
    keys = pygame.key.get_perssed()
    if keys[up]:
        self.y -= vel
    if keys[down]:
        self.y += vel

对于碰撞,我建议使用 pygame.Rect()colliderect,因为它更健壮,也可能更高效。

示例:

import random
import pygame

WIN = pygame.display
D = WIN.set_mode((800, 500))

class Paddle:
    def __init__(self, x, y, width, height, vel, color):
        self.x = x
        self.y = y
        self.width = width
        self.height = height
        self.vel = vel
        self.color = color

    def move(self, vel, up=pygame.K_UP, down=pygame.K_DOWN):
        keys = pygame.key.get_pressed()
        if keys[up]:
            self.y -= vel
        if keys[down]:
            self.y += vel
        
    def draw(self, window):
        pygame.draw.rect(window, self.color, (self.x, self.y, self.width, self.height), 0)

    def getRect(self):
        return pygame.Rect(self.x, self.y, self.width, self.height)

left_paddle = Paddle(20, 100, 10, 50, 5, (0, 0, 0))
right_paddle = Paddle(770, 350, 10, 50, 5, (0, 0, 0))

class Ball:
    def __init__(self, x, y, color, size):
        self.x = x
        self.y = y
        self.color = color
        self.size = size
        self.direction = [random.randint(0, 1), random.randint(0, 1)]
        self.speed = [0.3, random.uniform(0.2, 0.2)]
        
    def draw(self, window):
        pygame.draw.rect(window, self.color, (self.x, self.y, self.size, self.size))

    def switchDirection(self):
        self.direction[0] = not self.direction[0]
        self.direction[1] = not self.direction[1]
        self.speed = [0.2, random.uniform(0.1, 0.5)]

    def bounce(self):
        self.direction[1] = not self.direction[1]
        self.speed = [0.2, random.uniform(0.01, 0.2)]
        
    def move(self):
        if self.direction[0]:
            self.x += self.speed[0]
        else:
            self.x -= self.speed[0]
        if self.direction[1]:
            self.y += self.speed[1]
        else:
            self.y -= self.speed[1]

    def getRect(self):
        return pygame.Rect(self.x, self.y, self.size, self.size)

    def boundaries(self):
        if ball.x <= 10:
            self.switchDirection()
        if ball.x + self.size >= 800:
            self.switchDirection()
        if ball.y + self.size >= 490:
            self.bounce()
        if ball.y <= 0:
            self.bounce()


ball = Ball(400, 250, (255, 0, 0), 20)
while True:
    pygame.event.get()
    D.fill((255, 255, 255))

    ball.draw(D)
    ball.boundaries()
    ball.move()
    #print(ball.x, ball.y)
    
    left_paddle.draw(D)
    right_paddle.draw(D)

    right_paddle.move(0.4)
    left_paddle.move(0.4, down=pygame.K_s, up=pygame.K_w)

    if left_paddle.getRect().colliderect(ball.getRect()):
        ball.switchDirection()
    if right_paddle.getRect().colliderect(ball.getRect()):
        ball.switchDirection()
    
    WIN.flip()

一些关于如何减少所有球 movement/collision 代码并使事情更可重用的思考:

import pygame


class Ball:

    def __init__(self, bounds, color):
        from random import randint, choice
        self.bounds = bounds
        self.position = pygame.math.Vector2(
            randint(self.bounds.left, self.bounds.left+self.bounds.width),
            randint(self.bounds.top, self.bounds.top+self.bounds.height)
        )
        self.velocity = pygame.math.Vector2(choice((-1, 1)), choice((-1, 1)))
        self.color = color
        self.size = 8

    def draw(self, window):
        pygame.draw.rect(
            window,
            self.color,
            (
                self.position.x-self.size,
                self.position.y-self.size,
                self.size*2,
                self.size*2
            ),
            0
        )

    def update(self):
        self.position.x += self.velocity.x
        self.position.y += self.velocity.y
        if not self.bounds.left+self.size < self.position.x < self.bounds.left+self.bounds.width-self.size:
            self.velocity.x *= -1
        if not self.bounds.top+self.size < self.position.y < self.bounds.top+self.bounds.height-self.size:
            self.velocity.y *= -1


def main():

    from random import randint

    window_width, window_height = 800, 500

    window = pygame.display.set_mode((window_width, window_height))
    pygame.display.set_caption("Pong")

    clock = pygame.time.Clock()

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

    padding = 20

    bounds = pygame.Rect(padding, padding, window_width-(padding*2), window_height-(padding*2))

    ball = Ball(bounds, white)

    def redraw_window():
        window.fill(black)
        pygame.draw.rect(
            window,
            white,
            (
                padding,
                padding,
                window_width-(padding*2),
                window_height-(padding*2)
            ),
            1
        )

        ball.draw(window)

        pygame.display.update()

    while True:
        clock.tick(60)
        ball.update()
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                break
        else:
            redraw_window()
            continue
        break
    pygame.quit()
    return 0
        

if __name__ == "__main__":
    import sys
    sys.exit(main())

这不是一个完整的 drop-in 替换,我刚刚重新实现了 Ball class。没有桨。本质上,当实例化一个球时,您传入一个 pygame.Rect,它描述了允许球弹跳的范围。您还传入了一个颜色元组。然后球在边界内选择一个随机位置(该位置是 pygame.math.Vector2,而不是将 xy 存储为单独的实例变量)。一个球也有一个速度,它也是一个 pygame.math.Vector2,所以你可能有独立的速度分量 - 一个用于 x(水平速度),一个用于 y(垂直速度)。球的 size 简单地描述了球的尺寸。例如,如果 size 设置为 8,则球将为 16x16 像素。

Ballclass也有一个update方法,每game-loop次迭代调用一次。它将球移动到速度决定的下一个位置,并检查球是否与边界发生碰撞。如果是,则取反相应的速度分量。