课程:《Python程序设计》
班级: 2412
姓名: 闵岩竣
学号:20241223
实验教师:王志强
实验日期:2026年5月25日
必修/选修: 公选课
一、项目概述
本项目是一款基于Pygame开发的2D纵版飞行射击游戏《Star Fighter - Boss Rush》。游戏灵感来源于经典街机射击游戏,玩家操控星际战机在星空中作战,消灭敌机并挑战不断增强的Boss。
二、游戏核心玩法
游戏的核心玩法可以概括为 自动射击 + 躲避弹幕 + Boss挑战:

  1. 自动攻击系统:玩家战机持续自动向前方射击,无需手动开火
  2. 自由移动:使用方向键在屏幕范围内上下左右移动
  3. 敌机系统:普通敌机从屏幕上方随机生成并向下方移动
  4. Boss系统:达到一定分数后触发Boss战,Boss拥有多种攻击模式
  5. 道具系统:击毁敌机有概率掉落回血、武器强化、无敌盾三种道具
  6. 难度递增:分数越高,敌机生成越快;Boss等级越高,弹幕越密集
    游戏结束条件:
    • 玩家HP降为0
    • 被敌机、敌方子弹或Boss本体撞击(非无敌状态下)
    得分机制:
    • 击毁小型敌机:+10分
    • 击毁中型敌机:+25分
    • 击毁大型敌机:+50分
    • 击败Boss:+200 + Boss等级×50分
    三、模块拆分
    3.1 必选模块
    模块 功能描述 对应类/文件
    窗口管理 创建480×700游戏窗口,管理主循环和帧率 主循环代码
    玩家系统 玩家战机绘制、移动、射击、受伤无敌 Player类
    敌机系统 普通敌机生成、移动、射击 Enemy类
    Boss系统 多等级Boss、多种攻击模式、阶段切换 Boss类
    子弹系统 玩家子弹、敌人子弹、Boss子弹 Bullet、EnemyBullet、BossBullet类
    碰撞检测 子弹与敌机、子弹与Boss、玩家与敌人/Boss的碰撞 pygame.sprite.groupcollide
    得分系统 分数计算、最高分保存、UI显示 全局变量+文件读写
    3.2 增强模块
    模块 功能描述 实现方式
    粒子特效系统 引擎尾焰、爆炸效果 Particle类
    动态星空背景 多层星星滚动背景 Star类 + Background类
    道具系统 回血❤️、武器强化⚡、无敌盾⭐ PowerUp类
    音效系统 射击、爆炸、受伤、Boss警告音效 generate_beep()函数生成正弦波音效
    Boss警告系统 Boss出现前闪烁警告文字 计时器+文字透明度动画
    UI系统 血条、分数、Boss名称、武器等级显示 draw_health_bar()等辅助函数
    无敌系统 受伤后闪烁无敌状态 invincible属性+透明度切换
    四、逐步设计思路
    第一步:搭建游戏基础框架
    目标:创建游戏窗口,搭建主循环结构

在__main__部分初始化Pygame,设置窗口大小为480×700,标题为"Star Fighter - Boss Rush",设置帧率为60FPS。创建游戏主循环while True,处理退出事件。

python
WIDTH, HEIGHT = 480, 700
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Star Fighter - Boss Rush")
clock = pygame.time.Clock()
FPS = 60
运行结果:一个黑色窗口,能够正常关闭。

第二步:实现动态星空背景
目标:创建滚动星空背景,营造太空飞行感

屏幕截图 2026-05-25 201809

创建Star类,每颗星星具有随机位置、速度、大小和亮度。创建Background类管理100颗星星的集合。星星从屏幕上方生成,向下移动,移出屏幕后重置到顶部。

python
class Star:
def init(self):
self.x = random.randint(0, WIDTH)
self.y = random.randint(-HEIGHT, 0)
self.speed = random.uniform(0.5, 3)
self.size = random.randint(1, 3)
self.brightness = random.randint(100, 255)
运行结果:深蓝色背景上布满向下移动的闪烁星星。

第三步:绘制玩家战机
目标:创建可移动的玩家战机

屏幕截图 2026-05-25 201554

创建Player类继承pygame.sprite.Sprite,使用pygame.draw.polygon绘制三角形机身,pygame.draw.circle绘制驾驶舱。实现方向键移动控制,速度设为7像素/帧。添加边界检测防止飞出屏幕。

python
class Player(pygame.sprite.Sprite):
def draw_ship(self):
points = [(22, 0), (0, 48), (22, 38), (44, 48)]
pygame.draw.polygon(self.image, (0, 200, 255), points)
pygame.draw.circle(self.image, (100, 230, 255), (22, 22), 8)
运行结果:屏幕下方出现蓝色三角形战机,可用方向键移动。

第四步:实现自动射击系统
目标:战机自动发射子弹,实现基础攻击

创建Bullet类,子弹为渐变黄色矩形。在Player.update()中调用self.shoot()实现自动射击。设置射击间隔为200ms,防止子弹过于密集。子弹速度为-12像素/帧(向上)。

python
def shoot(self):
now = pygame.time.get_ticks()
if now - self.last_shoot < self.shoot_delay:
return
self.last_shoot = now
bullet = Bullet(self.rect.centerx, self.rect.top)
all_sprites.add(bullet)
bullets.add(bullet)
运行结果:玩家战机持续向上发射黄色子弹。

第五步:生成敌机系统
目标:创建普通敌机,实现战斗基础

创建Enemy类,包含三种类型:小型(红色,1HP,速度快)、中型(粉色,2HP,速度中等)、大型(紫色,3HP,速度慢)。敌机从屏幕上方随机位置生成,向下移动。部分敌机(30%)会向玩家发射子弹。

