介绍
本文档旨在详细介绍基于Python和simpleguitk库开发的桌上冰球游戏。该游戏是一个简单的二维模拟游戏,玩家通过键盘控制球槌,与电脑对手竞技,试图将冰球打入对方球门以得分。
文档目的
提供游戏开发的详细说明,包括游戏设计理念、实现细节、操作指南和代码解析,以便开发者理解、修改和扩展游戏功能。
游戏设计
游戏规则
- 玩家通过键盘上下键控制右侧球槌。
- 目标是将冰球打入对方球门得分。
- 每进一个球得1分,先得10分者获胜。
游戏元素
- 冰球:在游戏场地内移动,与球槌和边界发生碰撞。
- 球槌:玩家和电脑控制,用来击打冰球。
- 得分板:显示当前双方得分。
技术实现
初始化全局变量
定义游戏所需的各种参数,如画布大小、冰球和球槌的半径、得分等。
资源加载
加载游戏所需的图像和音效资源。
辅助函数
- draw_score:绘制得分。
- distance:计算两点之间的距离,用于碰撞检测。
- check_collision:处理冰球与墙壁、球槌的碰撞。
绘图和事件处理
- draw:负责在每个画面更新周期内重绘游戏的状态。
- key_down和key_up:处理键盘事件,控制球槌旋转。
GUI设置和游戏启动
创建游戏窗口,设置绘制函数和键盘事件处理函数,添加“重新开始”按钮,并启动游戏循环。
操作指南
- 使用键盘上的上下键来控制右侧球槌。
- 点击“重新开始”按钮可以重置游戏。
示例代码
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):
canvas.draw_image(score_image[score1], [30, 30], [60, 60], [CANVAS_WIDTH / 2 - 120, 50], [60, 60])
canvas.draw_image(score_image[score2], [30, 30], [60, 60], [CANVAS_WIDTH / 2 + 120, 50], [60, 60])
# 计算两点距离的辅助函数
def distance(p, q):
return math.sqrt((p[0] - q[0]) ** 2 + (p[1] - q[1]) ** 2)
# 碰撞检测辅助函数
def check_collision():
global puck_vel, mallet1_vel, mallet2_vel, score1, score2
collided = False
# 玩家进球
if puck_pos[1] >= CANVAS_HEIGHT / 2 - GATE_RADIUS + PUCK_RADIUS and puck_pos[
1] <= CANVAS_HEIGHT / 2 + GATE_RADIUS - PUCK_RADIUS and puck_vel[0] < 0 and puck_pos[0] <= 0:
score2 += 1
spawn_puck('RIGHT')
goal_sound.rewind()
goal_sound.play()
sleep(2)
return
# 计算机进球
if puck_pos[1] >= CANVAS_HEIGHT / 2 - GATE_RADIUS + PUCK_RADIUS and puck_pos[
1] <= CANVAS_HEIGHT / 2 + GATE_RADIUS - PUCK_RADIUS and puck_vel[0] > 0 and puck_pos[0] >= CANVAS_WIDTH:
score1 += 1
spawn_puck('LEFT')
goal_sound.rewind()
goal_sound.play()
sleep(2)
return
# 碰右壁
if puck_pos[0] >= CANVAS_WIDTH - PUCK_RADIUS - MARGIN_WIDTH + 8 and (
puck_pos[1] <= CANVAS_HEIGHT / 2 - GATE_RADIUS + PUCK_RADIUS or puck_pos[
1] >= CANVAS_HEIGHT / 2 + GATE_RADIUS - PUCK_RADIUS):
puck_vel[0] = -puck_vel[0]
collided = True
# 碰左壁
if puck_pos[0] <= PUCK_RADIUS + MARGIN_WIDTH - 8 and (
puck_pos[1] <= CANVAS_HEIGHT / 2 - GATE_RADIUS + PUCK_RADIUS or puck_pos[
1] >= CANVAS_HEIGHT / 2 + GATE_RADIUS - PUCK_RADIUS):
puck_vel[0] = -puck_vel[0]
collided = True
# 碰下壁
if puck_pos[1] >= CANVAS_HEIGHT - PUCK_RADIUS - MARGIN_WIDTH + 8:
puck_vel[1] = -puck_vel[1]
collided = True
# 碰上壁
if puck_pos[1] <= PUCK_RADIUS + MARGIN_WIDTH - 8:
puck_vel[1] = -puck_vel[1]
collided = True
# 冰球和计算机球槌碰撞
def vector_sub(v1, v2):
return [v1[0] - v2[0], v1[1] - v2[1]]
def vector_dotp(v1, v2):
return v1[0] * v2[0] + v1[1] * v2[1]
def vector_lengths(v):
return v[0] ** 2 + v[1] ** 2
if distance(puck_pos, mallet1_pos) <= PUCK_RADIUS + MALLET_RADIUS - 16:
if mallet1_angle_vel != 0:
theta = math.atan2(mallet1_pos[1] - CANVAS_HEIGHT / 2, mallet1_pos[0]) - math.pi / 2
speed = math.tan(mallet1_angle_vel) * GATE_RADIUS
mallet1_vel[0] = speed * math.cos(theta)
mallet1_vel[1] = speed * math.sin(theta)
pos_sub = vector_sub(puck_pos, mallet1_pos)
tmp = 2 * (vector_dotp(vector_sub(puck_vel, mallet1_vel), pos_sub)) / vector_lengths(pos_sub)
puck_vel[0] -= tmp * pos_sub[0]
puck_vel[1] -= tmp * pos_sub[1]
collided = True
# 冰球和玩家球槌碰撞
if distance(puck_pos, mallet2_pos) <= PUCK_RADIUS + MALLET_RADIUS - 16:
if mallet2_angle_vel != 0:
theta = math.atan2(mallet2_pos[1] - CANVAS_HEIGHT / 2, mallet2_pos[0] - CANVAS_WIDTH) - math.pi / 2
speed = math.tan(mallet2_angle_vel) * GATE_RADIUS
mallet2_vel[0] = speed * math.cos(theta)
mallet2_vel[1] = speed * math.sin(theta)
pos_sub = vector_sub(puck_pos, mallet2_pos)
tmp = 2 * (vector_dotp(vector_sub(puck_vel, mallet2_vel), pos_sub)) / vector_lengths(pos_sub)
puck_vel[0] -= tmp * pos_sub[0]
puck_vel[1] -= tmp * pos_sub[1]
collided = True
# 播放碰撞音效
if collided:
collision_sound.rewind()
collision_sound.play()
# 在球桌中央初始化冰球的位置和速度,方向可以向左或向右
def spawn_puck(direction):
global puck_pos
puck_pos = [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2] # 冰球初始位置
if direction == 'RIGHT':
puck_vel[0] = random.randrange(120, 240) / 60 # 冰球水平速度
if random.randrange(2) == 0:
puck_vel[1] = -random.randrange(60, 180) / 60 # 冰球垂直速度
else:
puck_vel[1] = random.randrange(60, 180) / 60 # 冰球垂直速度
else:
puck_vel[0] = -random.randrange(120, 240) / 60 # 冰球水平速度
if random.randrange(2) == 0:
puck_vel[1] = -random.randrange(60, 180) / 60 # 冰球垂直速度
else:
puck_vel[1] = random.randrange(60, 180) / 60 # 冰球垂直速度
# 初始化全局变量, 也是按钮事件处理函数,用来初始化游戏
def new_game():
global puck_pos, puck_vel, mallet1_vel, mallet2_vel, mallet1_pos, mallet2_pos, mallet1_angle, mallet2_angle
global score1, score2, game_over
puck_pos = [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2]
puck_vel = [0, 0]
mallet1_vel = [0, 0]
mallet1_angle = 0
mallet2_vel = [0, 0]
mallet2_angle = math.pi
mallet1_pos = [GATE_RADIUS, CANVAS_HEIGHT / 2]
mallet2_pos = [CANVAS_WIDTH - GATE_RADIUS, CANVAS_HEIGHT / 2]
score1 = 0
score2 = 0
game_over = False
if random.randrange(0, 2) == 0:
spawn_puck('LEFT')
else:
spawn_puck('RIGHT')
# 主绘制函数
def draw(canvas):
global score1, score2, mallet1_angle, mallet2_angle, mallet1_angle_vel, game_over
# 检测碰撞
check_collision()
# 绘制冰球桌
canvas.draw_image(table, [512, 384], [1024, 768], [CANVAS_WIDTH / 2, CANVAS_HEIGHT / 2],
[CANVAS_WIDTH, CANVAS_HEIGHT])
# 绘制比分
draw_score(canvas, score1, score2)
# 绘制冰球
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])
# 绘制右侧球槌
if mallet2_angle_vel < 0 and mallet2_angle > math.radians(90 + 17):
mallet2_angle += mallet2_angle_vel
if mallet2_angle_vel > 0 and mallet2_angle < math.radians(270 - 17):
mallet2_angle += mallet2_angle_vel
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])
# 绘制游戏结束信息、播放音效
msg = ""
if score1 == 7:
game_over = True
msg = "抱歉,你输了"
if puck_vel[0] != 0:
lose_sound.rewind()
lose_sound.play()
puck_vel[0] = 0
puck_vel[1] = 0
if score2 == 7:
game_over = True
msg = "恭喜,你赢了"
if puck_vel[0] != 0:
win_sound.rewind()
win_sound.play()
puck_vel[0] = 0
puck_vel[1] = 0
if game_over:
msg_width = frame.get_canvas_textwidth(msg, 60, 'sans-serif')
canvas.draw_text(msg, [(CANVAS_WIDTH - msg_width) / 2, CANVAS_HEIGHT / 2 + 130], 60, 'Red',
font_face='sans-serif')
# 键盘事件的处理函数
def key_down(key):
global mallet2_angle_vel
if game_over:
return
if key == gui.KEY_MAP["up"]:
mallet2_angle_vel = -math.pi / 180
elif key == gui.KEY_MAP["down"]:
mallet2_angle_vel = math.pi / 180
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()
结论
本文档提供了桌上冰球游戏的开发细节,旨在帮助感兴趣的开发者和学习者理解和参与游戏的开发过程。通过阅读本文档,读者应能够掌握使用simpleguitk库进行简单游戏开发的基本方法。