20254216 实验四《Python程序设计》实验4报告
课程:《Python程序设计》
班级: 2542
姓名: 李柳烨
学号:20254216
实验教师:王志强
实验日期:2026年6月14日
必修/选修:专选课
一、实验内容
Python综合应用:爬虫、数据处理、可视化、机器学习、神经网络、游戏、网络安全等。
例如:编写从社交网络爬取数据,实现可视化舆情监控或者情感分析。
例如:利用公开数据集,开展图像分类、恶意软件检测等
例如:利用Python库,基于OCR技术实现自动化提取图片中数据,并填入excel中。
例如:爬取天气数据,实现自动化微信提醒
例如:利用爬虫,实现自动化下载网站视频、文件等。
例如:编写小游戏:坦克大战、贪吃蛇、扫雷等等
注:在Windows/Linux系统上使用VIM、PDB、IDLE、Pycharm等工具编程实现。
评分标准:
(1)程序能运行,功能丰富(至少5个功能)。(需求提交源代码,并建议录制程序运行的视频)。
(2)综合实践报告,要体现实验分析、设计、实现过程、结果等信息,格式规范,逻辑清晰,结构合理。
(3)在实践报告中,需要对全课进行总结,并写课程感想体会、意见和建议等。
二、实验过程
源代码
点击查看代码
import tkinter as tk
import random
import math
import time
# =========================
# 基本设置
# =========================
WIDTH = 1000
HEIGHT = 650
FRAME_DELAY = 16
BG_COLOR = "#1e1e1e"
PLAYER_SPEED = 5
PLAYER_RADIUS = 18
MONSTER_RADIUS = 16
BULLET_RADIUS = 5
COIN_RADIUS = 7
CHEST_SIZE = 32
# =========================
# 工具函数
# =========================
def distance(x1, y1, x2, y2):
return math.hypot(x1 - x2, y1 - y2)
def normalize(dx, dy):
length = math.hypot(dx, dy)
if length == 0:
return 0, 0
return dx / length, dy / length
def clamp(value, low, high):
return max(low, min(high, value))
# =========================
# 武器系统
# =========================
WEAPONS = {
"手枪": {
"damage": 25,
"cooldown": 0.28,
"bullet_speed": 12,
"bullet_count": 1,
"spread": 0,
"color": "#ffff66"
},
"散弹枪": {
"damage": 18,
"cooldown": 0.75,
"bullet_speed": 10,
"bullet_count": 5,
"spread": 28,
"color": "#ff9933"
},
"步枪": {
"damage": 15,
"cooldown": 0.11,
"bullet_speed": 14,
"bullet_count": 1,
"spread": 3,
"color": "#66ccff"
},
"狙击枪": {
"damage": 80,
"cooldown": 1.1,
"bullet_speed": 20,
"bullet_count": 1,
"spread": 0,
"color": "#ff66ff"
},
}
class Player:
def __init__(self, canvas):
self.canvas = canvas
self.x = WIDTH // 2
self.y = HEIGHT // 2
self.hp = 100
self.max_hp = 100
self.gold = 0
self.score = 0
self.weapons = ["手枪"]
self.weapon_index = 0
self.last_shot_time = 0
self.item = canvas.create_oval(
self.x - PLAYER_RADIUS,
self.y - PLAYER_RADIUS,
self.x + PLAYER_RADIUS,
self.y + PLAYER_RADIUS,
fill="#4caf50",
outline="white",
width=2
)
self.gun_line = canvas.create_line(
self.x,
self.y,
self.x + 30,
self.y,
fill="white",
width=3
)
@property
def weapon_name(self):
return self.weapons[self.weapon_index]
@property
def weapon(self):
return WEAPONS[self.weapon_name]
def switch_weapon(self):
if len(self.weapons) > 1:
self.weapon_index = (self.weapon_index + 1) % len(self.weapons)
def add_weapon(self, name):
if name not in self.weapons:
self.weapons.append(name)
self.weapon_index = self.weapons.index(name)
def move(self, keys):
dx = 0
dy = 0
if "w" in keys or "W" in keys:
dy -= PLAYER_SPEED
if "s" in keys or "S" in keys:
dy += PLAYER_SPEED
if "a" in keys or "A" in keys:
dx -= PLAYER_SPEED
if "d" in keys or "D" in keys:
dx += PLAYER_SPEED
if dx != 0 and dy != 0:
dx *= 0.707
dy *= 0.707
self.x += dx
self.y += dy
self.x = clamp(self.x, PLAYER_RADIUS, WIDTH - PLAYER_RADIUS)
self.y = clamp(self.y, PLAYER_RADIUS, HEIGHT - PLAYER_RADIUS)
def draw(self, mouse_x, mouse_y):
self.canvas.coords(
self.item,
self.x - PLAYER_RADIUS,
self.y - PLAYER_RADIUS,
self.x + PLAYER_RADIUS,
self.y + PLAYER_RADIUS
)
dx, dy = normalize(mouse_x - self.x, mouse_y - self.y)
gun_x = self.x + dx * 34
gun_y = self.y + dy * 34
self.canvas.coords(
self.gun_line,
self.x,
self.y,
gun_x,
gun_y
)
def can_shoot(self):
now = time.time()
return now - self.last_shot_time >= self.weapon["cooldown"]
def shoot(self, target_x, target_y):
if not self.can_shoot():
return []
self.last_shot_time = time.time()
weapon = self.weapon
bullets = []
base_angle = math.atan2(target_y - self.y, target_x - self.x)
bullet_count = weapon["bullet_count"]
if bullet_count == 1:
angles = [base_angle]
else:
spread_rad = math.radians(weapon["spread"])
start = base_angle - spread_rad / 2
step = spread_rad / max(1, bullet_count - 1)
angles = [start + i * step for i in range(bullet_count)]
for angle in angles:
if weapon["spread"] > 0 and bullet_count == 1:
angle += math.radians(random.uniform(-weapon["spread"], weapon["spread"]))
vx = math.cos(angle) * weapon["bullet_speed"]
vy = math.sin(angle) * weapon["bullet_speed"]
bullets.append(
Bullet(
self.canvas,
self.x,
self.y,
vx,
vy,
weapon["damage"],
weapon["color"]
)
)
return bullets
class Bullet:
def __init__(self, canvas, x, y, vx, vy, damage, color):
self.canvas = canvas
self.x = x
self.y = y
self.vx = vx
self.vy = vy
self.damage = damage
self.alive = True
self.item = canvas.create_oval(
self.x - BULLET_RADIUS,
self.y - BULLET_RADIUS,
self.x + BULLET_RADIUS,
self.y + BULLET_RADIUS,
fill=color,
outline=""
)
def update(self):
self.x += self.vx
self.y += self.vy
if self.x < -20 or self.x > WIDTH + 20 or self.y < -20 or self.y > HEIGHT + 20:
self.alive = False
self.draw()
def draw(self):
self.canvas.coords(
self.item,
self.x - BULLET_RADIUS,
self.y - BULLET_RADIUS,
self.x + BULLET_RADIUS,
self.y + BULLET_RADIUS
)
def remove(self):
self.alive = False
self.canvas.delete(self.item)
class Monster:
def __init__(self, canvas, x, y, wave):
self.canvas = canvas
self.x = x
self.y = y
self.max_hp = 40 + wave * 10
self.hp = self.max_hp
self.speed = 1.2 + wave * 0.08
self.damage = 8 + wave * 1
self.alive = True
self.last_attack_time = 0
self.item = canvas.create_oval(
self.x - MONSTER_RADIUS,
self.y - MONSTER_RADIUS,
self.x + MONSTER_RADIUS,
self.y + MONSTER_RADIUS,
fill="#cc3333",
outline="white",
width=2
)
self.hp_bar_bg = canvas.create_rectangle(
self.x - 18,
self.y - 26,
self.x + 18,
self.y - 21,
fill="#333333",
outline=""
)
self.hp_bar = canvas.create_rectangle(
self.x - 18,
self.y - 26,
self.x + 18,
self.y - 21,
fill="#00ff00",
outline=""
)
def update(self, player):
dx, dy = normalize(player.x - self.x, player.y - self.y)
self.x += dx * self.speed
self.y += dy * self.speed
if distance(self.x, self.y, player.x, player.y) < MONSTER_RADIUS + PLAYER_RADIUS:
now = time.time()
if now - self.last_attack_time > 0.8:
player.hp -= self.damage
self.last_attack_time = now
self.draw()
def take_damage(self, amount):
self.hp -= amount
if self.hp <= 0:
self.alive = False
def draw(self):
self.canvas.coords(
self.item,
self.x - MONSTER_RADIUS,
self.y - MONSTER_RADIUS,
self.x + MONSTER_RADIUS,
self.y + MONSTER_RADIUS
)
self.canvas.coords(
self.hp_bar_bg,
self.x - 18,
self.y - 26,
self.x + 18,
self.y - 21
)
ratio = max(0, self.hp / self.max_hp)
self.canvas.coords(
self.hp_bar,
self.x - 18,
self.y - 26,
self.x - 18 + 36 * ratio,
self.y - 21
)
def remove(self):
self.alive = False
self.canvas.delete(self.item)
self.canvas.delete(self.hp_bar_bg)
self.canvas.delete(self.hp_bar)
class Coin:
def __init__(self, canvas, x, y, value=1):
self.canvas = canvas
self.x = x
self.y = y
self.value = value
self.alive = True
self.item = canvas.create_oval(
x - COIN_RADIUS,
y - COIN_RADIUS,
x + COIN_RADIUS,
y + COIN_RADIUS,
fill="#ffd700",
outline="white",
width=1
)
def check_pickup(self, player):
if distance(self.x, self.y, player.x, player.y) < PLAYER_RADIUS + COIN_RADIUS:
player.gold += self.value
player.score += self.value * 5
self.remove()
def remove(self):
self.alive = False
self.canvas.delete(self.item)
class Chest:
def __init__(self, canvas, x, y, cost=8):
self.canvas = canvas
self.x = x
self.y = y
self.cost = cost
self.opened = False
self.item = canvas.create_rectangle(
x,
y,
x + CHEST_SIZE,
y + CHEST_SIZE,
fill="#8b5a2b",
outline="#ffd700",
width=3
)
self.lock = canvas.create_rectangle(
x + 11,
y + 11,
x + 21,
y + 23,
fill="#ffd700",
outline=""
)
self.text = canvas.create_text(
x + CHEST_SIZE // 2,
y - 12,
fill="white",
font=("Arial", 11),
text=f"{cost}金币"
)
def rect_center(self):
return self.x + CHEST_SIZE / 2, self.y + CHEST_SIZE / 2
def can_open(self, player):
cx, cy = self.rect_center()
return (
not self.opened
and player.gold >= self.cost
and distance(cx, cy, player.x, player.y) < 55
)
def open(self, player):
if not self.can_open(player):
return None
self.opened = True
player.gold -= self.cost
possible = ["散弹枪", "步枪", "狙击枪"]
weapon = random.choice(possible)
player.add_weapon(weapon)
self.canvas.itemconfig(self.item, fill="#444444", outline="#999999")
self.canvas.itemconfig(self.lock, state="hidden")
self.canvas.itemconfig(self.text, text=f"获得:{weapon}", fill="#00ffcc")
return weapon
class Game:
def __init__(self, root):
self.root = root
self.root.title("射击打怪 - tkinter版")
self.canvas = tk.Canvas(
root,
width=WIDTH,
height=HEIGHT,
bg=BG_COLOR
)
self.canvas.pack()
self.keys = set()
self.mouse_x = WIDTH // 2
self.mouse_y = HEIGHT // 2
self.mouse_down = False
self.root.bind("<KeyPress>", self.key_down)
self.root.bind("<KeyRelease>", self.key_up)
self.canvas.bind("<Motion>", self.mouse_move)
self.canvas.bind("<ButtonPress-1>", self.mouse_press)
self.canvas.bind("<ButtonRelease-1>", self.mouse_release)
self.reset()
def reset(self):
self.canvas.delete("all")
self.player = Player(self.canvas)
self.bullets = []
self.monsters = []
self.coins = []
self.chests = []
self.wave = 1
self.monsters_to_spawn = 5
self.spawned_this_wave = 0
self.last_spawn_time = 0
self.game_over = False
self.paused = False
self.message = ""
self.create_map()
self.create_ui()
self.game_loop()
def create_map(self):
# 地图边界装饰
self.canvas.create_rectangle(
5,
5,
WIDTH - 5,
HEIGHT - 5,
outline="#555555",
width=3
)
# 障碍物,仅作为装饰和走位参考,当前版本不做障碍碰撞
self.canvas.create_rectangle(
150,
120,
280,
150,
fill="#333333",
outline="#777777"
)
self.canvas.create_rectangle(
720,
420,
860,
450,
fill="#333333",
outline="#777777"
)
self.canvas.create_rectangle(
440,
270,
560,
305,
fill="#333333",
outline="#777777"
)
# 初始宝箱
self.chests.append(Chest(self.canvas, 80, 80, cost=6))
self.chests.append(Chest(self.canvas, 870, 90, cost=10))
self.chests.append(Chest(self.canvas, 820, 540, cost=14))
def create_ui(self):
self.ui_text = self.canvas.create_text(
15,
15,
anchor="nw",
fill="white",
font=("Arial", 15),
text=""
)
self.tip_text = self.canvas.create_text(
WIDTH // 2,
HEIGHT - 25,
anchor="center",
fill="#cccccc",
font=("Arial", 13),
text="WASD移动 | 鼠标瞄准 | 左键射击 | E开宝箱 | Q切换武器 | P暂停 | R重开"
)
self.center_text = self.canvas.create_text(
WIDTH // 2,
HEIGHT // 2,
anchor="center",
fill="white",
font=("Arial", 34, "bold"),
text=""
)
# =========================
# 输入
# =========================
def key_down(self, event):
key = event.keysym
self.keys.add(key)
if key.lower() == "r":
self.reset()
return
if key.lower() == "p":
self.paused = not self.paused
return
if key.lower() == "q":
self.player.switch_weapon()
if key.lower() == "e":
self.try_open_chest()
def key_up(self, event):
self.keys.discard(event.keysym)
def mouse_move(self, event):
self.mouse_x = event.x
self.mouse_y = event.y
def mouse_press(self, event):
self.mouse_down = True
self.mouse_x = event.x
self.mouse_y = event.y
def mouse_release(self, event):
self.mouse_down = False
# =========================
# 游戏逻辑
# =========================
def try_open_chest(self):
if self.game_over or self.paused:
return
for chest in self.chests:
weapon = chest.open(self.player)
if weapon:
self.message = f"你从宝箱中获得了:{weapon}"
return
self.message = "靠近宝箱并拥有足够金币才能打开"
def spawn_monster(self):
side = random.choice(["top", "bottom", "left", "right"])
if side == "top":
x = random.randint(20, WIDTH - 20)
y = -20
elif side == "bottom":
x = random.randint(20, WIDTH - 20)
y = HEIGHT + 20
elif side == "left":
x = -20
y = random.randint(20, HEIGHT - 20)
else:
x = WIDTH + 20
y = random.randint(20, HEIGHT - 20)
self.monsters.append(Monster(self.canvas, x, y, self.wave))
def update_spawning(self):
if self.spawned_this_wave >= self.monsters_to_spawn:
return
now = time.time()
spawn_interval = max(0.35, 1.0 - self.wave * 0.05)
if now - self.last_spawn_time >= spawn_interval:
self.spawn_monster()
self.spawned_this_wave += 1
self.last_spawn_time = now
def next_wave_if_needed(self):
if (
self.spawned_this_wave >= self.monsters_to_spawn
and len(self.monsters) == 0
):
self.wave += 1
self.monsters_to_spawn = 5 + self.wave * 2
self.spawned_this_wave = 0
self.last_spawn_time = 0
# 每两波生成一个新宝箱
if self.wave % 2 == 0:
x = random.randint(80, WIDTH - 120)
y = random.randint(80, HEIGHT - 120)
cost = 6 + self.wave * 2
self.chests.append(Chest(self.canvas, x, y, cost))
self.message = f"第 {self.wave} 波怪物来了!"
def update_bullets(self):
for bullet in self.bullets[:]:
bullet.update()
if not bullet.alive:
bullet.remove()
self.bullets.remove(bullet)
def update_monsters(self):
for monster in self.monsters[:]:
monster.update(self.player)
if not monster.alive:
self.kill_monster(monster)
def kill_monster(self, monster):
monster.remove()
if monster in self.monsters:
self.monsters.remove(monster)
self.player.score += 20
drop_count = random.randint(1, 3)
for _ in range(drop_count):
x = monster.x + random.randint(-12, 12)
y = monster.y + random.randint(-12, 12)
self.coins.append(Coin(self.canvas, x, y, value=1))
def check_bullet_monster_collision(self):
for bullet in self.bullets[:]:
for monster in self.monsters[:]:
if distance(bullet.x, bullet.y, monster.x, monster.y) < BULLET_RADIUS + MONSTER_RADIUS:
monster.take_damage(bullet.damage)
bullet.remove()
if bullet in self.bullets:
self.bullets.remove(bullet)
if not monster.alive:
self.kill_monster(monster)
break
def update_coins(self):
for coin in self.coins[:]:
coin.check_pickup(self.player)
if not coin.alive:
self.coins.remove(coin)
def update_shooting(self):
if self.mouse_down:
new_bullets = self.player.shoot(self.mouse_x, self.mouse_y)
self.bullets.extend(new_bullets)
def check_game_over(self):
if self.player.hp <= 0:
self.player.hp = 0
self.game_over = True
self.message = "你被怪物击败了"
def update_ui(self):
weapon_list = " / ".join(self.player.weapons)
ui = (
f"血量:{int(self.player.hp)}/{self.player.max_hp}\n"
f"金币:{self.player.gold}\n"
f"分数:{self.player.score}\n"
f"波次:{self.wave}\n"
f"当前武器:{self.player.weapon_name}\n"
f"已拥有武器:{weapon_list}\n"
f"{self.message}"
)
self.canvas.itemconfig(self.ui_text, text=ui)
if self.paused and not self.game_over:
self.canvas.itemconfig(self.center_text, text="PAUSED")
elif self.game_over:
self.canvas.itemconfig(
self.center_text,
text="GAME OVER\n按 R 重新开始"
)
else:
self.canvas.itemconfig(self.center_text, text="")
def game_loop(self):
if not self.game_over and not self.paused:
self.player.move(self.keys)
self.player.draw(self.mouse_x, self.mouse_y)
self.update_shooting()
self.update_spawning()
self.update_bullets()
self.update_monsters()
self.check_bullet_monster_collision()
self.update_coins()
self.next_wave_if_needed()
self.check_game_over()
self.update_ui()
self.root.after(FRAME_DELAY, self.game_loop)
if __name__ == "__main__":
root = tk.Tk()
game = Game(root)
root.mainloop()