python
class Enemy(pygame.sprite.Sprite):
def init(self, difficulty=1):
self.type = random.choice(['small', 'medium', 'large'])
if self.type == 'small':
self.hp = 1
self.speed = random.randint(2, 4) + difficulty // 2
运行结果:红色、粉色、紫色敌机从上方不断出现。

第六步:碰撞检测与得分
目标:实现子弹消灭敌机、玩家受伤检测

使用pygame.sprite.groupcollide检测子弹与敌机的碰撞。子弹击中敌机扣除HP,HP归零后敌机爆炸并加分。使用spritecollide检测玩家与敌机/敌方子弹的碰撞,触发受伤逻辑。

python
hits = pygame.sprite.groupcollide(enemies, bullets, False, True)
for enemy, bullet_list in hits.items():
for _ in bullet_list:
enemy.hp -= 1
if enemy.hp <= 0:
score += enemy.score_val
enemy.kill()
运行结果:子弹命中敌机后敌机爆炸消失,分数增加。

第七步:粒子特效系统
目标:增加视觉表现力

创建Particle类,实现爆炸效果和引擎尾焰。粒子具有随机速度、重力影响、生命衰减和透明度渐变。敌机死亡时调用create_explosion()生成爆炸粒子;玩家每2帧生成引擎尾焰粒子。

python
class Particle(pygame.sprite.Sprite):
def init(self, x, y, color, speed_range, life=20, size=3):
# 随机角度和速度
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(*speed_range)
self.vx = math.cos(angle) * speed
self.vy = math.sin(angle) * speed
运行结果:敌机爆炸产生彩色粒子扩散,玩家尾部持续喷出橙黄色粒子。

第八步:道具系统
目标:增加游戏策略性

创建PowerUp类,包含三种道具:

回血❤️(绿色圆形,恢复1HP)

屏幕截图 2026-05-25 203740

武器强化⚡(金色三角形,双发/三发持续15秒)

屏幕截图 2026-05-25 203744

无敌盾⭐(淡蓝色圆形,3秒无敌)

屏幕截图 2026-05-25 203747

敌机被击毁时有25%概率掉落道具。道具以2像素/帧速度下落,玩家接触即拾取。

python
if random.random() < 0.25:
kind = random.choices(['heal', 'power', 'invincible'], weights=[40, 35, 25])[0]
p = PowerUp(enemy.rect.centerx, enemy.rect.centery, kind)
all_sprites.add(p)
powerups.add(p)
运行结果:敌机偶尔掉落彩色道具,拾取后获得相应效果。

第九步:Boss系统设计
目标:实现5个逐级增强的Boss

屏幕截图 2026-05-25 203803

创建Boss类,根据boss_level(1-5)配置不同属性:
Boss 名称 颜色 血量 攻击模式
Lv1 GUARDIAN 橙色 25 追踪弹+扇形弹
Lv2 SENTINEL 蓝色 35 追踪+扇形+圆环弹
Lv3 OVERLORD 紫色 45 追踪+扇形+圆环+螺旋
Lv4 DESTROYER 红色 55 高速弹幕,更密集
Lv5+ EMPEROR 金色 65+ 满屏弹幕地狱
Boss拥有4种攻击模式:
追踪弹:向玩家方向发射
扇形弹:向前方扇形区域散射
圆形弹:向四周360度发射
螺旋弹:旋转扩散的弹幕
半血时进入Phase 2,攻击间隔缩短,弹幕数量增加。
python
def attack(self):
level = self.boss_level
phase_mult = 1.5 if self.phase == 2 else 1.0
if level == 1:
if random.random() < 0.6:
self.shoot_tracking(int(2 * phase_mult))
else:
self.shoot_spread(int(5 * phase_mult), math.pi / 6)
运行结果:击败每个Boss后下一个更强,弹幕逐渐密集。
第十步:音效系统
目标:使用程序生成音效,无需外部文件
使用array模块生成正弦波音效,避免依赖外部音频文件:
python
def generate_beep(frequency=440, duration=0.05, volume=0.3):
sample_rate = 22050
n_samples = int(sample_rate * duration)
arr = array('h', [0] * n_samples)
for i in range(n_samples):
t = float(i) / sample_rate
arr[i] = int(math.sin(2 * math.pi * frequency * t) * 32767 * volume)
return pygame.mixer.Sound(arr)
创建5种音效:射击(800Hz)、敌机死亡(200Hz)、道具拾取(1200Hz)、受伤(100Hz)、Boss爆炸(50Hz)。
第十一步:UI系统完善
目标:显示完整的游戏信息
实现以下UI元素:
得分显示(左上角)
击败Boss数量(左上角)
玩家血条(左侧)
武器等级提示
无敌状态提示
Boss名称和血条(顶部居中)
Boss段显示
Boss出现前闪烁警告动画
开始界面和结束界面
最高分保存和显示
python
def draw_health_bar(x, y, w, h, current, max_val, color, bg_color=(80,80,80)):
pygame.draw.rect(screen, bg_color, (x, y, w, h))
fill_w = int(w * current / max_val)
pygame.draw.rect(screen, color, (x, y, fill_w, h))
pygame.draw.rect(screen, WHITE, (x, y, w, h), 2)
第十二步:游戏状态管理
目标:实现完整的游戏流程
使用game_state变量管理三种状态:
"start":开始界面,显示操作说明和最高分
"playing":游戏进行中
"gameover":结束界面,显示得分、击败Boss数,按R重开

屏幕截图 2026-05-25 201651

