
Pong游戏是数字娱乐史上最早、最具标志性的游戏之一。在其经典版本中,游戏模拟了一场乒乓球比赛,两个球拍沿屏幕垂直移动,击打一个弹跳的球。每位玩家控制一个球拍,需将球击回给对方,否则将给对方加一分。
这款游戏的历史颇具趣味性。Pong游戏由艾伦·奥尔康(Allan Alcom)在被雅达利(Atari)公司聘用时,作为测试项目设计编写。随后这款游戏大获成功,在全球的酒吧和酒馆售出了大量游戏机——有趣的是,这些游戏机常常因人们投入的大量硬币而堵塞,最终酒吧老板们不得不联系雅达利公司来维修机器!
在本教程中,我们将使用Python的面向对象编程(OOP)方法来编写Pong游戏。这是一个中级水平的Python编程教程,要求学习者具备Python基础知识的初步认知,包括列表、……
理解项目
编写这款游戏有多种方法。我们可以使用直接的方法,逐步执行每个任务并进行必要的重复操作;也可以使用Python的面向对象编程方法,避免重复代码,使代码整洁有序。我们将选择第二种方法,因为这会让游戏程序更系统、更简洁!
我们将使用Python的Turtle模块进行可视化游戏开发。Turtle模块是一个内置功能,能让我们以简单的方式可视化代码。它主要包含一只“海龟”,会根据编码者的指令在屏幕上移动,绘制图形和线条。它是创建入门级游戏的强大工具,能通过可视化屏幕提供即时反馈。
以下是我们将按顺序完成的关键任务:
创建游戏屏幕——这是显示Pong游戏的屏幕。
创建球拍及球拍类(Paddle Class)——这段代码将在屏幕上创建一个球拍,并配置其移动方式;我们将把这段代码转换为一个类,作为创建两个球拍(一个在左侧,一个在右侧)的蓝图。
创建球类(Ball Class)和球对象——继续使用面向对象编程方法,我们将创建一个通用的球类,然后创建在屏幕上移动的球,并定义其相关方法。
检测球与上下墙壁的碰撞——这段代码将检测球与上下墙壁的碰撞,若发生碰撞,使球沿y轴反弹。
检测球与球拍的碰撞——这段代码将检测球是否与球拍碰撞。若碰撞,则使球反弹;若球拍未击中球,则对方玩家得分,球回到屏幕中央重新开始游戏。
创建计分板类(Scoreboard Class)和计分板对象——这段代码包括在单独的Python文件中创建计分板类,并在主游戏文件中创建其对象。
创建游戏屏幕
第一项任务是创建游戏屏幕。该屏幕将为矩形,与真实游戏中的屏幕一致。我们首先在代码中导入turtle模块,使用其Screen类创建屏幕对象,并通过Screen类的setup()方法将其自定义为宽800像素、高600像素。我们将使用bgcolor()方法将背景颜色设置为黑色,通过title()方法将屏幕命名为“Pong Game”。以下是创建屏幕对象的代码:
from turtle import Turtle, Screen
# 设置游戏屏幕
screen = Screen()
screen.setup(width=800, height=600)
screen.bgcolor("black")
screen.title("Pong Game")
screen.exiton click()
注意,我们在最后一行使用了屏幕的exiton click()方法,以确保屏幕在我们点击之前保持显示状态。
如果对上述方法有任何疑问,可随时查看Turtle模块的官方文档,链接在此。
运行程序后,输出如下:

创建球拍及球拍类
下一项任务是创建球拍,即游戏屏幕两侧的矩形对象。我们将使用turtle模块的shape()函数创建这个球拍,通过color()方法将其自定义为白色,并使用shapesize()方法将其自定义为宽20像素、高100像素。注意,我们向shapesize()方法传递了5和1作为参数——这是因为shapesize()的单位不是像素,而是以20像素为基准。因此,要获得100像素的长度,我们需要传递5(因为20像素×5 = 100像素)。此外,我们将其定位在游戏开始时的右侧中间位置,即y坐标为0,x坐标为350(记住我们的屏幕宽800像素)。我们将使用penup()方法清除海龟的移动轨迹,并通过goto()方法将其移动到目标位置。
# 创建球拍
paddle = Turtle()
paddle.shape("square")
paddle.color("white")
paddle.shapesize(5,1)
paddle.penup()
paddle.goto(350,0)
上述代码的输出如下。我们可以看到游戏屏幕右侧创建了一个球拍,且没有海龟的移动轨迹。