效果:运行程序后,显示一个1000×650像素的深灰色窗口,标题为“射击打怪 - tkinter版”。窗口内暂无图形元素,但已能响应键盘和鼠标事件。
(2)定义游戏常量与工具函数:
预定义玩家、怪物、子弹、金币、宝箱等对象的尺寸、速度、颜色等常量。编写通用的数学工具函数(距离计算、向量归一化、数值钳位),便于后续碰撞检测和移动控制。

效果:此步骤不产生可视化变化,但为后续所有移动和碰撞逻辑提供了可靠的基础函数。
(3)实现玩家角色及移动控制:
创建Player类,包含位置、生命值、金币、分数等属性。通过canvas绘制圆形代表玩家,并绘制枪口指示线。根据按下的WASD键更新玩家坐标,并利用clamp函数限制移动范围不超出边界。


效果:窗口中央出现一个绿色圆形玩家,周围有白色描边,并带有一条白色短线作为枪口指示器。按下W/A/S/D键时,玩家可平滑移动,且不会移出窗口边界。
(4)实现武器系统与射击机制:
定义字典WEAPONS存储四种枪械的参数(伤害、冷却时间、子弹速度、弹片数量、散射角度、颜色)。Player类中维护已解锁武器列表和当前武器索引。根据鼠标位置计算发射角度,支持单发和散射。射击时生成Bullet对象列表。