reset_game()函数负责重置所有全局变量和精灵组。
第十三步:实验源代码
import pygame
import random
import sys
import math
from array import array

Initialize

pygame.init()
pygame.mixer.init(frequency=22050, size=-16, channels=2, buffer=512)

WIDTH, HEIGHT = 480, 700
screen = pygame.display.set_mode((WIDTH, HEIGHT))
pygame.display.set_caption("Star Fighter - Boss Rush")
clock = pygame.time.Clock()
FPS = 60

Colors

WHITE = (255, 255, 255)
BLACK = (0, 0, 0)
RED = (255, 0, 0)
GREEN = (0, 255, 0)
BLUE = (0, 100, 255)
YELLOW = (255, 255, 0)
ORANGE = (255, 150, 0)
CYAN = (0, 255, 255)
PURPLE = (200, 0, 255)
GOLD = (255, 215, 0)
PINK = (255, 100, 180)

Fonts

font_small = pygame.font.Font(None, 28)
font_medium = pygame.font.Font(None, 36)
font_large = pygame.font.Font(None, 48)
font_huge = pygame.font.Font(None, 72)

-------------------- Sound Generation --------------------

def generate_beep(frequency=440, duration=0.05, volume=0.3):
sample_rate = 22050
n_samples = int(sample_rate * duration)
arr = array('h', [0] * n_samples)
for i in range(n_samples):
t = float(i) / sample_rate
arr[i] = int(math.sin(2 * math.pi * frequency * t) * 32767 * volume)
return pygame.mixer.Sound(arr)

sound_shoot = generate_beep(800, 0.04, 0.5)
sound_enemy_die = generate_beep(200, 0.1, 0.6)
sound_powerup = generate_beep(1200, 0.15, 0.7)
sound_hit = generate_beep(100, 0.15, 0.8)
sound_boss_explode = generate_beep(50, 0.4, 0.9)
sound_boss_warning = generate_beep(300, 0.5, 0.8)

-------------------- Particle System --------------------

