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()
(1)环境搭建与游戏窗口初始化: 首先创建游戏主窗口,设定固定尺寸、背景色和帧刷新延迟。绑定键盘与鼠标事件,为后续交互做准备。

1

效果:运行程序后,显示一个1000×650像素的深灰色窗口,标题为“射击打怪 - tkinter版”。窗口内暂无图形元素,但已能响应键盘和鼠标事件。

(2)定义游戏常量与工具函数:
预定义玩家、怪物、子弹、金币、宝箱等对象的尺寸、速度、颜色等常量。编写通用的数学工具函数(距离计算、向量归一化、数值钳位),便于后续碰撞检测和移动控制。

2

效果:此步骤不产生可视化变化,但为后续所有移动和碰撞逻辑提供了可靠的基础函数。

(3)实现玩家角色及移动控制:
创建Player类,包含位置、生命值、金币、分数等属性。通过canvas绘制圆形代表玩家,并绘制枪口指示线。根据按下的WASD键更新玩家坐标,并利用clamp函数限制移动范围不超出边界。

3

33

效果:窗口中央出现一个绿色圆形玩家,周围有白色描边,并带有一条白色短线作为枪口指示器。按下W/A/S/D键时,玩家可平滑移动,且不会移出窗口边界。

(4)实现武器系统与射击机制:
定义字典WEAPONS存储四种枪械的参数(伤害、冷却时间、子弹速度、弹片数量、散射角度、颜色)。Player类中维护已解锁武器列表和当前武器索引。根据鼠标位置计算发射角度,支持单发和散射。射击时生成Bullet对象列表。

4

44

效果:按住鼠标左键时,玩家会按当前武器的射速朝鼠标方向发射子弹。手枪为单发黄色子弹;散弹枪一次射出5颗橙色子弹并呈扇形散开;步枪为高速连发蓝色子弹;狙击枪为单发高伤害紫色子弹。子弹飞出边界后自动消失。

(5)实现怪物类与AI追击:
Monster类存储位置、生命值、速度、攻击力。每帧根据玩家位置调用normalize计算单位方向向量,并乘以速度更新坐标。当与玩家距离小于碰撞半径时,按固定间隔减少玩家生命值。同时绘制怪物圆形和血条。

5

55

效果:红色圆形怪物从屏幕边缘出生,并持续向玩家移动。当怪物碰到玩家时,玩家生命值减少,且怪物的攻击有0.8秒冷却。怪物头顶显示绿色血条,生命值随波次增加而提升。

(6)实现子弹与碰撞检测:
在游戏主循环中,遍历所有子弹,调用其update方法(移动位置)。然后检测每个子弹与每个怪物的距离,若小于半径和则造成伤害并移除子弹。若怪物生命值归零,则调用击杀函数。

6

效果:子弹击中怪物时,怪物血条减少;当伤害累积超过怪物生命值时,怪物消失,并掉落金币(1-3枚)。同时玩家得分增加20。子弹命中后立即消失,避免一次穿透多个怪物。

(7)实现金币与宝箱系统:
Coin类在地图上显示金色圆形,玩家接触后增加金币和分数。Chest类绘制棕色箱子,标有开启所需金币数。玩家靠近箱子且拥有足够金币时,按E键可随机获得一种新武器(散弹枪/步枪/狙击枪),并扣除金币。

7

77

效果:地图上预置三个棕色宝箱,上方显示所需金币数。当玩家靠近宝箱且金币足够时,按下E键,宝箱变为灰色,文字变为“获得:xx武器”,同时玩家的武器列表增加该武器,并自动切换到新武器。

(8)实现波次管理与游戏主循环:
Game类维护当前波次、待生成怪物数量、已生成数量等。每波怪物全部消灭后,波次+1,怪物数量和强度提升。每两波生成一个新宝箱。在game_loop中按顺序执行:玩家移动、射击、怪物生成、子弹更新、怪物更新、碰撞检测、金币拾取、波次判断、游戏结束检测。

8

88

效果:游戏开始时为第1波,每隔约0.8秒生成一个怪物,共5只。消灭所有怪物后自动进入第2波,怪物生命值和速度提升。每隔两波在随机位置生成一个新宝箱。游戏以约60帧(16ms/帧)的速率持续运行,循环流畅。

(9)实现UI显示与游戏状态控制:
使用canvas.create_text显示玩家血量、金币、分数、当前波次、已拥有武器等。支持P键暂停游戏、R键重新开始、Q键切换武器。游戏结束时显示“GAME OVER”提示。

99

9

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

游戏1

游戏2

实验结果:【实验录屏】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")

四、实验中的问题和解决

  1. 子弹与怪物碰撞删除时的列表遍历错误
    问题:
    在 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()

  2. 玩家移动时对角线速度过快未正确归一化
    问题:
    代码中虽然对对角线移动乘以了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

  3. 重置游戏时旧对象未完全销毁导致内存/性能问题
    问题:
    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 等

  4. 暂停时鼠标左键仍会生成子弹
    问题:
    在 game_loop 中,update_shooting() 仅在 not self.paused 时调用。但鼠标按下事件记录 self.mouse_down = True,暂停期间松开再按下不会立即射击,然而如果暂停前一直按住左键,恢复后可能会残留一次射击。实际上当前逻辑已较为安全,但可进一步优化:暂停时忽略鼠标状态重置。
    修改:
    在 mouse_press 和 mouse_release 中加入 if self.paused: return,避免暂停时改变射击状态。

  5. 宝箱生成位置可能与现有宝箱或障碍物重叠
    问题:
    新宝箱生成时仅使用 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

