Python通过Pygame实现一个简化版弹幕射击躲避游戏
项目概述
本文通过 Pygame 实现一个简化版弹幕射击躲避游戏(Bullet Hell)。
游戏以鼠标控制玩家小点,在持续涌来的满屏弹幕中生存尽可能长的时间。弹幕按阶段切换模式,难度随生存时间线性提升,血量耗尽即游戏结束。其中:
- 鼠标操控:玩家小点实时跟随鼠标位置移动,操作直觉自然,无需学习成本。
- 五种弹幕模式:散射、追踪、旋转、扇形、密集,每隔约 5 秒自动切换,节奏变化丰富。
- 判定碰撞:玩家判定圆半径仅 8px,子弹半径 5px,碰撞采用圆心距离精确计算,手感宽松不误判。
- 无敌帧机制:受伤后有 90 帧(约 1.5 秒)无敌时间,玩家有机会脱离危险区域。
- 血量系统:5 点 HP,以圆形图标逐个展示,低血量时视觉警示明显。
- 变速机制:子弹基础速度 2.5px/帧,随分数线性递增(每分 +0.008px/帧),越到后期越考验走位。
- 键盘操作:鼠标点击:开始/重新开始;ESC:退出。
游戏实现

初始化与基础设置
游戏启动时初始化 Pygame,定义窗口尺寸和颜色常量。
pygame.init() W, H = 600, 680 FPS = 60 PLAYER_R = 8 BULLET_R = 5 INVINCIBLE_TIME = 90 # 帧
PLAYER_R 和 BULLET_R 分别定义玩家与子弹的碰撞半径,两者之和(13px)即为触发碰撞的最大圆心间距。INVINCIBLE_TIME = 90 对应 60FPS 下约 1.5 秒的无敌窗口,保证受击后有足够时间脱身。
颜色定义
C_BG = (8, 5, 20) C_PLAYER = (80, 220, 255) C_BULLET = [(255, 80, 120), (255, 180, 50), (120, 255, 150), (180, 100, 255)] C_HP = (80, 200, 120) C_HP_LOW = (220, 80, 60) C_SHIELD = (100, 180, 255) C_STAR = (255, 240, 100)
整体延续深色宇宙风格:接近纯黑的深蓝背景搭配高饱和霓虹色子弹,四种弹幕颜色随阶段轮换,让玩家在混乱中仍能直觉分辨当前弹幕模式;玩家以冷蓝色呈现,在暖色弹幕中突出醒目。
子弹类设计
class Bullet:
def __init__(self, x, y, vx, vy, color_idx=0, r=BULLET_R):
self.x, self.y = float(x), float(y)
self.vx, self.vy = vx, vy
self.color = C_BULLET[color_idx % len(C_BULLET)]
self.r = r
self.trail = []
def update(self):
self.trail.append((self.x, self.y))
if len(self.trail) > 6:
self.trail.pop(0)
self.x += self.vx
self.y += self.vy
def alive(self):
return -50 < self.x < W+50 and -50 < self.y < H+50
trail 列表保存最近 6 帧的历史坐标,形成拖尾轨迹。每帧先追加当前坐标再移动,保证拖尾始终落后于弹头。alive() 在边界外 50px 才判定为失效,避免子弹在画面边缘突然消失的视觉跳变。
子弹渲染
def draw(self, surf):
for i, (tx, ty) in enumerate(self.trail):
a = int(40 * (i+1) / len(self.trail))
r = max(1, self.r - 2)
s = pygame.Surface((r*2+1, r*2+1), pygame.SRCALPHA)
pygame.draw.circle(s, (*self.color, a), (r, r), r)
surf.blit(s, (tx-r, ty-r))
pygame.draw.circle(surf, self.color, (int(self.x), int(self.y)), self.r)
pygame.draw.circle(surf, (255,255,255,200), (int(self.x), int(self.y)), max(1, self.r-2), 1)
拖尾透明度按序号线性递增(最老的帧 alpha 最低),形成渐隐效果。弹头上额外叠加一圈白色高光描边,使子弹在任意背景色下保持清晰可辨的立体感。
粒子系统
class Particle:
def __init__(self, x, y, color=(255,255,255)):
self.x, self.y = x, y
a = random.uniform(0, math.pi*2)
s = random.uniform(1, 5)
self.vx, self.vy = math.cos(a)*s, math.sin(a)*s
self.life = 1.0
self.color = color
self.r = random.randint(2, 5)
def update(self):
self.x += self.vx
self.y += self.vy
self.vx *= 0.92
self.vy *= 0.92
self.life -= 0.03
return self.life > 0
粒子初速度方向随机均匀分布于整个圆周(全向爆炸感),速度大小 1–5px/帧。每帧对速度分量乘以摩擦系数 0.92,模拟空气阻力减速,使粒子在靠近中心时自然刹车而非骤停。life 约 33 帧(0.55 秒)归零,同步控制透明度淡出。
受伤时一次性生成 20 颗红色粒子,数量与颜色共同强调冲击感:
for _ in range(20):
particles.append(Particle(px, py, (255, 100, 100)))
五种弹幕模式
散射(phase 0)
def spawn_scatter(cx, cy, count, speed, ci):
bullets = []
for i in range(count):
a = (2*math.pi*i/count)
bullets.append(Bullet(cx, cy, math.cos(a)*speed, math.sin(a)*speed, ci))
return bullets
以固定角度间隔在 360° 内均匀散射 count 颗子弹,从画面随机边缘点发射。发射源位置随机、圆心均匀,玩家需时刻注意来球方向。随分数提升 count 增大(8 + score//60),后期散射圈数更密。
追踪扇形(phase 1)
def spawn_aimed(px, py, ox, oy, speed, spread=3, ci=0):
dx, dy = px-ox, py-oy
for s in range(-spread, spread+1):
a = math.atan2(dy, dx) + s * 0.15
bullets.append(Bullet(ox, oy, math.cos(a)*speed, math.sin(a)*speed, ci))
以 atan2 实时计算玩家方向角,在此基础上以 0.15 弧度(约 8.6°)间隔生成 2*spread+1 颗子弹构成扇形。追踪弹从屏幕左右两侧发射,迫使玩家保持横向走位。
旋转(phase 2)
spin_angle += 0.3
for j in range(3):
a = spin_angle + j * 2.1
bullets.append(Bullet(W//2, H//2, math.cos(a)*spd, math.sin(a)*spd, ci))
从画面中心每帧旋转 0.3 弧度(约 17°/帧),以 120° 间隔同时发射 3 颗,形成旋转三叉星弹幕。spin_angle 在主循环中持续累加,弹幕轨迹不断螺旋外扩,玩家须绕圈走位。
顶部扇形(phase 3)
def spawn_fan(cx, cy, base_angle, count, spread, speed, ci):
for i in range(count):
a = base_angle - spread/2 + spread*i/(max(1,count-1))
bullets.append(Bullet(cx, cy, math.cos(a)*speed, math.sin(a)*speed, ci))
在顶部随机 x 坐标向下发射扇形子弹群,基准角加上随机偏转(±0.5 弧度),每次出现方向略有不同。扇形宽度 1.2 弧度、7 颗子弹,覆盖约 69° 的扇面,迫使玩家左右机动躲避。
密集追踪(phase 4)
edge = random.randint(0, 3)
# 根据 edge 决定四条边上的随机发射点
dx, dy = px-cx, py-cy
a = math.atan2(dy, dx)
for k in range(-1, 2):
bullets.append(Bullet(cx, cy, math.cos(a+k*0.25)*speed, math.sin(a+k*0.25)*speed, ci))
从四条边随机位置向玩家发射三联弹,0.25 弧度(约 14°)偏角形成小扇面覆盖,容错空间极小。四边同时可能出现发射源,叠加前期残余弹幕,后期画面弹幕密度最高。
弹幕调度与难度曲线
phase_timer += 1
if phase_timer > 300 + phase * 20:
phase = (phase + 1) % 5
phase_timer = 0
interval = max(8, 30 - score//30)
spd = 2.5 + score * 0.008
每个阶段持续时间随阶段编号递增(300、320、340……帧),使前期阶段切换更频繁、后期单一模式持续更久,考验玩家对特定弹幕的应对深度。发射间隔从 30 帧压缩至最低 8 帧(密度上限约为原来的 4 倍);速度线性递增无上限,理论上游戏难度可以无限提升。
碰撞检测
if math.hypot(b.x-px, b.y-py) < PLAYER_R + b.r - 2:
bullets.remove(b)
hp -= 1
invincible = INVINCIBLE_TIME
...
使用 math.hypot 计算圆心距,与两圆半径之和比较。-2 的容差有意将有效碰撞半径缩小 2px,给玩家略多的"擦边而过"空间,降低误判带来的挫败感——这是弹幕游戏的通行设计惯例。无敌帧期间完全跳过碰撞检测,受击子弹不被移除(避免无敌时无意"吃掉"更多子弹后重置无敌计时)。
玩家渲染与无敌反馈
pc = C_PLAYER if invincible == 0 else (
C_SHIELD if shield > 0 else
((255,255,255) if (invincible//5)%2 else C_PLAYER)
)
# 光晕
glow = pygame.Surface((PLAYER_R*6, PLAYER_R*6), pygame.SRCALPHA)
pygame.draw.circle(glow, (*pc, 40), (PLAYER_R*3, PLAYER_R*3), PLAYER_R*3)
screen.blit(glow, (px-PLAYER_R*3, py-PLAYER_R*3))
pygame.draw.circle(screen, pc, (px, py), PLAYER_R)
pygame.draw.circle(screen, C_PLAYER_C, (px, py), 3)
无敌状态下玩家每 5 帧切换一次颜色(白色/原色交替),产生规律闪烁,让玩家和观看者清晰感知无敌持续时间。内圈叠加浅色中心点强调判定核心位置,提醒玩家真正需要避开的是这个核心。半径 3 倍的半透明光晕以 alpha=40 叠加,增强玩家在深色背景上的辨识度。
星空背景
STARS = [(random.randint(0,W), random.randint(0,H), random.random()*2+1) for _ in range(80)]
def draw_stars(surf, t):
for sx, sy, sr in STARS:
a = int((math.sin(t*0.02 + sx) * 0.5 + 0.5) * 180 + 60)
pygame.draw.circle(surf, (a, a, a), (sx, sy), int(sr))
80 颗星点在初始化时一次性随机生成并固定位置,运行时零额外内存分配。亮度以 math.sin(t*0.02 + sx) 随帧数缓慢变化,每颗星的 sx 作为相位偏移,使星点彼此错开闪烁节奏而非同步。亮度映射到 60–240 区间,避免全灭或过曝。
绘制层次
绘制顺序为:星空背景 → 粒子 → 子弹(含拖尾)→ 玩家(光晕 + 实体) → UI(分数/HP/阶段提示/最高分)→ 游戏结束遮罩。严格保证层次正确,玩家始终在子弹之上以便定位,UI 文字不被弹幕遮挡。
HP 圆形图标
for i in range(max_hp):
hc = (C_HP if i < hp else (40, 40, 60))
pygame.draw.circle(screen, hc, (20 + i*26, 24), 10)
if i < hp:
pygame.draw.circle(screen, (200,255,220), (20 + i*26, 24), 4)
以离散圆形代替条状血条,每格代表一条命,视觉上比百分比更直观。现存 HP 内叠加浅色中心点,视觉上强调"满格"状态;空格以暗色填充表示消耗,无需额外文字说明。
游戏结束弹窗
lines = [
(FONT_LG, "GAME OVER", C_BULLET[0], -55),
(FONT_MD, f"生存时间:{score} 分", C_TEXT, 0),
(FONT_MD, f"最高纪录:{best} 分", C_STAR, 35),
(FONT_SM, "点击鼠标重新开始", C_GREY, 80),
]
for font, text, color, dy in lines:
t = font.render(text, True, color)
screen.blit(t, t.get_rect(centerx=W//2, centery=H//2+dy))
best 变量在整个进程生命周期内保留最高分(死亡后不重置),鼓励玩家反复挑战自己记录。弹窗背景以半透明黑色遮罩叠加在游戏画面之上而非清空屏幕,让玩家在结算界面仍能看到"最后的弹幕",增强现场感。
主循环:
while True:
mx, my = pygame.mouse.get_pos()
dt = clock.tick(FPS) / 1000.0
frame += 1
score = frame // 6
score 直接由帧数整除 6 换算(60FPS 下约每 0.1 秒 +1 分),避免浮点累加误差,也使分数增长速率在任何帧率下保持一致。鼠标坐标每帧顶部获取一次,保证玩家位置与系统光标严格同步。
全部代码
"""
躲避弹幕 — 操控小点在满屏弹幕中生存
操作:鼠标移动控制玩家,生存越久分越高
弹幕模式随时间切换:散射、旋转、追踪、扇形
"""
import pygame
import sys
import random
import math
import time
pygame.init()
W, H = 600, 680
FPS = 60
C_BG = (8, 5, 20)
C_PLAYER = (80, 220, 255)
C_PLAYER_C = (200, 240, 255)
C_BULLET = [(255, 80, 120), (255, 180, 50), (120, 255, 150), (180, 100, 255)]
C_TEXT = (220, 220, 255)
C_GREY = (100, 100, 140)
C_HP = (80, 200, 120)
C_HP_LOW = (220, 80, 60)
C_SHIELD = (100, 180, 255)
C_STAR = (255, 240, 100)
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"
FONT_LG = pygame.font.Font(CHINESE_FONT_PATH, 32)
FONT_MD = pygame.font.Font(CHINESE_FONT_PATH, 20)
FONT_SM = pygame.font.Font(CHINESE_FONT_PATH, 15)
PLAYER_R = 8
BULLET_R = 5
INVINCIBLE_TIME = 90 # 帧
SHIELD_TIME = 180
class Bullet:
def __init__(self, x, y, vx, vy, color_idx=0, r=BULLET_R):
self.x, self.y = float(x), float(y)
self.vx, self.vy = vx, vy
self.color = C_BULLET[color_idx % len(C_BULLET)]
self.r = r
self.trail = []
def update(self):
self.trail.append((self.x, self.y))
if len(self.trail) > 6:
self.trail.pop(0)
self.x += self.vx
self.y += self.vy
def alive(self):
return -50 < self.x < W+50 and -50 < self.y < H+50
def draw(self, surf):
for i, (tx, ty) in enumerate(self.trail):
a = int(40 * (i+1) / len(self.trail))
r = max(1, self.r - 2)
s = pygame.Surface((r*2+1, r*2+1), pygame.SRCALPHA)
pygame.draw.circle(s, (*self.color, a), (r, r), r)
surf.blit(s, (tx-r, ty-r))
pygame.draw.circle(surf, self.color, (int(self.x), int(self.y)), self.r)
pygame.draw.circle(surf, (255,255,255,200), (int(self.x), int(self.y)), max(1, self.r-2), 1)
class Particle:
def __init__(self, x, y, color=(255,255,255)):
self.x, self.y = x, y
a = random.uniform(0, math.pi*2)
s = random.uniform(1, 5)
self.vx, self.vy = math.cos(a)*s, math.sin(a)*s
self.life = 1.0
self.color = color
self.r = random.randint(2, 5)
def update(self):
self.x += self.vx
self.y += self.vy
self.vx *= 0.92
self.vy *= 0.92
self.life -= 0.03
return self.life > 0
def draw(self, surf):
a = int(self.life * 255)
s = pygame.Surface((self.r*2+1, self.r*2+1), pygame.SRCALPHA)
pygame.draw.circle(s, (*self.color, a), (self.r, self.r), self.r)
surf.blit(s, (int(self.x)-self.r, int(self.y)-self.r))
# ── 弹幕模式 ─────────────────────────────────────────────────────────
def spawn_scatter(cx, cy, count, speed, ci):
bullets = []
for i in range(count):
a = (2*math.pi*i/count)
bullets.append(Bullet(cx, cy, math.cos(a)*speed, math.sin(a)*speed, ci))
return bullets
def spawn_aimed(px, py, ox, oy, speed, spread=3, ci=0):
bullets = []
dx, dy = px-ox, py-oy
dist = math.hypot(dx, dy) or 1
for s in range(-spread, spread+1):
a = math.atan2(dy, dx) + s * 0.15
bullets.append(Bullet(ox, oy, math.cos(a)*speed, math.sin(a)*speed, ci))
return bullets
def spawn_fan(cx, cy, base_angle, count, spread, speed, ci):
bullets = []
for i in range(count):
a = base_angle - spread/2 + spread*i/(max(1,count-1))
bullets.append(Bullet(cx, cy, math.cos(a)*speed, math.sin(a)*speed, ci))
return bullets
# ── 星星背景 ─────────────────────────────────────────────────────────
STARS = [(random.randint(0,W), random.randint(0,H), random.random()*2+1) for _ in range(80)]
def draw_stars(surf, t):
for sx, sy, sr in STARS:
a = int((math.sin(t*0.02 + sx) * 0.5 + 0.5) * 180 + 60)
pygame.draw.circle(surf, (a, a, a), (sx, sy), int(sr))
# ── 主循环 ────────────────────────────────────────────────────────────
def main():
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("躲避弹幕")
clock = pygame.time.Clock()
pygame.mouse.set_visible(False)
bullets = []
particles = []
score = 0
best = 0
hp = 5
max_hp = 5
invincible = 0
shield = 0
frame = 0
game_over = False
started = False
spawn_timer = 0
phase_timer = 0
phase = 0
spin_angle = 0
px, py = W//2, H//2
def reset():
nonlocal bullets, particles, score, hp, invincible, shield
nonlocal frame, game_over, started, spawn_timer, phase_timer, phase, spin_angle
bullets.clear()
particles.clear()
score = 0; hp = max_hp; invincible = 0; shield = 0
frame = 0; game_over = False; started = True
spawn_timer = 0; phase_timer = 0; phase = 0; spin_angle = 0
while True:
mx, my = pygame.mouse.get_pos()
for event in pygame.event.get():
if event.type == pygame.QUIT:
pygame.mouse.set_visible(True)
pygame.quit(); sys.exit()
if event.type == pygame.KEYDOWN:
if event.key == pygame.K_ESCAPE:
pygame.mouse.set_visible(True)
pygame.quit(); sys.exit()
if event.type == pygame.MOUSEBUTTONDOWN:
if not started or game_over:
reset()
if started and not game_over:
px, py = mx, my
frame += 1
score = frame // 6
if invincible > 0: invincible -= 1
if shield > 0: shield -= 1
# 切换弹幕阶段
phase_timer += 1
if phase_timer > 300 + phase * 20:
phase = (phase + 1) % 5
phase_timer = 0
spawn_timer += 1
interval = max(8, 30 - score//30)
ci = phase % len(C_BULLET)
spd = 2.5 + score * 0.008
if spawn_timer >= interval:
spawn_timer = 0
if phase == 0: # 四面散射
cx, cy = random.choice([W//2, 0, W, 0, W]), random.choice([0, H, H//2, 0, H])
bullets += spawn_scatter(cx, cy, 8+score//60, spd, ci)
elif phase == 1: # 追踪扇形
ex = random.choice([0, W])
ey = random.randint(100, H-100)
bullets += spawn_aimed(px, py, ex, ey, spd, 2, ci)
elif phase == 2: # 旋转
spin_angle += 0.3
for j in range(3):
a = spin_angle + j * 2.1
bullets.append(Bullet(W//2, H//2, math.cos(a)*spd, math.sin(a)*spd, ci))
elif phase == 3: # 顶部扇形
ca = math.pi/2 + random.uniform(-0.5, 0.5)
bullets += spawn_fan(random.randint(50,W-50), -10, ca, 7+score//80, 1.2, spd, ci)
elif phase == 4: # 全方向密集
edge = random.randint(0, 3)
if edge==0: cx,cy=random.randint(0,W),0
elif edge==1: cx,cy=W,random.randint(0,H)
elif edge==2: cx,cy=random.randint(0,W),H
else: cx,cy=0,random.randint(0,H)
dx,dy=px-cx,py-cy; dist=math.hypot(dx,dy) or 1
a=math.atan2(dy,dx)
for k in range(-1,2):
bullets.append(Bullet(cx,cy, math.cos(a+k*0.25)*spd, math.sin(a+k*0.25)*spd, ci))
# 更新子弹
for b in bullets[:]:
b.update()
if not b.alive():
bullets.remove(b)
elif invincible == 0 and shield == 0:
if math.hypot(b.x-px, b.y-py) < PLAYER_R + b.r - 2:
bullets.remove(b)
hp -= 1
invincible = INVINCIBLE_TIME
for _ in range(20):
particles.append(Particle(px, py, (255,100,100)))
if hp <= 0:
game_over = True
best = max(best, score)
particles = [p for p in particles if p.update()]
# ── 绘制 ──────────────────────────────────────────────────────
screen.fill(C_BG)
draw_stars(screen, frame)
if started:
for p in particles: p.draw(screen)
for b in bullets: b.draw(screen)
# 玩家
if not game_over:
pc = C_PLAYER if invincible == 0 else (
C_SHIELD if shield > 0 else
((255,255,255) if (invincible//5)%2 else C_PLAYER)
)
# 光晕
glow = pygame.Surface((PLAYER_R*6, PLAYER_R*6), pygame.SRCALPHA)
pygame.draw.circle(glow, (*pc, 40), (PLAYER_R*3, PLAYER_R*3), PLAYER_R*3)
screen.blit(glow, (px-PLAYER_R*3, py-PLAYER_R*3))
pygame.draw.circle(screen, pc, (px, py), PLAYER_R)
pygame.draw.circle(screen, C_PLAYER_C, (px, py), 3)
# UI
st = FONT_LG.render(f"{score}", True, C_TEXT)
screen.blit(st, st.get_rect(centerx=W//2, y=8))
# HP
for i in range(max_hp):
hc = (C_HP if i < hp else (40, 40, 60))
pygame.draw.circle(screen, hc, (20 + i*26, 24), 10)
if i < hp:
pygame.draw.circle(screen, (200,255,220), (20 + i*26, 24), 4)
# 阶段提示
phase_names = ["散射", "追踪", "旋转", "扇形", "密集"]
pt = FONT_SM.render(f"模式:{phase_names[phase]}", True, C_GREY)
screen.blit(pt, (W-120, 8))
# 最高分
bt = FONT_SM.render(f"最高:{best}", True, C_GREY)
screen.blit(bt, (W-100, 28))
# 游戏结束
if game_over:
ov = pygame.Surface((W, H), pygame.SRCALPHA)
ov.fill((0,0,0,150))
screen.blit(ov, (0,0))
box = pygame.Rect(W//2-160, H//2-100, 320, 210)
pygame.draw.rect(screen, (15,10,35), box, border_radius=16)
pygame.draw.rect(screen, C_BULLET[0], box, 2, border_radius=16)
lines = [
(FONT_LG, "GAME OVER", C_BULLET[0], -55),
(FONT_MD, f"生存时间:{score} 分", C_TEXT, 0),
(FONT_MD, f"最高纪录:{best} 分", C_STAR, 35),
(FONT_SM, "点击鼠标重新开始", C_GREY, 80),
]
for font, text, color, dy in lines:
t = font.render(text, True, color)
screen.blit(t, t.get_rect(centerx=W//2, centery=H//2+dy))
else:
# 开始界面
t1 = FONT_LG.render("☄ 躲避弹幕", True, C_PLAYER)
t2 = FONT_MD.render("移动鼠标控制白点", True, C_TEXT)
t3 = FONT_SM.render("点击鼠标开始", True, C_GREY)
screen.blit(t1, t1.get_rect(centerx=W//2, centery=H//2-60))
screen.blit(t2, t2.get_rect(centerx=W//2, centery=H//2))
screen.blit(t3, t3.get_rect(centerx=W//2, centery=H//2+50))
pygame.display.flip()
clock.tick(FPS)
if __name__ == "__main__":
main()
以上就是Python通过Pygame实现一个简化版弹幕射击躲避游戏的详细内容,更多关于Python Pygame躲避弹幕游戏的资料请关注脚本之家其它相关文章!
相关文章
新手学习Python2和Python3中print不同的用法
在本篇文章里小编给大家分享的是关于Python2和Python3中print不同的用法,有兴趣的朋友们可以学习下。2020-06-06


最新评论