class Particle(pygame.sprite.Sprite):
def init(self, x, y, color, speed_range, life=20, size=3):
super().init()
self.image = pygame.Surface((size, size), pygame.SRCALPHA)
self.color = color
self.life = life
self.max_life = life
self.size = size
pygame.draw.circle(self.image, color, (size//2, size//2), size//2)
self.rect = self.image.get_rect(center=(x, y))
angle = random.uniform(0, 2 * math.pi)
speed = random.uniform(*speed_range)
self.vx = math.cos(angle) * speed
self.vy = math.sin(angle) * speed
self.gravity = 0.1

def update(self):
self.rect.x += self.vx
self.rect.y += self.vy
self.vy += self.gravity
self.life -= 1
alpha = int(255 * self.life / self.max_life)
self.image.set_alpha(alpha)
if self.life <= 0:
self.kill()

def create_explosion(group, x, y, color, count=20, speed_range=(1, 5), life=15):
for _ in range(count):
p = Particle(x, y, color, speed_range, life, random.randint(2, 5))
group.add(p)

-------------------- Background --------------------

class Star:
def init(self):
self.reset()

def reset(self):
self.x = random.randint(0, WIDTH)
self.y = random.randint(-HEIGHT, 0)
self.speed = random.uniform(0.5, 3)
self.size = random.randint(1, 3)
self.brightness = random.randint(100, 255)

def update(self):
self.y += self.speed
if self.y > HEIGHT:
self.reset()
self.y = -5

def draw(self, surface):
c = self.brightness
color = (c, c, c)
pygame.draw.circle(surface, color, (int(self.x), int(self.y)), self.size)

class Background:
def init(self):
self.stars = [Star() for _ in range(100)]

def update(self):
for star in self.stars:
star.update()

def draw(self, surface):
surface.fill((5, 5, 15))
for star in self.stars:
star.draw(surface)

-------------------- Player --------------------

class Player(pygame.sprite.Sprite):
def init(self):
super().init()
self.image = pygame.Surface((44, 48), pygame.SRCALPHA)
self.draw_ship()
self.rect = self.image.get_rect()
self.rect.centerx = WIDTH // 2
self.rect.bottom = HEIGHT - 30
self.speed = 7
self.hp = 8
self.max_hp = 8
self.shoot_delay = 200
self.last_shoot = 0
self.invincible = 0
self.invincible_duration = 120
self.power_level = 1
self.power_timer = 0
self.engine_timer = 0
self.power_duration = 60 * 15
self.auto_fire = True

def draw_ship(self):
self.image.fill((0,0,0,0))
points = [(22, 0), (0, 48), (22, 38), (44, 48)]
pygame.draw.polygon(self.image, (0, 200, 255), points)
pygame.draw.circle(self.image, (100, 230, 255), (22, 22), 8)
pygame.draw.rect(self.image, (80, 80, 80), (14, 38, 16, 6))
pygame.draw.line(self.image, (0, 150, 255), (22, 10), (8, 32), 3)
pygame.draw.line(self.image, (0, 150, 255), (22, 10), (36, 32), 3)

def update(self):
keys = pygame.key.get_pressed()
if keys[pygame.K_LEFT] and self.rect.left > 0:
self.rect.x -= self.speed
if keys[pygame.K_RIGHT] and self.rect.right < WIDTH:
self.rect.x += self.speed
if keys[pygame.K_UP] and self.rect.top > 0:
self.rect.y -= self.speed
if keys[pygame.K_DOWN] and self.rect.bottom < HEIGHT:
self.rect.y += self.speed

if self.auto_fire:
self.shoot()

if self.invincible > 0:
self.invincible -= 1
if self.invincible % 4 < 2:
self.image.set_alpha(50)
else:
self.image.set_alpha(255)
else:
self.image.set_alpha(255)

if self.power_timer > 0:
self.power_timer -= 1
if self.power_timer <= 0:
self.power_level = 1

self.engine_timer += 1
if self.engine_timer % 2 == 0:
p = Particle(self.rect.centerx - 4, self.rect.bottom, (255, 200, 50), (0.2, 1.5), 8, 2)
all_sprites.add(p)
p = Particle(self.rect.centerx + 4, self.rect.bottom, (255, 150, 0), (0.2, 1.5), 8, 2)
all_sprites.add(p)

def shoot(self):
now = pygame.time.get_ticks()
if now - self.last_shoot < self.shoot_delay:
return
self.last_shoot = now
sound_shoot.play()

if self.power_level == 1:
bullet = Bullet(self.rect.centerx, self.rect.top)
all_sprites.add(bullet)
bullets.add(bullet)
elif self.power_level == 2:
bullet = Bullet(self.rect.centerx - 10, self.rect.top)
all_sprites.add(bullet)
bullets.add(bullet)
bullet = Bullet(self.rect.centerx + 10, self.rect.top)
all_sprites.add(bullet)
bullets.add(bullet)
elif self.power_level >= 3:
for dx in [-14, 0, 14]:
bullet = Bullet(self.rect.centerx + dx, self.rect.top)
all_sprites.add(bullet)
bullets.add(bullet)

def power_up(self):
if self.power_level < 3:
self.power_level += 1
self.power_timer = self.power_duration

def hit(self):
if self.invincible > 0:
return False
self.hp -= 1
self.invincible = self.invincible_duration
sound_hit.play()
create_explosion(all_sprites, self.rect.centerx, self.rect.centery, (255, 100, 0), 15)
return True

-------------------- Bullets --------------------

class Bullet(pygame.sprite.Sprite):
def init(self, x, y):
super().init()
self.image = pygame.Surface((6, 18), pygame.SRCALPHA)
for i in range(18):
alpha = 255 - i * 10
color = (255, 255, 100, alpha)
pygame.draw.line(self.image, color, (0, i), (6, i))
self.rect = self.image.get_rect(center=(x, y))
self.speed = -12

def update(self):
self.rect.y += self.speed
if self.rect.bottom < 0:
self.kill()

class EnemyBullet(pygame.sprite.Sprite):
def init(self, x, y, vx, vy, color=RED):
super().init()
self.image = pygame.Surface((8, 8), pygame.SRCALPHA)
pygame.draw.circle(self.image, color, (4, 4), 4)
self.rect = self.image.get_rect(center=(x, y))
self.vx = vx
self.vy = vy

def update(self):
self.rect.x += self.vx
self.rect.y += self.vy
if self.rect.top > HEIGHT or self.rect.bottom < 0 or self.rect.left > WIDTH or self.rect.right < 0:
self.kill()

-------------------- Enemy --------------------

class Enemy(pygame.sprite.Sprite):
def init(self, difficulty=1):
super().init()
self.type = random.choice(['small', 'medium', 'large'])
if self.type == 'small':
w, h = 30, 30
self.hp = 1
self.speed = random.randint(2, 4) + difficulty // 2
self.color = (255, 80, 80)
self.score_val = 10
elif self.type == 'medium':
w, h = 40, 40
self.hp = 2
self.speed = random.randint(1, 3) + difficulty // 2
self.color = (255, 50, 180)
self.score_val = 25
else:
w, h = 55, 45
self.hp = 3
self.speed = random.randint(1, 2) + difficulty // 3
self.color = (180, 0, 220)
self.score_val = 50

self.image = pygame.Surface((w, h), pygame.SRCALPHA)
points = [(w//2, h), (0, 0), (w, 0)]
pygame.draw.polygon(self.image, self.color, points)
pygame.draw.circle(self.image, (255, 200, 200), (w//2, h//3), 5)
self.rect = self.image.get_rect()
self.rect.x = random.randint(0, WIDTH - w)
self.rect.y = random.randint(-150, -40)
self.shoot_delay = random.randint(2000, 4000)
self.last_shot = pygame.time.get_ticks()
self.shoot_chance = 0.3

def update(self):
self.rect.y += self.speed
now = pygame.time.get_ticks()
if now - self.last_shot > self.shoot_delay and self.rect.top > 0 and random.random() < self.shoot_chance:
self.last_shot = now
dx = player.rect.centerx - self.rect.centerx
dy = player.rect.centery - self.rect.centery
dist = math.hypot(dx, dy)
if dist > 0:
vx = dx / dist * 3
vy = dy / dist * 3
bullet = EnemyBullet(self.rect.centerx, self.rect.bottom, vx, vy, (255, 100, 100))
all_sprites.add(bullet)
enemy_bullets.add(bullet)
if self.rect.top > HEIGHT:
self.kill()

def kill(self):
create_explosion(all_sprites, self.rect.centerx, self.rect.centery, self.color, 12)
super().kill()

-------------------- BOSS System --------------------

class BossBullet(pygame.sprite.Sprite):
def init(self, x, y, vx, vy, color=ORANGE, size=5):
super().init()
self.image = pygame.Surface((size2, size2), pygame.SRCALPHA)
pygame.draw.circle(self.image, color, (size, size), size)
self.rect = self.image.get_rect(center=(x, y))
self.vx = vx
self.vy = vy

def update(self):
self.rect.x += self.vx
self.rect.y += self.vy
if self.rect.top > HEIGHT + 50 or self.rect.bottom < -50 or self.rect.left > WIDTH + 50 or self.rect.right < -50:
self.kill()

class Boss(pygame.sprite.Sprite):
def init(self, boss_level=1):
super().init()
self.boss_level = boss_level
self.target_y = 80
self.phase = 1
self.attack_timer = 0
self.direction = 1
self.move_speed = 2 + boss_level * 0.5
self.alive = True # Track if boss is alive

self.max_hp = 15 + boss_level * 10
self.hp = self.max_hp

self.configure_boss()

def configure_boss(self):
level = self.boss_level
self.image = pygame.Surface((120 + level * 20, 80 + level * 10), pygame.SRCALPHA)
self.rect = self.image.get_rect()
self.rect.centerx = WIDTH // 2
self.rect.y = -150

if level == 1:
self.body_color = (200, 100, 0)
self.accent_color = (255, 150, 0)
self.eye_color = RED
self.name = "GUARDIAN"
elif level == 2:
self.body_color = (0, 150, 200)
self.accent_color = (0, 200, 255)
self.eye_color = (255, 200, 0)
self.name = "SENTINEL"
elif level == 3:
self.body_color = (150, 0, 200)
self.accent_color = (200, 100, 255)
self.eye_color = RED
self.name = "OVERLORD"
elif level == 4:
self.body_color = (200, 0, 0)
self.accent_color = (255, 100, 100)
self.eye_color = YELLOW
self.name = "DESTROYER"
else:
self.body_color = (255, 100, 0)
self.accent_color = (255, 200, 0)
self.eye_color = WHITE
self.name = "EMPEROR"

self.draw_boss()

def draw_boss(self):
self.image.fill((0,0,0,0))
w = self.rect.width
h = self.rect.height

body_rect = pygame.Rect(w//4, h//6, w//2, h*2//3)
pygame.draw.ellipse(self.image, self.body_color, body_rect)
pygame.draw.ellipse(self.image, self.accent_color, body_rect.inflate(-20, -20))

eye_size = 8 + self.boss_level
eye_y = h // 3
pygame.draw.circle(self.image, self.eye_color, (w//3, eye_y), eye_size)
pygame.draw.circle(self.image, self.eye_color, (w2//3, eye_y), eye_size)
pygame.draw.circle(self.image, BLACK, (w//3 + 3, eye_y), eye_size//2)
pygame.draw.circle(self.image, BLACK, (w
2//3 + 3, eye_y), eye_size//2)

wing_size = 30 + self.boss_level * 10
points_left = [(w//4, h//3), (-wing_size//2, h//2), (-wing_size//2, h-10), (w//4, h-10)]
points_right = [(w3//4, h//3), (w + wing_size//2, h//2), (w + wing_size//2, h-10), (w3//4, h-10)]
pygame.draw.polygon(self.image, self.body_color, points_left)
pygame.draw.polygon(self.image, self.body_color, points_right)

for i in range(min(self.boss_level, 5)):
star_x = w//2 - self.boss_level * 8 + i * 16
pygame.draw.circle(self.image, GOLD, (star_x, h-15), 5)

def update(self):
if not self.alive:
return

if self.rect.y < self.target_y:
self.rect.y += self.move_speed
else:
self.rect.x += self.direction * (1.5 + self.boss_level * 0.3)
if self.rect.left < 0 or self.rect.right > WIDTH:
self.direction *= -1

if self.hp <= self.max_hp // 2 and self.phase == 1:
self.phase = 2
self.move_speed += 1
create_explosion(all_sprites, self.rect.centerx, self.rect.centery, self.accent_color, 30)

self.attack_timer += 1
attack_interval = max(30, 90 - self.boss_level * 10)

if self.attack_timer >= attack_interval:
self.attack_timer = 0
self.attack()

def attack(self):
if not self.alive:
return

level = self.boss_level
phase_mult = 1.5 if self.phase == 2 else 1.0

if level == 1:
if random.random() < 0.6:
self.shoot_tracking(int(2 * phase_mult))
else:
self.shoot_spread(int(5 * phase_mult), math.pi / 6)
elif level == 2:
if random.random() < 0.4:
self.shoot_tracking(int(3 * phase_mult), speed=3.5)
elif random.random() < 0.7:
self.shoot_spread(int(7 * phase_mult), math.pi / 4)
else:
self.shoot_circle(int(8 * phase_mult))
elif level == 3:
choice = random.random()
if choice < 0.3:
self.shoot_tracking(int(4 * phase_mult), speed=4)
elif choice < 0.6:
self.shoot_spread(int(9 * phase_mult), math.pi / 3)
elif choice < 0.85:
self.shoot_circle(int(10 * phase_mult))
else:
self.shoot_spiral(int(12 * phase_mult))
elif level == 4:
choice = random.random()
if choice < 0.3:
self.shoot_tracking(int(5 * phase_mult), speed=5)
elif choice < 0.55:
self.shoot_spread(int(11 * phase_mult), math.pi / 2.5)
elif choice < 0.8:
self.shoot_circle(int(14 * phase_mult), speed=3.5)
else:
self.shoot_spiral(int(16 * phase_mult), speed=3.5)
else:
choice = random.random()
if choice < 0.25:
self.shoot_tracking(int(6 * phase_mult), speed=5.5)
elif choice < 0.5:
self.shoot_spread(int(15 * phase_mult), math.pi / 2)
elif choice < 0.75:
self.shoot_circle(int(18 * phase_mult), speed=4)
else:
self.shoot_spiral(int(20 * phase_mult), speed=4)

def shoot_tracking(self, count, speed=3):
if not self.alive:
return
for _ in range(count):
bx = self.rect.centerx + random.randint(-30, 30)
by = self.rect.bottom
dx = player.rect.centerx - bx
dy = player.rect.centery - by
dist = math.hypot(dx, dy)
if dist > 0:
vx = dx / dist * speed
vy = dy / dist * speed
bullet = BossBullet(bx, by, vx, vy, RED)
all_sprites.add(bullet)
boss_bullets.add(bullet)

def shoot_spread(self, count, angle_range):
if not self.alive:
return
angle_start = -math.pi / 2 - angle_range
angle_end = -math.pi / 2 + angle_range
speed = 3 + self.boss_level * 0.5
for i in range(count):
angle = angle_start + (angle_end - angle_start) * i / max(count - 1, 1)
vx = math.cos(angle) * speed
vy = math.sin(angle) * speed
bullet = BossBullet(self.rect.centerx, self.rect.bottom, vx, vy, ORANGE)
all_sprites.add(bullet)
boss_bullets.add(bullet)

def shoot_circle(self, count, speed=3):
if not self.alive:
return
for i in range(count):
angle = 2 * math.pi * i / count
vx = math.cos(angle) * speed
vy = math.sin(angle) * speed
bullet = BossBullet(self.rect.centerx, self.rect.centery, vx, vy, CYAN, size=4)
all_sprites.add(bullet)
boss_bullets.add(bullet)

def shoot_spiral(self, count, speed=3):
if not self.alive:
return
base_angle = self.attack_timer * 0.1
for i in range(count):
angle = base_angle + 2 * math.pi * i / count
vx = math.cos(angle) * speed * (1 + i * 0.1)
vy = math.sin(angle) * speed * (1 + i * 0.1)
bullet = BossBullet(self.rect.centerx, self.rect.centery, vx, vy, PINK, size=4)
all_sprites.add(bullet)
boss_bullets.add(bullet)

def take_damage(self, damage):
"""Safely apply damage to boss"""
if not self.alive:
return False
self.hp -= damage
if self.hp <= 0:
self.hp = 0
self.alive = False
return True
return False

def kill(self):
self.alive = False
sound_boss_explode.play()
create_explosion(all_sprites, self.rect.centerx, self.rect.centery, self.accent_color, 50, (2, 8), 30)
super().kill()

-------------------- Power-ups --------------------

class PowerUp(pygame.sprite.Sprite):
def init(self, x, y, kind):
super().init()
self.kind = kind
size = 20
self.image = pygame.Surface((size, size), pygame.SRCALPHA)
if kind == 'heal':
color = (0, 255, 100)
pygame.draw.circle(self.image, color, (size//2, size//2), size//2)
pygame.draw.circle(self.image, WHITE, (size//2, size//2), 4)
elif kind == 'power':
color = (255, 200, 0)
pygame.draw.polygon(self.image, color, [(size//2, 0), (0, size), (size, size)])
pygame.draw.line(self.image, WHITE, (size//2, 4), (size//2, size-4), 2)
elif kind == 'invincible':
color = (200, 200, 255)
pygame.draw.circle(self.image, color, (size//2, size//2), size//2)
pygame.draw.circle(self.image, WHITE, (size//2, size//2), 6, 2)

self.rect = self.image.get_rect(center=(x, y))
self.speed = 2

def update(self):
self.rect.y += self.speed
if self.rect.top > HEIGHT:
self.kill()

-------------------- Helper Functions --------------------

def draw_text_with_shadow(text, font_obj, color, x, y, shadow_color=(50,50,50)):
surf = font_obj.render(text, True, shadow_color)
rect = surf.get_rect(center=(x+2, y+2))
screen.blit(surf, rect)
surf = font_obj.render(text, True, color)
rect = surf.get_rect(center=(x, y))
screen.blit(surf, rect)

def draw_health_bar(x, y, w, h, current, max_val, color, bg_color=(80,80,80)):
pygame.draw.rect(screen, bg_color, (x, y, w, h))
if max_val > 0:
fill_w = int(w * current / max_val)
pygame.draw.rect(screen, color, (x, y, fill_w, h))
pygame.draw.rect(screen, WHITE, (x, y, w, h), 2)

-------------------- Game Setup --------------------

all_sprites = pygame.sprite.Group()
enemies = pygame.sprite.Group()
bullets = pygame.sprite.Group()
enemy_bullets = pygame.sprite.Group()
boss_bullets = pygame.sprite.Group()
powerups = pygame.sprite.Group()

bg = Background()
player = Player()
all_sprites.add(player)

score = 0
high_score = 0
boss_kills = 0

try:
with open("highscore.txt", "r") as f:
high_score = int(f.read())
except:
pass

game_state = "start"
boss = None
next_boss_level = 1
difficulty = 0
enemy_spawn_timer = 0
max_enemy_spawn = 45
boss_spawn_score = 150
boss_warning_timer = 0
boss_warning_duration = 90

def reset_game():
global score, boss, difficulty, enemy_spawn_timer, game_state
global boss_kills, next_boss_level, boss_spawn_score, boss_warning_timer
score = 0
boss = None
difficulty = 0
enemy_spawn_timer = 0
game_state = "playing"
boss_kills = 0
next_boss_level = 1
boss_spawn_score = 150
boss_warning_timer = 0
enemies.empty()
bullets.empty()
enemy_bullets.empty()
boss_bullets.empty()
powerups.empty()
all_sprites.empty()
all_sprites.add(player)
player.hp = player.max_hp
player.power_level = 1
player.power_timer = 0
player.invincible = 60
player.rect.centerx = WIDTH // 2
player.rect.bottom = HEIGHT - 30

def spawn_boss():
global boss, next_boss_level, boss_warning_timer
boss = Boss(next_boss_level)
all_sprites.add(boss)
boss_warning_timer = boss_warning_duration

-------------------- Main Loop --------------------

while True:
clock.tick(FPS)

========= Start Screen =========

if game_state == "start":
bg.update()
bg.draw(screen)
draw_text_with_shadow("STAR FIGHTER", font_huge, CYAN, WIDTH//2, 160)
draw_text_with_shadow("Arrow Keys to Move", font_medium, WHITE, WIDTH//2, 280)
draw_text_with_shadow("Auto-Fire ON", font_medium, GREEN, WIDTH//2, 330)
draw_text_with_shadow("Defeat Bosses to Progress", font_medium, WHITE, WIDTH//2, 380)
draw_text_with_shadow("Bosses get stronger each time!", font_small, GOLD, WIDTH//2, 430)
draw_text_with_shadow(f"High Score: {high_score}", font_medium, YELLOW, WIDTH//2, 500)
draw_text_with_shadow("Press Any Key to Start", font_large, WHITE, WIDTH//2, 570)
pygame.display.flip()

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
game_state = "playing"
reset_game()
continue

========= Game Over Screen =========

if game_state == "gameover":
bg.update()
bg.draw(screen)
draw_text_with_shadow("GAME OVER", font_huge, RED, WIDTH//2, 200)
draw_text_with_shadow(f"Score: {score}", font_large, WHITE, WIDTH//2, 280)
draw_text_with_shadow(f"Bosses Defeated: {boss_kills}", font_medium, GOLD, WIDTH//2, 330)
if score > high_score:
high_score = score
try:
with open("highscore.txt", "w") as f:
f.write(str(high_score))
except:
pass
draw_text_with_shadow("NEW HIGH SCORE!", font_medium, GOLD, WIDTH//2, 380)
draw_text_with_shadow(f"High Score: {high_score}", font_medium, YELLOW, WIDTH//2, 430)
draw_text_with_shadow("Press R to Restart", font_large, WHITE, WIDTH//2, 500)
pygame.display.flip()

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_r:
reset_game()
continue

========= Gameplay =========

for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.quit()
sys.exit()

Update

all_sprites.update()
bg.update()

Boss warning display

if boss_warning_timer > 0:
boss_warning_timer -= 1

Difficulty scaling

difficulty = score // 150
enemy_spawn_timer += 1
spawn_threshold = max(15, max_enemy_spawn - difficulty)
if enemy_spawn_timer >= spawn_threshold:
enemy_spawn_timer = 0
e = Enemy(difficulty)
all_sprites.add(e)
enemies.add(e)

Boss spawning logic

if boss is None and boss_warning_timer <= 0:
if score >= boss_spawn_score:
spawn_boss()

Bullet-enemy collision

hits = pygame.sprite.groupcollide(enemies, bullets, False, True)
for enemy, bullet_list in hits.items():
for _ in bullet_list:
enemy.hp -= 1
if enemy.hp <= 0:
score += enemy.score_val
sound_enemy_die.play()
if random.random() < 0.25:
kind = random.choices(['heal', 'power', 'invincible'], weights=[40, 35, 25])[0]
p = PowerUp(enemy.rect.centerx, enemy.rect.centery, kind)
all_sprites.add(p)
powerups.add(p)
enemy.kill()

Bullet-boss collision (FIXED)

if boss is not None and boss.alive:
# Get all bullets that collide with boss
b_hits = pygame.sprite.spritecollide(boss, bullets, True)
if b_hits:
# Apply damage for each hit
for _ in b_hits:
boss_died = boss.take_damage(1)
if boss_died:
# Boss defeated
score += 200 + next_boss_level * 50
boss_kills += 1
next_boss_level += 1
boss_spawn_score = score + 200
boss.kill()
boss = None
# Heal player
player.hp = min(player.max_hp, player.hp + 2)
# Clear enemy bullets
for b in enemy_bullets:
b.kill()
for b in boss_bullets:
b.kill()
# Brief invincibility
player.invincible = max(player.invincible, 60)
break # Stop processing hits since boss is dead

Player collisions

hit_list = pygame.sprite.spritecollide(player, enemies, True)
for e in hit_list:
if player.hit():
if player.hp <= 0:
game_state = "gameover"
e.kill()

hit_list = pygame.sprite.spritecollide(player, enemy_bullets, True)
for _ in hit_list:
if player.hit():
if player.hp <= 0:
game_state = "gameover"

hit_list = pygame.sprite.spritecollide(player, boss_bullets, True)
for _ in hit_list:
if player.hit():
if player.hp <= 0:
game_state = "gameover"

Player-Boss body collision

if boss is not None and boss.alive and pygame.sprite.collide_rect(player, boss):
if player.hit():
if player.hp <= 0:
game_state = "gameover"

Power-up collection

pu_hits = pygame.sprite.spritecollide(player, powerups, True)
for pu in pu_hits:
sound_powerup.play()
if pu.kind == 'heal':
player.hp = min(player.max_hp, player.hp + 1)
elif pu.kind == 'power':
player.power_up()
elif pu.kind == 'invincible':
player.invincible = 180

========= Draw =========

bg.draw(screen)
all_sprites.draw(screen)

UI

draw_text_with_shadow(f"Score: {score}", font_medium, WHITE, 70, 20)
draw_text_with_shadow(f"Bosses: {boss_kills}", font_small, GOLD, 70, 45)
draw_health_bar(10, 65, 150, 16, player.hp, player.max_hp, (0, 255, 100))
if player.power_level > 1:
draw_text_with_shadow(f"Weapon Lv.{player.power_level}", font_small, GOLD, 100, 90)
if player.invincible > 0:
draw_text_with_shadow("Shield Active", font_small, (200, 200, 255), 350, 20)

Boss warning

if boss_warning_timer > 0:
alpha = abs(math.sin(boss_warning_timer * 0.1)) * 255
boss_names = ['GUARDIAN', 'SENTINEL', 'OVERLORD', 'DESTROYER', 'EMPEROR']
warning_text = f"WARNING! {boss_names[min(next_boss_level-1, 4)]} APPROACHING!"
surf = font_large.render(warning_text, True, RED)
surf.set_alpha(int(alpha))
screen.blit(surf, surf.get_rect(center=(WIDTH//2, HEIGHT//2)))

Boss health bar (with safety check)

if boss is not None and boss.alive:
draw_text_with_shadow(f"{boss.name}", font_medium, boss.accent_color, WIDTH//2, 15)
draw_text_with_shadow(f"Phase {boss.phase}", font_small, RED, WIDTH//2, 40)
draw_health_bar(WIDTH//2 - 120, 55, 240, 12, boss.hp, boss.max_hp, boss.body_color)

pygame.display.flip()

pygame.quit()
sys.exit()
五、遇到的问题及解决方案
问题1:击败第一个Boss后游戏闪退
错误信息:AttributeError: 'NoneType' object has no attribute 'hp'
原因分析:Boss被击败后boss = None,但同一帧内还有其他子弹在对Boss进行碰撞检测,导致访问None.hp。
解决方案:
为Boss添加alive属性跟踪状态
创建take_damage()方法安全处理伤害
所有访问碰撞处理中使用break防止对已死Boss的重复处理
问题2:画面闪烁和性能优化
原因分析:Pygame使用软件渲染,大量精灵同时绘制可能导致性能问题。
解决方案:
使用pygame.SRCALPHA创建带透明通道的Surface
合理控制粒子数量和生命周期
子弹移出屏幕后立即kill()回收内存
控制敌人生成间隔,避免同屏精灵过多
问题3:音效延迟和卡顿
原因分析:实时生成音效可能造成主线程阻塞。
解决方案:
使用pygame.mixer.Sound预生成音效
减小音效时长(0.04-0.4秒)
合理控制音量(0.3-0.9)
六、程序运行视频
https://www.bilibili.com/video/BV1yCJc6hEop/?vd_source=27dc7fd68fc778709635b22a5c7fff55
将代码托管到gitee上
https://gitee.com/min-yanjun/112112/commit/7006dbeaece1233337b83e145be45019872e6ae8
七、实验感想
通过这次大实验,我深入学习了Pygame游戏开发框架,掌握了以下技能:
面向对象编程:将游戏中的各个元素(玩家、敌机、Boss、子弹、粒子、道具)抽象为类,体会了封装、多态的实际应用。
游戏循环与状态管理:理解了while True主循环的本质,学会了使用状态变量管理游戏流程。
碰撞检测:掌握了pygame.sprite.groupcollide和spritecollide的使用,理解了矩形碰撞检测的原理。
粒子系统:通过ai实现了一个简单的粒子系统,包括速度、重力、生命衰减和透明度渐变。
程序化音效生成:ai教我学会了使用正弦波数学原理生成音效,无需依赖外部音频文件。
UI设计:自己上网搜索学会了绘制血条、文字阴影、动画闪烁等UI元素。
Bug调试:通过解决Boss死亡后闪退的问题,加深了对游戏循环中对象生命周期管理的理解。
八、课程感想
选这门课的时候其实没想太多,主要是因为听说王老师给分高。我自己之前写过一些Python脚本,觉得语法差不多都会了,应该不用怎么听。结果上了一学期,发现原来的想法挺幼稚的
王老师讲课不太按书本来。第一节课没讲变量循环,反而跟我们聊Python适合干什么、跟其他语言有什么区别。当时觉得这有什么好讲的,后来才明白,搞清楚什么场景用什么工具,比死记语法有用得多。后面讲到pip装库、pyinstaller打包的时候,这些东西书里基本没有,但实际干活经常用到,我才反应过来老师一开始为什么要铺垫那些。
英语单词打卡这事,说实话刚开始有点烦,觉得跟编程课有什么关系。但坚持了几周,发现看报错信息不用再截图翻译了,给变量起名字的时候脑子里也能蹦出几个合适的单词,确实有帮助。
课程内容里我印象最深的是socket通信那一节。以前觉得程序就是自己电脑上跑的东西,结果老师演示了两台电脑互传数据,我才意识到代码是可以"走出去"的。还有爬虫,之前一直觉得是很神秘的技术,学完发现原理其实没那么复杂,关键是分析网页结构和模拟请求,有种揭开黑盒子的感觉。
王老师讲序列那几种类型的时候也让我挺有收获。列表、元组、集合、字典,我以前一直是混着用的,觉得都能存东西差不太多。课上讲了它们底层的区别和适用场景之后,写代码的时候思路清楚多了。
大实验我做的是一个飞行射击游戏。一个月前就搭了框架,中间改了好几版。Pygame确实难用,很多功能得自己从零写,但也正因为这样,游戏循环、碰撞检测这些底层的东西反而搞明白了。后来还去学了JavaScript的Phaser框架做了个网页版对比,一下子就更清楚两种框架各自的设计思路了。
一个学期下来,不光是学会了Python,更重要的是知道了怎么管理代码版本、怎么打包发布、遇到问题该往哪个方向查。这些考试不怎么考,但我觉得以后真干活的时候会用到。
感谢王老师,这门课选得不亏。