介绍
雅达利(Atari) 是诺兰·布什内尔(Nolan Key Bushnell) 和泰得・都布尼(Ted Dabney)在1972年成立的电脑公司,它是街机、家用电子游戏机和家用电脑的早期拓荒者。《Pong》是雅达利在1972年11月29日推出的一款投币式街机游戏,它是一款模拟乒乓球比赛的2D体育游戏,Pong 来自乒乓球被打击后所发出的声音。Pong 的设计师是 艾伦·奥尔康(Allan Alcorn)。
在游戏中, 玩家能和电脑玩家或另一位人类玩家进行游戏。玩家在此游戏中需要控制乒乓球拍上下移动来反弹乒乓球。当玩家未能反弹乒乓球的话,对方就会得到一分。玩家在此游戏的目的就是尽量反弹乒乓球并夺取高分以击败对手。
接下来,我们使用Python 模仿 Pong 游戏实现两位人类玩家控制乒乓球拍上下移动反弹乒乓球对战。
开发环境
- Visual Studio Code
- Python3.7
Turtle(海龟绘图) 模块简介
Turtle 库是 Python 语言中一个直观有趣的图形绘制函数库,在海龟绘图中,我们可以通过指令让一只带着钢笔的虚拟的海龟在屏幕上移动,它爬行的轨迹成为了所绘制的图形。
绘图坐标系统
在使用 Turtle 模块之前,我们先简单地认识海龟绘图的坐标系统。Turtle 模块的坐标系统和平时在数学课程上的笛卡尔坐标系一样,绘图界面的中心点为坐标原点(0,0),越往右边 x 坐标坐标越大,越往上边 y 坐标越大,如图:
使用海龟绘制图形
在 Visual Studio Code 上新建一个 名为 my_turtle.py 的Python 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import turtle
screen = turtle.Screen()
screen.setup(500, 400)
myTurtle = turtle.Turtle()
myTurtle.forward(150)
myTurtle.left(90)
myTurtle.forward(75)
screen.exitonclick()
|
import turtle : 导入 turtle 模块,本示例中使用 turtle 模块的 Screen() 以及 Turtle() 两个方法;
screen = turtle.Screen(): 利用 turtle 模块的 Screen() 方法产生一个Screen对象,并赋值给 screen 变量;
screen.setup(500, 400) :利用Screen 对象的 setup() 方法设定绘图屏幕宽为500像素,长为400 像素;
myTurtle = turtle.Turtle(): 利用 turtle 模块的 Turtle() 方法产生一个 Turtle 对象并赋值给变量 myTurtle
myTurtle.forward(150) : 调用forward() 方法向前(沿着x坐标向右)移动150个像素距离;
myTurtle.left(90): 调用 left() 方法让海龟左转90 度
注意:turtle.Turtle() 乌龟 对象创建时,预设方向为面向东方,也就是 沿x坐标右边;
myTurtle.forward(75): 调用forward() 方法向前移动75个像素距离(沿y坐标向上);
screen.exitonclick() : 用户点击程序窗口时,程序退出。
运行 my_turtle.py:Visual Studio Code -> 右键-> Run Python File Terminal
运行效果:
运行代码后,生成了一个 500*400 的程序窗口,小海龟出现在窗口中心,它的预设方向为沿x坐标右边。
小海龟首先向前(沿着x坐标向右)移动150个像素距离,然后左转90 度,最后向前(沿着x坐标向右)移动75个像素距离。
用户点击程序窗口,程序退出。
现在已经使用 turtle 模块作了简单的图形绘制,下面将正式开发 Pong 游戏。
Pong 游戏窗口配置
在 Visual Studio Code 上新建一个 名为 pong.py 的Python 文件
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| import turtle
wn = turtle.Screen()
wn.title("Pong by @MatrixTech")
wn.bgcolor("black")
wn.setup(width=800, height=600)
wn.tracer(0)
while True: wn.update()
|
这里特别注意的是,wn.tracer(0) 使用tracer方法禁止动画显示,然后通过wn.update() 对屏幕进行刷新,让绘制的图形一次性显示在窗口,无需漫长地等待绘制过程。
运行 pong.py:Visual Studio Code -> 右键-> Run Python File Terminal
运行成功后,会出现游戏应用的窗体。
添加球拍和乒乓球
添加球拍 A
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| import turtle
wn = turtle.Screen()
wn.title("Pong by @MatrixTech")
wn.bgcolor("black")
wn.setup(width=800, height=600)
wn.tracer(0)
paddle_a = turtle.Turtle()
paddle_a.speed(0)
paddle_a.shape("square")
paddle_a.color("white")
paddle_a.penup()
paddle_a.goto(-350,0)
while True: wn.update()
|
运行程序,查看效果:
程序运行后,球拍A是一个长和高都是20像素的方块,因此我们需要调整它形状大小。
使用 turtle.shapesize 方法可以调整球拍的形状大小。
turtle.shapesize(stretch_wid=None, stretch_len=None, outline=None)
stretch_wid : 接受正数值,垂直方向拉伸;
stretch_len : 接受正数值,水平方向拉伸;
outline :接受正数值,形状轮廓描边的宽度;
返回或设置画笔的属性 x/y-拉伸因子和/或轮廓。海龟基于拉伸因子调整外观: stretch_wid 为垂直于其朝向的宽度拉伸因子,stretch_len 为水平于其朝向的长度拉伸因子,决定形状轮廓线的粗细。
turtle.shapesize 方法的详细介绍可参阅手册 :
https://docs.python.org/zh-cn/3/library/turtle.html#turtle.shapesize
球拍A 方块原来是20*20 像素,现在垂直方向拉伸5,水平拉伸1,最终长水平方向为20像素,垂直方向为100像素。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ...... ...... ......
paddle_a = turtle.Turtle()
paddle_a.speed(0)
paddle_a.shape("square")
paddle_a.color("white")
paddle_a.shapesize(stretch_wid=5,stretch_len=1)
paddle_a.penup()
paddle_a.goto(-350,0) ...... ...... ......
|
运行,查看效果:
添加球拍 B
球拍B的代码与球拍A的代码相似,因此复制球拍A的代码块,做些许修改即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50
| import turtle
wn = turtle.Screen()
wn.title("Pong by @MatrixTech")
wn.bgcolor("black")
wn.setup(width=800, height=600)
wn.tracer(0)
paddle_a = turtle.Turtle()
paddle_a.speed(0)
paddle_a.shape("square")
paddle_a.color("white") paddle_a.shapesize(stretch_wid=5,stretch_len=1)
paddle_a.penup()
paddle_a.goto(-350,0)
paddle_b = turtle.Turtle()
paddle_b.speed(0)
paddle_b.shape("square")
paddle_b.color("white") paddle_b.shapesize(stretch_wid=5,stretch_len=1)
paddle_b.penup()
paddle_b.goto(350,0)
while True: wn.update()
|
运行程序,查看效果:
添加乒乓球
在原点添加一个方块作乒乓求,添加的方法和上面的球拍添加一样, 代码如下:
注意:由于篇幅所限,不一定显示完整代码,”……” 代表省略的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| ...... ...... ......
paddle_b = turtle.Turtle()
paddle_b.speed(0)
paddle_b.shape("square")
paddle_b.color("white") paddle_b.shapesize(stretch_wid=5,stretch_len=1)
paddle_b.penup()
paddle_b.goto(350,0)
ball = turtle.Turtle() ball.speed(0) ball.shape("square") ball.color("white") ball.penup() ball.goto(0,0)
while True: wn.update()
|
运行代码,查看效果:
实现球拍的移动
步骤
- 创建控制球拍垂直方向上下移动的函数;
- 把移动函数与键盘按键事件绑定,程序监听键盘按键事件;
- 用户按下被绑定的按键,触发移动的函数;
移动球拍 A
球拍A 向上移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ...... ...... ......
ball = turtle.Turtle() ball.speed(0) ball.shape("square") ball.color("white") ball.penup() ball.goto(0,0)
def paddle_a_up(): y = paddle_a.ycor() y += 20 paddle_a.sety(y)
wn.listen()
wn.onkeypress(paddle_a_up,'w')
|
运行程序,查看效果:
球拍A 向下移动
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| ...... ...... ......
def paddle_a_up(): y = paddle_a.ycor() y += 20 paddle_a.sety(y)
def paddle_a_down(): y = paddle_a.ycor() y -= 20 paddle_a.sety(y)
wn.listen()
wn.onkeypress(paddle_a_up,'w')
wn.onkeypress(paddle_a_down,'s')
|
运行程序,查看效果:
移动球拍 B
球拍B的移动实现与球拍A的一样,可以参考球拍A移动的代码。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| ...... ...... ......
def paddle_b_up(): y = paddle_b.ycor() y += 20 paddle_b.sety(y)
def paddle_b_down(): y = paddle_b.ycor() y -= 20 paddle_b.sety(y)
wn.listen()
wn.onkeypress(paddle_a_up,'w')
wn.onkeypress(paddle_a_down,'s')
wn.onkeypress(paddle_b_up,'Up')
wn.onkeypress(paddle_b_down,'Down')
...... ...... ......
|
运行程序,查看效果:
实现乒乓球的移动
程序运行后,在游戏的主循环里不断地按一个坐标增量去更新原先的乒乓球坐标,从而实现乒乓球的移动。
乒乓球的移动
为乒乓球添加坐标的增量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| ...... ...... ......
ball = turtle.Turtle() ball.speed(0) ball.shape("square") ball.color("white") ball.penup() ball.goto(0,0)
ball.dx = 2
ball.dy = 2 ...... ...... ......
|
在游戏主循环中更新乒乓球坐标:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ...... ...... ......
while True: wn.update() ball.setx(ball.xcor() + ball.dx) ball.sety(ball.ycor() + ball.dy) ...... ...... ......
|
如果在添加上述代码后运行程序,就会看到乒乓球向右上角移动然后消失。
我们可以为乒乓球添加边界检测解决其消失的问题,实现当乒乓球碰撞到窗口边界后反弹。
我们先把乒乓球对象的 ball.penup() 注释,让“钢笔”放下,这样海龟移动时候就出现轨迹,方便我们看看它碰撞边界后反弹的轨迹。
上边界检测
注释 ball.penup() :
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ...... ...... ......
ball = turtle.Turtle() ball.speed(0) ball.shape("square") ball.color("white")
ball.goto(0,0)
ball.dx = 2
ball.dy = 2
...... ...... ......
|
在游戏主循环添加上边界检测代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ...... ...... ...... ...... ......
while True: print("x: ",ball.xcor()," y: ",ball.ycor() ) wn.update() ball.setx(ball.xcor() + ball.dx) ball.sety(ball.ycor() + ball.dy)
if ball.ycor() > 290: ball.sety(290) ball.dy *= -1
|
运行程序后,看到效果:
程序启动后,乒乓球从坐标原点(0,0)以增量 (dx=2,dy=2) 往右上角移动。当乒乓球的 y 坐标大于 290 时,乒乓球被被反弹,以增量(dx=2,dy=-2)移动。
下边界检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| ...... ...... ...... ...... ......
while True: print("x: ",ball.xcor()," y: ",ball.ycor() ) wn.update() ball.setx(ball.xcor() + ball.dx) ball.sety(ball.ycor() + ball.dy)
if ball.ycor() > 290: ball.sety(290) ball.dy *= -1 if ball.ycor() < -290: ball.sety(-290) ball.dy *= -1
|
为了查看效果,在运行程序之前,还需要修改乒乓球的坐标增量如下:
1 2 3 4 5 6 7 8 9 10 11
| ball = turtle.Turtle() ball.speed(0) ball.shape("square") ball.color("white")
ball.goto(0,0)
ball.dx = 2
ball.dy = -2
|
把 ball.dy = 2 改成 ball.dy = -2 后,乒乓球在程序运行后会向右下方移动。
运行程序,查看结果:
至此,已经完成了上下边界的检测,可以把 ball.penup() 的注释取消:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| ...... ...... ......
ball = turtle.Turtle() ball.speed(0) ball.shape("square") ball.color("white") ball.penup() ball.goto(0,0)
ball.dx = 2
ball.dy = 2
...... ...... ......
|
左右边界检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30
| ...... ...... ......
while True: ...... ...... ......
if ball.ycor() < -290: ball.sety(-290) ball.dy *= -1
if ball.xcor() > 390: ball.goto(0,0) ball.dx *= -1
if ball.xcor() < -390: ball.goto(0,0) ball.dx *= -1
|
球拍 A 的 y 坐标为 -350,球拍 B 的 y 坐标为 350。
当乒乓球的 y 坐标大于350 时候,说明球拍 B 没有接住乒乓球,球拍 B 失一分,球拍 A 得一分;
反之,当乒乓球的 y 坐标小于 - 350 时候,说明球拍 A 没有接住乒乓球,球拍 A 失一分,球拍 B 得一分;
运行程序,查看结果:
实现球拍和球的碰撞检测
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| ...... ...... ......
while True: ...... ...... ......
if ball.xcor() < -390: ball.goto(0,0) ball.dx *= -1
if (ball.xcor() > 340 and ball.xcor() < 350) and (ball.ycor() < paddle_b.ycor() + 50 and ball.ycor() > paddle_b.ycor() - 50 ): ball.setx(340) ball.dx *= -1
if (ball.xcor() < -340 and ball.xcor() > -350) and (ball.ycor() < paddle_a.ycor() + 50 and ball.ycor() > paddle_a.ycor() - 50 ): ball.setx(-340) ball.dx *= -1
|
运行程序,查看效果:
分数计算
创建 Pen 绘制得分
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29
| ...... ...... ......
ball = turtle.Turtle() ball.speed(0) ball.shape("square") ball.color("white") ball.penup() ball.goto(0,0)
ball.dx = 2
ball.dy = -2
pen = turtle.Turtle() pen.speed(0) pen.color("white") pen.penup()
pen.hideturtle() pen.goto(0,260) pen.write("玩家 A: 0 玩家 B: 0", align="center", font=("Courier",24,"normal"))
...... ...... ......
|
运行程序,查看效果:
计算得分
分别为玩家 A 和玩家 B 添加分数变量:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| ...... ...... ......
pen = turtle.Turtle() pen.speed(0) pen.color("white") pen.penup()
pen.hideturtle() pen.goto(0,260) pen.write("玩家 A: 0 玩家 B: 0", align="center", font=("Courier",24,"normal"))
score_a = 0 score_b = 0
...... ...... ......
|
添加计分逻辑代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39
| ...... ...... ......
while True: ...... ...... ......
if ball.xcor() > 390: ball.goto(0,0) ball.dx *= -1 score_a += 1 pen.clear() pen.write("玩家 A: {} 玩家 B: {}".format(score_a,score_b), align="center", font=("Courier",24,"normal"))
if ball.xcor() < -390: ball.goto(0,0) ball.dx *= -1 score_b += 1 pen.clear() pen.write("玩家 A: {} 玩家 B: {}".format(score_a,score_b), align="center", font=("Courier",24,"normal"))
...... ...... ......
|
运行程序,查看效果:
添加音效
MacOS 系统
如果在 MacOS 上开发 Pong, 可以引入 os 模块使用 sytem 方法调用 afplay 命令播放音效文件。
在顶部引入 os 模块
1 2 3 4
| import turtle
import os
|
在碰撞检测的地方添加音效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| while True: wn.update() ball.setx(ball.xcor() + ball.dx) ball.sety(ball.ycor() + ball.dy)
if ball.ycor() > 290: ball.sety(290) ball.dy *= -1 os.system("afplay bounce.wav&")
if ball.ycor() < -290: ball.sety(-290) ball.dy *= -1 os.system("afplay bounce.wav&")
if ball.xcor() > 390: ball.goto(0,0) ball.dx *= -1 score_a += 1 pen.clear() pen.write("玩家 A: {} 玩家 B: {}".format(score_a,score_b), align="center", font=("Courier",24,"normal"))
if ball.xcor() < -390: ball.goto(0,0) ball.dx *= -1 score_b += 1 pen.clear() pen.write("玩家 A: {} 玩家 B: {}".format(score_a,score_b), align="center", font=("Courier",24,"normal"))
if (ball.xcor() > 340 and ball.xcor() < 350) and (ball.ycor() < paddle_b.ycor() + 50 and ball.ycor() > paddle_b.ycor() - 50 ): ball.setx(340) ball.dx *= -1 os.system("afplay bounce.wav&")
if (ball.xcor() < -340 and ball.xcor() > -350) and (ball.ycor() < paddle_a.ycor() + 50 and ball.ycor() > paddle_a.ycor() - 50 ): ball.setx(-340) ball.dx *= -1 os.system("afplay bounce.wav&")
|
Windows 系统
如果在 Windows 系统下开发 Pong ,可以使用 winsound 模块播放 音效文件。
在源码文件顶部引入 winsound 模块
1 2 3 4 5 6 7 8
| import turtle
...... ...... ......
|
在碰撞检测的地方添加音效:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71
| ...... ...... ......
while True: wn.update() ball.setx(ball.xcor() + ball.dx) ball.sety(ball.ycor() + ball.dy)
if ball.ycor() > 290: ball.sety(290) ball.dy *= -1
if ball.ycor() < -290: ball.sety(-290) ball.dy *= -1
if ball.xcor() > 390: ball.goto(0,0) ball.dx *= -1 score_a += 1 pen.clear() pen.write("玩家 A: {} 玩家 B: {}".format(score_a,score_b), align="center", font=("Courier",24,"normal"))
if ball.xcor() < -390: ball.goto(0,0) ball.dx *= -1 score_b += 1 pen.clear() pen.write("玩家 A: {} 玩家 B: {}".format(score_a,score_b), align="center", font=("Courier",24,"normal"))
if (ball.xcor() > 340 and ball.xcor() < 350) and (ball.ycor() < paddle_b.ycor() + 50 and ball.ycor() > paddle_b.ycor() - 50 ): ball.setx(340) ball.dx *= -1
if (ball.xcor() < -340 and ball.xcor() > -350) and (ball.ycor() < paddle_a.ycor() + 50 and ball.ycor() > paddle_a.ycor() - 50 ): ball.setx(-340) ball.dx *= -1
|
源代码
源代码地址 : https://github.com/matrixtechxyz/Pong
参考资源
Comments