桌上冰球
程序说明
游戏主程序的文件名称为AirHockey.py,通过该项目,你可以巩固前三个项目已经学习过的Python语言功能,同时开始接触Python列表(list),学习如何检测碰撞,如何处理反射。
通过该项目你可以获得以下能力:
学习如何在程序中使用列表(list)
深入理解SimpleGUITk刷新屏幕事件的本质
掌握通过刷新屏幕事件实现物体运动的编程技巧
掌握碰撞检测和物体反射的实现方法
增强逻辑思维能力
培养解决问题的能力
编码步骤
我们已经为该小项目提供了一个基本模板,我们建议“桌上冰球”游戏的开发策略为:
1、仔细观察我们建议的一些存储游戏状态的全局变量。你的程序可以直接使用这些变量,如果你不喜欢这些变量,当然你也可以使用自己定义的全局变量。
2、为了能够直接地观察到后续代码的运行效果,建议首先编写绘制画布函数draw(canvas),先在draw函数中添加代码来绘制可以在球桌中运动的冰球,编写冰球位置更新代码。(参见“对象移动”课程视频)
3、在spawn_puck(direction)函数中添加代码使其能在球桌中央生成一个速度暂时固定的冰球,先不要考虑direction参数。
4、在new_game()函数中增加对spawn_puck函数的调用代码来启动桌上冰球游戏。注意项目模板包含对new_game函数的调用以初始化游戏。
5、在check_collision()函数中添加代码来检测冰球是否和球桌的四个边发生碰撞,如果发生碰撞,冰球应当产生反射。请调整冰球的速度(角度)来测试你的代码。
6、在spawn_puck(direction)函数中实现冰球初始速度的随机化功能。如果direction== 'LEFT ',冰球应当向左方移动,如果direction== 'RIGHT ',冰球应当向右方移动。冰球的水平和垂直速度应当由random.randrange()函数生成,对于水平速度,建议范围为random.randrange(120, 240)像素每秒,而垂直速度建议为random.randrange(60, 180)像素每秒。请注意速度的正负号很重要,必须正确设置。
7、在draw函数中添加代码来绘制左侧球槌,左侧球槌由计算机自动控制其运动模式,该球槌应当沿着左侧球门弧线匀速来回移动,注意它不能运动到球桌之外。(参见“对象移动”课程视频)
8、在check_collision()函数中添加代码来检测冰球是否和左侧的球槌发生碰撞,如果发生碰撞,冰球应当产生反射,请按弹性碰撞的物理原理计算碰撞后冰球的运动方向及速度。
9、在draw函数中添加代码来绘制右侧球槌,右侧球槌由玩家控制其运动模式。
10、在key_down键盘事件的处理函数中添加代码来控制右侧球槌的运动。当玩家按住上箭头键时,球槌沿着球门弧线向上运动,当玩家按住下箭头键时,球槌沿着球门弧线向下运动,注意它不能运动到球桌之外。
11、在key_up键盘事件的处理函数中添加代码以实现释放按键时右侧球槌停止运动的功能。
12、在check_collision()函数中添加代码来检测冰球是否和右侧的球槌发生碰撞,如果发生碰撞,冰球应当产生反射,请按弹性碰撞的物理原理计算碰撞后冰球的运动方向及速度。
13、在check_collision()函数中添加代码来检测冰球是否进入球门,如果计算机一方将冰球射入右侧球门,计算机得1分,如果玩家将冰球射入左侧球门,玩家得1分。此时应当调用spawn_puck函数由得分方重新发球。
14、完善draw_score(canvas, score1, score2)函数来绘制比分。
15、在draw函数的合适位置调用draw_score来绘制比分。
16、在draw函数中检测双方的得分,任何一方得分达到7分时比赛结束,一场比赛结束后,应当在画布中央显示输赢结果。
17、完善new_game()函数来重新初始化比分等全局变量,然后调用spawn_puck函数生成一个冰球。在控制面板添加“重新开始”按钮,将该按钮的事件处理函数设置为new_game()以初始化游戏。
18、对碰撞、进球、比赛结束等重要状态添加音效。
项目模板
# "桌上冰球"游戏
import simpleguitk as gui
import random
import math
from time import sleep
# 全局变量初始化
CANVAS_WIDTH = 1024 # 画布宽度
CANVAS_HEIGHT = 768 # 画布高度
PUCK_RADIUS = 40 # 冰球半径
MALLET_RADIUS = 47 # 球槌半径
MARGIN_WIDTH = 10 # 桌边宽度
GATE_RADIUS = 200 # 球门弧半径
puck_pos = [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2] # 冰球的初始位置
puck_vel = [0, 0] # 冰球的初始速度
mallet1_angle = 0 # 左侧球槌的角度(以左侧球门中心为原点)
mallet1_angle_vel = math.pi / 180 # 左侧球槌的角速度
mallet1_vel = [0, 0] # 左侧球槌的线速度
mallet1_pos = [GATE_RADIUS, CANVAS_HEIGHT / 2] # 左侧球槌的初始位置
mallet2_angle = math.pi # 右侧球槌的角度(右侧球门中心为原点)
mallet2_angle_vel = 0 # 右侧球槌的角速度
mallet2_vel = [0, 0] # 由侧球槌的线速度
mallet2_pos = [CANVAS_WIDTH - GATE_RADIUS, CANVAS_HEIGHT / 2] # 右侧球槌的初始位置
score1 = 0 # 计算机得分
score2 = 0 # 玩家得分
game_over = False # 一场比赛是否结束
# 加载图片资源
table = gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/table2.png')
puck = gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/puck.png')
mallet = gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/mallet.png')
score_image = [gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/0_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/1_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/2_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/3_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/4_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/5_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/6_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/7_60x60.png')]
# 加载音效资源
collision_sound = gui.load_sound('http://202.201.225.74/video/PythonResoure/ProjectResource/sounds/project4/collision.wav')
goal_sound = gui.load_sound('http://202.201.225.74/video/PythonResoure/ProjectResource/sounds/project4/goal.wav')
lose_sound = gui.load_sound('http://202.201.225.74/video/PythonResoure/ProjectResource/sounds/project4/gameOver.ogg')
win_sound = gui.load_sound('http://202.201.225.74/video/PythonResoure/ProjectResource/sounds/project4/applause.ogg')
# 绘制比分的辅助函数
def draw_score(canvas, score1, score2):
pass
# 计算两点距离的辅助函数
def distance(p,q):
pass
# 碰撞检测辅助函数
def check_collision():
global puck_vel,mallet1_vel, mallet2_vel,score1, score2
collided = False
# 玩家进球
# 计算机进球
# 碰右壁
# 碰左壁
# 碰下壁
# 碰上壁
# 冰球和计算机球槌碰撞
# 冰球和玩家球槌碰撞
# 播放碰撞音效
# 在球桌中央初始化冰球的位置和速度,方向可以向左或向右
def spawn_puck(direction):
pass
# 初始化全局变量, 也是按钮事件处理函数,用来初始化游戏
def new_game():
pass
# 主绘制函数
def draw(canvas):
global score1, score2, mallet1_angle,mallet2_angle,mallet1_angle_vel, game_over
# 检测碰撞
# 绘制冰球桌
# 绘制比分
# 绘制冰球
# 绘制左侧球槌
# 绘制右侧球槌
#绘制游戏结束信息、播放音效
# 键盘事件的处理函数
def key_down(key):
pass
def key_up(key):
pass
# 创建窗口
frame = gui.create_frame("桌上冰球", CANVAS_WIDTH, CANVAS_HEIGHT)
button = frame.add_button('重新开始', new_game, 50)
frame.set_draw_handler(draw)
frame.set_keydown_handler(key_down)
frame.set_keyup_handler(key_up)
# 启动游戏
new_game()
frame.start()
以下为完整代码
# "桌上冰球"游戏
import simpleguitk as gui
import random
import math
from time import sleep
# 全局变量初始化
CANVAS_WIDTH = 1024 # 画布宽度
CANVAS_HEIGHT = 768 # 画布高度
PUCK_RADIUS = 40 # 冰球半径
MALLET_RADIUS = 47 # 球槌半径
MARGIN_WIDTH = 10 # 桌边宽度
GATE_RADIUS = 200 # 球门弧半径
puck_pos = [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2] # 冰球的初始位置
puck_vel = [0, 0] # 冰球的初始速度
mallet1_angle = 0 # 左侧球槌的角度(以左侧球门中心为原点)
mallet1_angle_vel = math.pi / 180 # 左侧球槌的角速度
mallet1_vel = [0, 0] # 左侧球槌的线速度
mallet1_pos = [GATE_RADIUS, CANVAS_HEIGHT / 2] # 左侧球槌的初始位置
mallet2_angle = math.pi # 右侧球槌的角度(右侧球门中心为原点)
mallet2_angle_vel = 0 # 右侧球槌的角速度
mallet2_vel = [0, 0] # 由侧球槌的线速度
mallet2_pos = [CANVAS_WIDTH - GATE_RADIUS, CANVAS_HEIGHT / 2] # 右侧球槌的初始位置
score1 = 0 # 计算机得分
score2 = 0 # 玩家得分
game_over = False # 一场比赛是否结束
dire_w = ""
# 加载图片资源
table = gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/table2.png')
puck = gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/puck.png')
mallet = gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/mallet.png')
score_image = [gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/0_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/1_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/2_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/3_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/4_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/5_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/6_60x60.png'),
gui.load_image('http://202.201.225.74/video/PythonResoure/ProjectResource/images/project4/7_60x60.png')]
# 加载音效资源
collision_sound = gui.load_sound('http://202.201.225.74/video/PythonResoure/ProjectResource/sounds/project4/collision.wav')
goal_sound = gui.load_sound('http://202.201.225.74/video/PythonResoure/ProjectResource/sounds/project4/goal.wav')
lose_sound = gui.load_sound('http://202.201.225.74/video/PythonResoure/ProjectResource/sounds/project4/gameOver.ogg')
win_sound = gui.load_sound('http://202.201.225.74/video/PythonResoure/ProjectResource/sounds/project4/applause.ogg')
# 绘制比分的辅助函数
def draw_score(canvas, score1, score2):
canvas.draw_image(score_image[score1 % 8], [30, 30], [60, 60], [425, 45], [60, 60])
canvas.draw_image(score_image[score2 % 8], [30, 30], [60, 60], [600, 45], [60, 60])
# 计算两点距离的辅助函数
def distance(p, q):
d = math.sqrt((p[0]-q[0])**2 + (p[1]-q[1])**2)
return d
def locate_dire():
global dire_width, dire_w
if dire_width == 0:
dire_w = "LEFT"
elif dire_width == 1:
dire_w = "RIGHT"
return dire_w
# 碰撞检测辅助函数
def check_collision():
global puck_vel, mallet1_vel, mallet2_vel, score1, score2
collided = False
dis_1 = distance(puck_pos, mallet1_pos)
dis_2 = distance(puck_pos, mallet2_pos)
dis_3 = distance(puck_pos, [CANVAS_WIDTH, CANVAS_HEIGHT / 2])
dis_4 = distance(puck_pos, [0, CANVAS_HEIGHT / 2])
# 玩家进球
if dis_3 < GATE_RADIUS and score1 <= 7:
score2 = score2 + 1
puck_pos[0] = CANVAS_WIDTH / 2
puck_pos[1] = CANVAS_HEIGHT / 2
dire_w1 = locate_dire()
spawn_puck(dire_w1)
goal_sound.play()
# 计算机进球
if dis_4 < GATE_RADIUS and score1 <= 7:
score1 = score1 + 1
puck_pos[0] = CANVAS_WIDTH / 2
puck_pos[1] = CANVAS_HEIGHT / 2
dire_w1 = locate_dire()
spawn_puck(dire_w1)
goal_sound.play()
# 碰右壁
if puck_pos[0] + PUCK_RADIUS + puck_vel[0] >= CANVAS_WIDTH:
puck_vel[0] = - puck_vel[0]
collided = True
# 碰左壁
if puck_pos[0] - PUCK_RADIUS + puck_vel[0] <= 0:
puck_vel[0] = - puck_vel[0]
collided = True
# 碰下壁
if puck_pos[1] + puck_vel[1] + PUCK_RADIUS >= CANVAS_HEIGHT:
puck_vel[1] = - puck_vel[1]
collided = True
# 碰上壁
if puck_pos[1] - PUCK_RADIUS + puck_vel[1] <= 0:
puck_vel[1] = - puck_vel[1]
collided = True
# 冰球和计算机球槌碰撞
if dis_1 <= MALLET_RADIUS + PUCK_RADIUS and dis_1 >= MALLET_RADIUS and puck_pos[0] >= mallet1_pos[0]:
puck_vel[0] = -puck_vel[0]
if dis_1 <= MALLET_RADIUS + PUCK_RADIUS and dis_1 >= MALLET_RADIUS and puck_pos[0] < mallet1_pos[0]:
puck_vel[1] = -puck_vel[1]
collided = True
# 冰球和玩家球槌碰撞
if dis_2 <= MALLET_RADIUS + PUCK_RADIUS and dis_2 >= MALLET_RADIUS and puck_pos[0] >= mallet2_pos[0]:
puck_vel[1] = -puck_vel[1]
if dis_2 <= MALLET_RADIUS + PUCK_RADIUS and dis_2 >= MALLET_RADIUS and puck_pos[0] < mallet2_pos[0]:
puck_vel[0] = -puck_vel[0]
collided = True
puck_pos[1] = puck_pos[1] + puck_vel[1]
puck_pos[0] = puck_pos[0] + puck_vel[0]
# 播放碰撞音效
if collided:
collision_sound.play()
# 在球桌中央初始化冰球的位置和速度,方向可以向左或向右
def spawn_puck(direction):
pass
global puck_pos, puck_vel
if dire_height == 0:
puck_vel[1] = random.randrange(3, 6)
else:
puck_vel[1] = -random.randrange(3, 6)
if direction == "LEFT":
puck_vel[0] = -random.randrange(5, 8)
elif direction == "RIGHT":
puck_vel[0] = random.randrange(5, 8)
# 初始化全局变量, 也是按钮事件处理函数,用来初始化游戏
def new_game():
global dire_width, dire_height, score1, score2, game_over,mallet1_angle_vel,mallet1_angle,\
puck_pos,puck_vel,mallet1_vel,mallet1_pos,mallet2_angle,mallet2_angle_vel,mallet2_vel,mallet2_pos
puck_vel = [0, 0] # 冰球的初始速度
mallet1_angle_vel = math.pi / 180 # 左侧球槌的角速度
puck_pos = [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2] # 冰球的初始位置
mallet1_angle = 0 # 左侧球槌的角度(以左侧球门中心为原点)
mallet1_vel = [0, 0] # 左侧球槌的线速度
mallet1_pos = [GATE_RADIUS, CANVAS_HEIGHT / 2] # 左侧球槌的初始位置
mallet2_angle = math.pi # 右侧球槌的角度(右侧球门中心为原点)
mallet2_angle_vel = 0 # 右侧球槌的角速度
mallet2_vel = [0, 0] # 右侧球槌的线速度
mallet2_pos = [CANVAS_WIDTH - GATE_RADIUS, CANVAS_HEIGHT / 2] # 右侧球槌的初始位置
score1 = 0 # 计算机得分
score2 = 0 # 玩家得分
dire_width = random.randrange(-1, 2)
dire_height = random.randrange(-1, 2)
direction_w = locate_dire()
spawn_puck(direction_w)
game_over = False
# 主绘制函数
def draw(canvas):
global score1, score2, mallet1_angle, mallet2_angle, mallet1_angle_vel, game_over,mallet2_angle_vel
# 检测碰撞
check_collision()
# 绘制冰球桌
canvas.draw_image(table, [512, 384], [1024, 768], [512, 384], [1024, 768])
# 绘制比分
draw_score(canvas, score2, score1)
# 绘制冰球
puck_pos[0] += puck_vel[0]
puck_pos[1] += puck_vel[1]
canvas.draw_image(puck, [128, 128], [256, 256], puck_pos, [PUCK_RADIUS * 2, PUCK_RADIUS * 2])
# 绘制左侧球槌
mallet1_angle += mallet1_angle_vel
if mallet1_angle_vel > 0 and mallet1_angle > math.radians(90-17):
mallet1_angle_vel = - mallet1_angle_vel
if mallet1_angle_vel < 0 and mallet1_angle < math.radians(-90+17):
mallet1_angle_vel = - mallet1_angle_vel
mallet1_pos[0] = GATE_RADIUS * math.cos(mallet1_angle)
mallet1_pos[1] = CANVAS_HEIGHT / 2 - GATE_RADIUS * math.sin(mallet1_angle)
canvas.draw_image(mallet, [47, 47], [94, 94], mallet1_pos, [MALLET_RADIUS * 2, MALLET_RADIUS * 2])
# 绘制右侧球槌
mallet2_pos[0] = CANVAS_WIDTH + GATE_RADIUS * math.cos(mallet2_angle)
mallet2_pos[1] = CANVAS_HEIGHT / 2 + GATE_RADIUS * math.sin(mallet2_angle)
canvas.draw_image(mallet, [47, 47], [94, 94], mallet2_pos, [MALLET_RADIUS * 2, MALLET_RADIUS * 2])
# 绘制游戏结束信息、播放音效
if score1 == 7 or score2 == 7:
game_over = True
win_sound.play()
if game_over:
canvas.draw_text("游戏结束,重新开始", (100, 150), 20, 'Black', 'arial')
# 键盘事件的处理函数
def key_down(key):
global mallet2_angle_vel,mallet2_angle
if key == gui.KEY_MAP["up"]:
mallet2_angle_vel += math.pi / 200
elif key == gui.KEY_MAP["down"]:
mallet2_angle_vel -= math.pi / 200
mallet2_angle += mallet2_angle_vel
if mallet2_angle < math.radians(90 + 17):
mallet2_angle_vel = - mallet2_angle_vel
if mallet2_angle > math.radians(270 - 17):
mallet2_angle_vel = - mallet2_angle_vel
def key_up(key):
global mallet2_angle_vel
mallet2_angle_vel = 0
# 创建窗口
frame = gui.create_frame("桌上冰球", CANVAS_WIDTH, CANVAS_HEIGHT)
button = frame.add_button('重新开始', new_game, 50)
frame.set_draw_handler(draw)
frame.set_keydown_handler(key_down)
frame.set_keyup_handler(key_up)
# 启动游戏
new_game()
frame.start()