五、程序目前的不足

  1. 障碍物无碰撞,仅为视觉装饰
    地图中绘制的三个灰色矩形没有碰撞检测,玩家和怪物可以直接穿过,降低了战术走位的策略性。缺少障碍物也使得怪物追击过于简单。
    改进方向:
    为障碍物添加矩形碰撞检测,玩家和怪物不能穿越,可以形成卡位和躲避的玩法。

  2. 缺少音效和视觉特效
    游戏没有任何声音反馈(射击、受伤、开宝箱、捡金币等),也缺少命中闪白、受伤红屏等特效,打击感和沉浸感不足。
    改进方向:
    可使用 pygame.mixer 或 playsound 库添加音效,利用 canvas 的闪烁效果或粒子特效增强反馈。

  3. 怪物AI过于简单(纯直线追踪)
    所有怪物都使用完全相同的追踪算法(向玩家单位移动),没有差异化行为(如远程攻击、自爆、召唤等),后期波次只是数值提升,玩法单一。
    改进方向:
    增加多种怪物类型,例如:移动缓慢但高血量的坦克型、高速低血的刺客型、会在远处发射子弹的法师型。

  4. 子弹和怪物的碰撞检测性能随数量下降
    当屏幕上子弹和怪物数量较多时(例如步枪连续射击 + 后期30只怪物),O(N*M) 的双层循环碰撞检测会导致帧率下降。
    改进方向:
    使用空间划分(如网格、四叉树)或简单的先按区域分组检测,减少无效距离计算。

  5. 缺少存档/读档及难度选择功能
    游戏关闭后进度完全丢失,无法保存高分榜或解锁记录。也没有难度选择(怪物成长曲线固定)。
    改进方向:
    利用 json 或 pickle 保存玩家最高波次、得分等数据;添加难度选项调整怪物成长系数。

  6. 玩家攻击可无限穿透(步枪/手枪可连续击中多个怪物)
    当前子弹每次命中一个怪物后即消失,但手枪和步枪单发子弹只击中一个敌人,散弹枪每颗散弹也只能击中一个。代码实现符合预期,但缺少穿透型武器(如狙击枪应可穿透多个敌人)。
    改进方向:
    为武器添加 penetration 参数,控制一颗子弹能击中几个怪物。

  7. UI 文字更新过于频繁
    每帧都调用 update_ui() 重新设置所有文字内容,虽然开销不大,但可改为仅在数值变化时更新。
    改进方向:
    使用事件驱动或脏标记,只在血量、金币、波次等改变时刷新对应文本。

  8. 玩家死亡后重置(R键)无法保留分数/金币,挫败感较强
    当前 reset() 完全初始化,没有肉鸽元素或继承机制。玩家重新开始会失去一切,容易劝退。
    改进方向:
    增加“重生”功能:消耗一定金币原地复活,或允许继承部分武器/金币重新开始。

  9. 多武器切换时没有直观的武器图标或提示
    当前仅靠左上角文字显示武器名称,枪口颜色虽有变化但不够明显。新手可能不清楚当前使用何种武器。
    改进方向:
    在鼠标附近或屏幕角落显示当前武器的小图标,或绘制武器名称的浮动标签。

  10. 地图大小固定,缺乏动态地形或视野缩放
    游戏始终全窗口显示,无法切换地图区域,也没有小地图功能。玩家很容易被怪物包围。
    改进方向:
    实现摄像机跟随玩家(当玩家靠近边缘时移动背景),或添加可缩放的小地图。

六、总结和感悟
第一章到最后一章节的知识点,就像游戏里的闯关地图,将Python从入门到实战串得明明白白。我最直观的感受是:老师课程内容安排对我们太友好了。前几章语法和序列打底,像搭地基;函数和面向对象开始长骨架;到文件操作、数据库、GUI和爬虫时,就是盖房子了。特别是最后用Web框架做个完整项目,成就感拉满!以前觉得“面向对象”很抽象,但跟着案例走下来,发现就是把代码当乐高搭,好玩又好懂。小小建议就是,如果课时允许,可以多留点时间给第16、17章的实战环节,因为前面章节知识点多,容易走马观花,真正动手写项目时才最消化知识。像爬虫和Pygame这种趣味性强的章节,可以提前布置一些小作品和小游戏,这样就可以边玩边学,更加有意思。老师是一位靠谱的引路人,python这门课是一盏明灯,是我进一步认识电脑,进入编程的启蒙。这门课从不让我觉得编程枯燥,虽然代码运行失败又找不到原因的时候很让人烦恼,但一个个解决后回头看,真的很有成就感!希望后续课程能再多一点综合性练手项目,让学到的知识都“活”起来~ 😄

posted @ 2026-06-16 16:55  20254216李柳烨  阅读(7)  评论(0)    收藏  举报