运行上述代码将创建球拍,但我们会发现球拍是先被创建,然后才移动到目标位置。为了关闭动画效果,我们将在代码中添加屏幕类的tracer()方法,这也需要我们手动更新屏幕:
# 保留原有代码
screen.tracer(0)
screen.update()
screen.exiton click()
调用tracer()方法并传递0作为参数,将关闭动画效果。
创建球拍并通过关闭动画更新屏幕后,下一步是配置球拍的移动。为此,我们将使用屏幕监听器(screen listeners)。屏幕类的listen()方法允许我们监听键盘事件,onkey()方法允许我们在按下特定按键时调用已定义的函数。因此,我们将定义go_up和go_down两个函数,使球拍沿y轴上下移动。
def go_up():
new_y = paddle.ycor() + 40
paddle.goto(paddle.xcor(), new_y)
def go_down():
new_y = paddle.ycor() - 40
paddle.goto(paddle.xcor(), new_y)
如你所见,我们定义了球拍的上下移动函数,使其从原始位置垂直移动40像素。接下来,我们将使用屏幕监听器功能,让这些函数在按下键盘按键时被调用。
screen.listen()
screen.onkey(paddle.go_up, "Up")
screen.onkey(paddle.go_down, "Down")

现在我们已经创建了球拍并配置了其移动机制,接下来将代码转换为面向对象编程方法。这是因为我们的游戏需要两个球拍,而拥有一个能快速创建球拍的通用蓝图将使我们的任务更轻松。我们将重构代码,以便轻松创建另一个球拍。我们会将所有与球拍相关的代码移到另一个文件中,并在该文件中创建球拍类。
由于我们创建的球拍本质上是海龟对象,因此我们将使这个球拍类继承自Turtle类。因此,我们将在PyCharm IDE中创建一个新的Python文件,并在这个单独的Python文件中再次导入turtle模块的Turtle类。接下来,我们将使用类创建语法和def init()方法定义Paddle类。由于左右两个球拍在游戏屏幕上的位置不同,我们将x坐标和y坐标作为类的属性。
现在我们将使用面向对象编程中的继承概念,将Turtle类作为父类,球拍类将继承其属性和方法。接下来,我们只需将之前创建球拍时代码中的“paddle”替换为“self”关键字即可。
from turtle import Turtle, Screen
class Paddle(Turtle):
def __init__(self,x,y):
# 创建球拍对象
super().__init__()
self.shape("square")
self.color("white")
self.shapesize(5, 1)
self.penup()
self.x = x
self.y = y
self.goto(x,y)
# 配置球拍移动
def go_up(self):
new_y = self.ycor() + 40
self.goto(self.xcor(), new_y)
def go_down(self):
new_y = self.ycor() - 40
self.goto(self.xcor(),new_y)
如上述代码所示,我们还定义了Paddle类的两个方法——一个是向上移动,另一个是向下移动,这两个方法我们之前已经定义过。定义好Paddle类后,我们将创建球拍对象,并配置两个球拍的上下移动:
from paddle import Paddle
# 创建球拍对象
left_paddle = Paddle(-350, 0)
right_paddle = Paddle(350, 0)
# 配置球拍移动
screen.listen()
screen.onkey(right_paddle.go_up, "Up")
screen.onkey(right_paddle.go_down, "Down")
screen.onkey(left_paddle.go_up, "w")
screen.onkey(left_paddle.go_down, "s")
运行游戏
为了运行游戏并使用Screen的update()方法更新游戏,我们将定义一个while循环,该循环将持续运行,直到被外部停止或循环条件变为False。
# 游戏开始
game_is_on = True
while game_is_on:
screen.update()
现在,当你运行主文件时,将看到游戏屏幕和球拍被创建,并且球拍能够移动。
创建球类及球对象
继续使用面向对象编程方法编写这款游戏,我们将创建Ball类作为通用蓝图,并在主Python文件中从中创建球对象。我们将球创建为海龟对象,使Ball类继承自父类Turtle。我们将使用Turtle类的color()和shape()方法,初始化一个白色的圆形球。和之前一样,我们将使用turtle的penup()方法隐藏海龟的移动轨迹。
from turtle import Turtle
class Ball(Turtle):
def __init__(self):
super().__init__()
self.color("white")
self.shape("circle")
self.penup()
定义好球的属性后,我们还将创建球在游戏开始时就移动的方法。游戏开始时,球将位于游戏屏幕的中心,当屏幕刷新时,球将首先向右移动。我们将在主while循环中调用这个方法,这样球在游戏运行期间(即游戏处于开启状态时)将持续移动——也就是说,球的x坐标和y坐标将在每次游戏屏幕刷新时发生变化。
使球移动的方法是将其x坐标和y坐标各改变一定数值,暂时设为10。我们将定义球的move()方法,并编写上述逻辑:
class Ball(Turtle):
# 保留之前的代码
def move(self):
new_x = self.xcor() + 10
new_y = self.ycor() + 10
self.goto(new_x, new_y)
我们将在游戏的while循环中调用球对象的这个方法:
# 游戏开始
game_is_on = True
while game_is_on:
screen.update()
ball.move()
运行代码后,我们会发现球很快消失,只剩下两个球拍。