效果:按住鼠标左键时,玩家会按当前武器的射速朝鼠标方向发射子弹。手枪为单发黄色子弹;散弹枪一次射出5颗橙色子弹并呈扇形散开;步枪为高速连发蓝色子弹;狙击枪为单发高伤害紫色子弹。子弹飞出边界后自动消失。
(5)实现怪物类与AI追击:
Monster类存储位置、生命值、速度、攻击力。每帧根据玩家位置调用normalize计算单位方向向量,并乘以速度更新坐标。当与玩家距离小于碰撞半径时,按固定间隔减少玩家生命值。同时绘制怪物圆形和血条。


效果:红色圆形怪物从屏幕边缘出生,并持续向玩家移动。当怪物碰到玩家时,玩家生命值减少,且怪物的攻击有0.8秒冷却。怪物头顶显示绿色血条,生命值随波次增加而提升。
(6)实现子弹与碰撞检测:
在游戏主循环中,遍历所有子弹,调用其update方法(移动位置)。然后检测每个子弹与每个怪物的距离,若小于半径和则造成伤害并移除子弹。若怪物生命值归零,则调用击杀函数。

效果:子弹击中怪物时,怪物血条减少;当伤害累积超过怪物生命值时,怪物消失,并掉落金币(1-3枚)。同时玩家得分增加20。子弹命中后立即消失,避免一次穿透多个怪物。
(7)实现金币与宝箱系统:
Coin类在地图上显示金色圆形,玩家接触后增加金币和分数。Chest类绘制棕色箱子,标有开启所需金币数。玩家靠近箱子且拥有足够金币时,按E键可随机获得一种新武器(散弹枪/步枪/狙击枪),并扣除金币。