我们可以通过注释掉screen.tracer()相关代码并重新运行,恢复动画效果。此时我们将看到两个球拍和球被创建并移动。

另一种可视化方法是使用time模块,在游戏的主while循环中添加延迟(无需注释掉tracer()函数),具体操作如下:
import time
# 保留原有代码
# 游戏开始
game_is_on = True
while game_is_on:
time.sleep(0.1)
screen.update()
ball.move()
现在你可以看到球以较慢的速度移动,我们可以用球拍接住它。
检测球与上下墙壁的碰撞
现在我们的球已经创建并开始移动,我们需要设计一个机制,使球撞到上下墙壁时反弹;而对于左右墙壁,球应该被左右球拍接住。如果球未被接住,则意味着对方玩家得分。
因此,假设球从屏幕中心向右上角移动并到达角落,它需要反弹。简单来说,反弹就是沿y轴改变方向,因为球在x轴上仍然会向前移动。现在我们将定义Ball类的一个新方法bounce(),并在球到达边界时在主游戏循环中调用它:
from turtle import Turtle
class Ball(Turtle):
def __init__(self):
super().__init__()
self.color("white")
self.shape("circle")
self.penup()
self.x_move = 10
self.y_move = 10
def move(self):
new_x = self.xcor() + self.x_move
new_y = self.ycor() + self.y_move
self.goto(new_x, new_y)
def bounce(self):
self.y_move *= -1
注意,在上述代码中,我们定义了Ball类的两个新属性x_move和y_move,并将它们设为10。然后,在move()方法中,我们用这些属性替换了之前的数值10。显然,这对我们的bounce()方法很有帮助。现在,每当球反弹时,它将向之前y方向的反方向移动。这意味着,如果球向上移动并撞到墙壁,y_move将从+10变为-10,球将向下移动(因为负数表示球向下移动);因此,撞到下墙壁时,y_move将从-10变为+10,球将向上移动。
现在,我们将这个条件添加到主while循环中:
while game_is_on:
# 保留原有代码
# 检测球与上下墙壁的碰撞
if ball.ycor() > 275 or ball.ycor() < -275:
ball.bounce_y()
在上述代码中,我们添加了检测球与墙壁碰撞的条件,然后调用bounce()方法。你可以为边界设置任何值,但经过多次尝试,275这个值是比较合适的!

检测球与球拍的碰撞
现在我们知道如何让球从上下墙壁反弹,下一步是检测球与球拍的碰撞,并让球从球拍反弹。我们将采用与之前类似的方法,只是这次针对的是x轴。
检测球与墙壁碰撞的常规方法是使用distance方法。如果两者之间的距离小于某个数值,我们可以判定它们已经接触/碰撞。但要注意,distance()函数是通过计算两个海龟对象中心之间的距离来工作的。在我们的案例中,一个是20×20像素的球,另一个是20×200像素的矩形球拍。它们之间的距离会沿球拍的长度变化。如果球撞到球拍的边缘,distance()方法可能无法判定两者已经接触。
我们可以添加另一个条件:检查球是否已经超过x轴上的某个点(以右侧球拍为例),并且与球拍的距离在50像素以内,那么就可以判定球已经接触到球拍。我们将这个条件添加到主while循环中。一旦检测到碰撞,我们将让球反弹,但这次是沿x轴方向。我们将重新定义反弹函数,这样就有两个反弹函数:一个用于与球拍碰撞时沿x轴反弹,另一个用于与墙壁碰撞时沿y轴反弹:
def bounce_y(self):
self.y_move *= -1
def bounce_x(self):
self.x_move *= -1
while game_is_on:
...
# 检测球与右侧球拍的碰撞
if ball.distance(right_paddle) < 50 and ball.xcor() > 320:
ball.bounce_x()
# 检测球与左侧球拍的碰撞
elif ball.distance(left_paddle) < 50 and ball.xcor() < -320:
ball.bounce_x()
注意,经过多次尝试和球与球拍碰撞的可视化测试,我们设置了320这个数值。