效果:地图上预置三个棕色宝箱,上方显示所需金币数。当玩家靠近宝箱且金币足够时,按下E键,宝箱变为灰色,文字变为“获得:xx武器”,同时玩家的武器列表增加该武器,并自动切换到新武器。
(8)实现波次管理与游戏主循环:
Game类维护当前波次、待生成怪物数量、已生成数量等。每波怪物全部消灭后,波次+1,怪物数量和强度提升。每两波生成一个新宝箱。在game_loop中按顺序执行:玩家移动、射击、怪物生成、子弹更新、怪物更新、碰撞检测、金币拾取、波次判断、游戏结束检测。


效果:游戏开始时为第1波,每隔约0.8秒生成一个怪物,共5只。消灭所有怪物后自动进入第2波,怪物生命值和速度提升。每隔两波在随机位置生成一个新宝箱。游戏以约60帧(16ms/帧)的速率持续运行,循环流畅。
(9)实现UI显示与游戏状态控制:
使用canvas.create_text显示玩家血量、金币、分数、当前波次、已拥有武器等。支持P键暂停游戏、R键重新开始、Q键切换武器。游戏结束时显示“GAME OVER”提示。


效果:游戏窗口左上角实时显示玩家状态,底部显示操作提示。按下P键时屏幕中央显示“PAUSED”,游戏冻结;再次按P恢复。玩家生命值归零时显示“GAME OVER”并按R可重新开始。按Q键可在已获得的武器间循环切换,枪口颜色随武器变化。


实验结果:【实验录屏】https://www.bilibili.com/video/BV1Z3jG63EiE?vd_source=dbd8f1e7fc8d83e2f981af4c1969fd7e
(老师不好意思,看了群里发的嵌入视频的方法但是试了很多次都不行,只能附上链接啦,需要跳转至B站观看~)
三、程序功能
(1)差异化多武器系统(含散射、射速、弹道参数):
游戏内置了四种可解锁武器(手枪、散弹枪、步枪、狙击枪),每种武器拥有独立的伤害、冷却时间、子弹速度、弹片数量、散射角度和子弹颜色。玩家按 Q 键切换已获得的武器,鼠标按住左键时根据当前武器参数生成对应子弹。发射时根据 bullet_count 和 spread 计算多个角度,生成对应的 Bullet 对象。例如散弹枪一次射出 5 颗呈 28° 扇形散开的子弹,步枪则为 3° 微散射的高速连发。
代码依据:
WEAPONS = {
"手枪": {"damage": 25, "cooldown": 0.28, "bullet_speed": 12, "bullet_count": 1, "spread": 0, "color": "#ffff66"},
"散弹枪": {"damage": 18, "cooldown": 0.75, "bullet_speed": 10, "bullet_count": 5, "spread": 28, "color": "#ff9933"},
"步枪": {"damage": 15, "cooldown": 0.11, "bullet_speed": 14, "bullet_count": 1, "spread": 3, "color": "#66ccff"},
"狙击枪": {"damage": 80, "cooldown": 1.1, "bullet_speed": 20, "bullet_count": 1, "spread": 0, "color": "#ff66ff"},
}
(2)波次动态难度系统(怪物属性与数量随波次增长):
游戏采用无限波次机制。每波需要生成的怪物数量、怪物的生命值、速度、攻击力均随波次线性提升。当本波全部怪物被消灭后自动进入下一波,并在地图随机位置生成新宝箱(每两波一次)。怪物从屏幕四周边界外出生,避免堆叠在玩家身边。波次切换条件:spawned_this_wave >= monsters_to_spawn and len(monsters) == 0。波次提升后重置生成计数器,并显示提示文字。
代码依据:
self.monsters_to_spawn = 5 + self.wave * 2
self.max_hp = 40 + wave * 10
self.speed = 1.2 + wave * 0.08
self.damage = 8 + wave * 1
(3)宝箱经济与武器解锁系统:
地图上分布着宝箱,每个宝箱标有打开所需金币数(初始为6/10/14,后续波次递增)。玩家靠近宝箱并拥有足够金币时,按 E 键即可扣除金币并随机获得一种未拥有的武器(散弹枪、步枪、狙击枪之一)。武器获得后自动加入玩家武器列表并切换至该武器。宝箱生成规则:初始三个固定位置;之后每两波(wave % 2 == 0)在随机空地生成一个新宝箱,价格 6 + wave * 2。金币来源:每个怪物死亡掉落 1~3 枚 Coin,每枚价值 1 金币。
代码依据:
class Chest:
def open(self, player):
if not self.can_open(player): return None
self.opened = True
player.gold -= self.cost
weapon = random.choice(["散弹枪", "步枪", "狙击枪"])
player.add_weapon(weapon)
(4)完整的游戏状态控制(暂停/重开/武器切换):
游戏支持多种键盘指令实现状态切换,并在画布上实时反馈:
P 键:暂停/恢复游戏,屏幕中央显示 “PAUSED”。
R 键:重置整个游戏(清空所有对象,重新从第 1 波开始)。
Q 键:循环切换已拥有的武器。
E 键:尝试开启附近宝箱。
WASD 移动,鼠标按住左键射击。
game_loop 中判断 if not self.game_over and not self.paused 才更新逻辑,否则仅更新 UI 或显示提示文字。
代码依据:
def key_down(self, event):
if key.lower() == "r": self.reset()
if key.lower() == "p": self.paused = not self.paused
if key.lower() == "q": self.player.switch_weapon()
if key.lower() == "e": self.try_open_chest()
(5)动态视觉反馈(枪口指向、怪物血条、子弹颜色):
游戏通过多种图形化元素实时反馈玩家操作和战斗状态:
枪口指示线:玩家到鼠标位置的连线,长度超过玩家半径,随鼠标旋转,颜色与当前武器子弹颜色一
致。
怪物头顶血条:每个怪物上方显示绿色/灰色矩形,宽度随当前生命值比例动态变化。
子弹颜色区分:不同武器发射不同颜色子弹(手枪黄、散弹橙、步枪蓝、狙击紫),便于识别武器类
型。
宝箱视觉变化:未开启时为棕色金边,并显示价格;开启后变为灰色,文字改为获得的武器名称(青
色)。
代码依据:
枪口指示线更新
dx, dy = normalize(mouse_x - self.x, mouse_y - self.y)
gun_x = self.x + dx * 34; gun_y = self.y + dy * 34
self.canvas.coords(self.gun_line, self.x, self.y, gun_x, gun_y)
怪物血条动态宽度
ratio = max(0, self.hp / self.max_hp)
self.canvas.coords(self.hp_bar, self.x-18, self.y-26, self.x-18+36*ratio, self.y-21)
宝箱开启后变色改文字
self.canvas.itemconfig(self.item, fill="#444444", outline="#999999")
self.canvas.itemconfig(self.text, text=f"获得:{weapon}", fill="#00ffcc")
四、实验中的问题和解决
-
子弹与怪物碰撞删除时的列表遍历错误
问题:
在 check_bullet_monster_collision 中,同时遍历 self.bullets[:] 和 self.monsters[:],当子弹命中后执行 bullet.remove()(内部调用 canvas.delete 并设置 alive=False),然后从 self.bullets 中移除。但如果在同一帧内一颗子弹同时命中多个怪物(极近距离散弹枪),可能出现重复删除同一个子弹的异常。
修改:
在碰撞检测命中后立即 break 跳出内层循环,确保每颗子弹每帧最多命中一个怪物。已实现的代码中已有 break,但更安全的做法是使用标志位或额外判断 bullet.alive。
代码:
if bullet.alive and distance(...) < ...:
monster.take_damage(bullet.damage)
bullet.remove() -
玩家移动时对角线速度过快未正确归一化
问题:
代码中虽然对对角线移动乘以了0.707(即 1/√2),但该值写在条件内部,且乘以 PLAYER_SPEED 后又乘了0.707。实际上,在 dx 和 dy 非零时,应该先将 dx, dy 归一化再乘以速度。当前写法在斜向移动时速度约为 5 * 0.707 ≈ 3.535,但水平和垂直速度为5,导致斜向速度比水平慢,体验不符合直觉。
修改:
改为先归一化再乘以速度,保证各方向速度一致。
代码:
if dx != 0 or dy != 0:
dx, dy = normalize(dx, dy)
self.x += dx * PLAYER_SPEED
self.y += dy * PLAYER_SPEED -
重置游戏时旧对象未完全销毁导致内存/性能问题
问题:
reset() 方法调用 self.canvas.delete("all") 删除了所有图形,但 self.bullets、self.monsters、self.coins、self.chests 列表中的对象仍然持有已删除的 canvas item ID。虽然 Python 垃圾回收会处理,但后续如果意外访问这些对象的 item 属性可能导致 TclError。
修改:
重置时显式清空所有游戏对象列表,并重新初始化所有组件。
代码:
def reset(self):
self.canvas.delete("all")
self.bullets.clear()
self.monsters.clear()
self.coins.clear()
self.chests.clear()
然后重新创建 player, ui, map 等 -
暂停时鼠标左键仍会生成子弹
问题:
在 game_loop 中,update_shooting() 仅在 not self.paused 时调用。但鼠标按下事件记录 self.mouse_down = True,暂停期间松开再按下不会立即射击,然而如果暂停前一直按住左键,恢复后可能会残留一次射击。实际上当前逻辑已较为安全,但可进一步优化:暂停时忽略鼠标状态重置。
修改:
在 mouse_press 和 mouse_release 中加入 if self.paused: return,避免暂停时改变射击状态。 -
宝箱生成位置可能与现有宝箱或障碍物重叠
问题:
新宝箱生成时仅使用 random.randint(80, WIDTH-120) 等边界限制,未检测是否与其他宝箱、障碍物或玩家位置重叠,可能导致宝箱重叠或无法正常交互。
修改:
添加简单的碰撞检测,当新宝箱与任何已有宝箱距离小于 CHEST_SIZE*2 时重新随机位置。
代码:
def is_position_valid(x, y, existing_chests):
for c in existing_chests:
if distance(x + CHEST_SIZE/2, y + CHEST_SIZE/2, c.x + CHEST_SIZE/2, c.y + CHEST_SIZE/2) < CHEST_SIZE * 1.5:
return False
return True
五、程序目前的不足
-
障碍物无碰撞,仅为视觉装饰
地图中绘制的三个灰色矩形没有碰撞检测,玩家和怪物可以直接穿过,降低了战术走位的策略性。缺少障碍物也使得怪物追击过于简单。
改进方向:
为障碍物添加矩形碰撞检测,玩家和怪物不能穿越,可以形成卡位和躲避的玩法。 -
缺少音效和视觉特效
游戏没有任何声音反馈(射击、受伤、开宝箱、捡金币等),也缺少命中闪白、受伤红屏等特效,打击感和沉浸感不足。
改进方向:
可使用 pygame.mixer 或 playsound 库添加音效,利用 canvas 的闪烁效果或粒子特效增强反馈。 -
怪物AI过于简单(纯直线追踪)
所有怪物都使用完全相同的追踪算法(向玩家单位移动),没有差异化行为(如远程攻击、自爆、召唤等),后期波次只是数值提升,玩法单一。
改进方向:
增加多种怪物类型,例如:移动缓慢但高血量的坦克型、高速低血的刺客型、会在远处发射子弹的法师型。 -
子弹和怪物的碰撞检测性能随数量下降
当屏幕上子弹和怪物数量较多时(例如步枪连续射击 + 后期30只怪物),O(N*M) 的双层循环碰撞检测会导致帧率下降。
改进方向:
使用空间划分(如网格、四叉树)或简单的先按区域分组检测,减少无效距离计算。 -
缺少存档/读档及难度选择功能
游戏关闭后进度完全丢失,无法保存高分榜或解锁记录。也没有难度选择(怪物成长曲线固定)。
改进方向:
利用 json 或 pickle 保存玩家最高波次、得分等数据;添加难度选项调整怪物成长系数。 -
玩家攻击可无限穿透(步枪/手枪可连续击中多个怪物)
当前子弹每次命中一个怪物后即消失,但手枪和步枪单发子弹只击中一个敌人,散弹枪每颗散弹也只能击中一个。代码实现符合预期,但缺少穿透型武器(如狙击枪应可穿透多个敌人)。
改进方向:
为武器添加 penetration 参数,控制一颗子弹能击中几个怪物。 -
UI 文字更新过于频繁
每帧都调用 update_ui() 重新设置所有文字内容,虽然开销不大,但可改为仅在数值变化时更新。
改进方向:
使用事件驱动或脏标记,只在血量、金币、波次等改变时刷新对应文本。 -
玩家死亡后重置(R键)无法保留分数/金币,挫败感较强
当前 reset() 完全初始化,没有肉鸽元素或继承机制。玩家重新开始会失去一切,容易劝退。
改进方向:
增加“重生”功能:消耗一定金币原地复活,或允许继承部分武器/金币重新开始。 -
多武器切换时没有直观的武器图标或提示
当前仅靠左上角文字显示武器名称,枪口颜色虽有变化但不够明显。新手可能不清楚当前使用何种武器。
改进方向:
在鼠标附近或屏幕角落显示当前武器的小图标,或绘制武器名称的浮动标签。 -
地图大小固定,缺乏动态地形或视野缩放
游戏始终全窗口显示,无法切换地图区域,也没有小地图功能。玩家很容易被怪物包围。
改进方向:
实现摄像机跟随玩家(当玩家靠近边缘时移动背景),或添加可缩放的小地图。
六、总结和感悟
第一章到最后一章节的知识点,就像游戏里的闯关地图,将Python从入门到实战串得明明白白。我最直观的感受是:老师课程内容安排对我们太友好了。前几章语法和序列打底,像搭地基;函数和面向对象开始长骨架;到文件操作、数据库、GUI和爬虫时,就是盖房子了。特别是最后用Web框架做个完整项目,成就感拉满!以前觉得“面向对象”很抽象,但跟着案例走下来,发现就是把代码当乐高搭,好玩又好懂。小小建议就是,如果课时允许,可以多留点时间给第16、17章的实战环节,因为前面章节知识点多,容易走马观花,真正动手写项目时才最消化知识。像爬虫和Pygame这种趣味性强的章节,可以提前布置一些小作品和小游戏,这样就可以边玩边学,更加有意思。老师是一位靠谱的引路人,python这门课是一盏明灯,是我进一步认识电脑,进入编程的启蒙。这门课从不让我觉得编程枯燥,虽然代码运行失败又找不到原因的时候很让人烦恼,但一个个解决后回头看,真的很有成就感!希望后续课程能再多一点综合性练手项目,让学到的知识都“活”起来~ 😄

浙公网安备 33010602011771号