如果其中一个球拍未击中球,那么对方玩家将获得一分,游戏将重新开始,球回到屏幕中心。为了检测球拍是否未击中球,我们可以通过观察球是否超出水平轴上的某个点来判断。我们知道屏幕宽度为800像素,球拍位于x轴的350像素处,因此球拍的实际范围是从340像素到360像素(因为球拍宽20像素),所以如果球超出x轴的360像素,就意味着球拍未击中球。这意味着我们需要将球重置到起始位置(0,0)。我们将定义Ball类的reset_position()方法,在满足上述条件时调用该方法。此外,我们还将添加一个功能:让球反向移动,即从向右移动改为向左移动。
class Ball(Turtle):
...
def reset_position(self):
self.goto(0, 0)
self.bounce_x()
bounce_x()方法将使球像撞到球拍时一样反向移动。将这些条件添加到游戏的主while循环中:
while game_is_on:
...
# 检测右侧球拍未击中球
if ball.xcor() > 380:
ball.reset_position()
# 检测左侧球拍未击中球
if ball.xcor() < -380:
ball.reset_position()
运行上述代码,我们将看到当球拍未击中球时的效果:球将反向移动,朝向另一个球拍。现在,只剩下创建一个计分板来记录和显示每位玩家的得分。
创建计分板
为了显示和更新每位玩家的得分,我们将在一个新的Python文件中定义计分板类(Scoreboard Class)。我们将创建继承自Turtle类的Scoreboard类,并定义帮助海龟对象进行书写的属性。首先,我们将初始化两个属性l_score(左侧玩家得分)和r_score(右侧玩家得分),并在游戏开始时将它们设为0。我们将定义两个方法l_point和r_point,每当有玩家未击中球时调用这两个方法,为对方玩家加分。我们还将定义一个名为update_scoreboard()的方法,在玩家获得额外分数时调用该方法。该方法被调用时,将简单地更新计分板。
以下是计分板类的创建代码:
from turtle import Turtle
class Scoreboard(Turtle):
def __init__(self):
super().__init__()
self.color("white")
self.penup()
self.hideturtle()
self.l_score = 0
self.r_score = 0
self.update_scoreboard()
def update_scoreboard(self):
self.clear()
self.goto(-100, 200)
self.write(self.l_score, align="center", font=("Arial", 40, "normal"))
self.goto(100, 200)
self.write(self.r_score, align="center", font=("Arial", 40, "normal"))
def l_point(self):
self.l_score += 1
self.update_scoreboard()
def r_point(self):
self.r_score += 1
self.update_scoreboard()
update_scoreboard()方法创建一个海龟,在主屏幕上显示两位玩家的得分。注意,我们在这里使用了Turtle模块的write()函数。
接下来,我们将在主文件中导入并创建计分板对象,并使用该对象访问其方法,以满足以下两个条件:每当有玩家的球拍未击中球时,对方玩家将获得一分。
from scoreboard import Scoreboard
# 初始化计分板对象
scoreboard = Scoreboard()
while game_is_on:
...
# 检测右侧球拍未击中球
if ball.xcor() > 380:
ball.reset_position()
scoreboard.l_point()
# 检测左侧球拍未击中球
if ball.xcor() < -380:
ball.reset_position()
scoreboard.r_point()
至此,游戏的设计和编码就完成了。运行主Python文件,将生成游戏屏幕及其组件,游戏开始时球将自动移动。现在,你只需要找一个玩家来一起玩这款游戏!
你也可以通过修改代码来改变游戏速度(这需要你自己去探索!)。
总结
在本文中,我们借助Python的Turtle模块开发了经典的Pong游戏。我们使用面向对象编程的概念创建类、初始化属性和方法,并从这些类中在主游戏文件中创建对象。这是一个中级水平的Python项目,如果你在代码的某个部分遇到困难,请务必参考Python官方文档,或复习你的基础知识,尤其是面向对象编程(OOP)相关知识。
推荐学习书籍 《CDA一级教材》适合CDA一级考生备考,也适合业务及数据分析岗位的从业者提升自我。完整电子版已上线CDA网校,累计已有10万+在读~ !



雷达卡





京公网安备 11010802022788号







