Python使用Pygame实现一个简易节奏点击游戏
项目概述
本文通过 Pygame 实现一个简化版节奏音游(Rhythm Game)。
游戏画面由四条纵向轨道组成,音符从顶部不断下落,在抵达底部判定线时按下对应按键即可得分。连击越高分数倍率越大,随游戏时间推进音符下落速度自动加快,血量耗尽(Miss 过多)即游戏结束。其中:
- 四轨操作:D、F、J、K 四键分别对应四条轨道,符合主流音游的手指分布习惯。
- 判定等级:落点与判定线偏差 <20px 为 Perfect(得 300 分),<50px 为 Good(得 100 分),超出范围未接为 Miss(扣血 10 点)。
- 连击倍率:当前连击数每满 10 次,分数倍率 +1,鼓励保持连击。
- 变速机制:音符基础速度为 5px/帧,随游戏时长线性递增(每秒 +0.03px/帧),越到后期越考验反应。
- 随机节拍:每拍随机选取 1–2 条轨道生成音符,下一拍间隔在 0.5、1.0、1.5 拍中随机选取,节奏感丰富不单调。
- 音效反馈:命中时播放对应轨道音调的短促音效(由程序实时合成,无需外部音频文件)。
- 粒子特效:命中时在判定线处爆发彩色粒子,带有重力与透明度衰减,视觉反馈直观。
- 血量系统:HP 条实时显示,低于 30 时变红警示;归零即触发游戏结束画面。
- 键盘操作:D/F/J/K:打击对应轨道;R:重新开始;ESC:退出。
游戏实现

初始化与基础设置
游戏启动时初始化 Pygame 及混音器,并定义窗口尺寸、轨道布局和颜色常量。
pygame.init() pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512) W, H = 480, 680 FPS = 60 LANES = 4 LANE_W = 90 LANE_GAP = 10 TOTAL_W = LANES * LANE_W + (LANES-1) * LANE_GAP OFFSET_X = (W - TOTAL_W) // 2 JUDGE_Y = H - 110 NOTE_H = 22 NOTE_SPD = 5
pygame.mixer.init 的参数需精确指定:frequency=44100(CD 音质采样率)、size=-16(有符号 16 位整数)、channels=2(立体声)、buffer=512(小缓冲区降低打击延迟)。四条轨道居中排布,OFFSET_X 自动计算使轨道组水平居中于窗口。
颜色定义
C_BG = (15, 12, 30) # 深紫黑背景 C_LANE = (30, 25, 55) # 轨道底色 C_NOTE = [(220,80,120),(80,180,220),(120,220,100),(220,180,60)] # 四轨音符色 C_PERFECT = (255, 230, 50) # Perfect 判定文字金黄 C_GOOD = (100, 220, 255) # Good 判定文字蓝 C_MISS = (200, 60, 60) # Miss / 游戏结束红 C_KEY = [(180,50,90),(50,140,180),(80,180,70),(180,140,40)] # 四轨键位底色
整体采用深色背景搭配高饱和度的四色轨道,与音游常见的霓虹风格保持一致;判定颜色则沿用大多数音游的金(Perfect)/ 蓝(Good)/ 红(Miss)视觉惯例,让玩家无需学习即可理解。
字体加载
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc" FONT_LG = pygame.font.Font(CHINESE_FONT_PATH, 36) # 分数 FONT_MD = pygame.font.Font(CHINESE_FONT_PATH, 22) # 连击 / 键位标签 FONT_SM = pygame.font.Font(CHINESE_FONT_PATH, 16) # 小说明文字 FONT_SC = pygame.font.Font(CHINESE_FONT_PATH, 48) # 结束画面大字
音效合成
本游戏不依赖任何外部音频文件,所有音效均由程序实时生成。
def gen_beep(freq=440, duration=0.08, vol=0.4):
sr = 44100
n = int(sr * duration)
buf = bytearray(n * 4)
for i in range(n):
t = i / sr
v = math.sin(2 * math.pi * freq * t) # 正弦波
env = 1.0 - i / n # 线性衰减包络
s = int(v * env * vol * 32767)
s = max(-32768, min(32767, s))
buf[i*4] = s & 0xFF
buf[i*4+1] = (s >> 8) & 0xFF
buf[i*4+2] = s & 0xFF
buf[i*4+3] = (s >> 8) & 0xFF
return _make_sound(buf, sr, n)
HIT_SOUNDS = [gen_beep(f) for f in [330, 392, 440, 523]]
核心思路:用 math.sin 生成指定频率的正弦波,乘以线性衰减包络 (1 - i/n) 使音符结尾自然收音,避免爆音。四条轨道对应 Do–Sol–La–Do(330/392/440/523 Hz)四个音调,命中时声音各有不同,兼具功能性与音乐感。
生成的字节流通过 numpy 转换为 Pygame 可播放的 Sound 对象。若环境未安装 numpy,则静默降级为无音效模式,游戏仍可正常运行。
粒子系统
class Particle:
def __init__(self, x, y, color):
self.x, self.y = x, y
self.vx = random.uniform(-3, 3)
self.vy = random.uniform(-5, -1)
self.life = 1.0
self.color = color
self.r = random.randint(3, 7)
def update(self):
self.x += self.vx
self.y += self.vy
self.vy += 0.2 # 重力加速度
self.life -= 0.04
return self.life > 0 # 返回 False 表示粒子已消亡
def draw(self, surf):
a = int(self.life * 255)
s = pygame.Surface((self.r*2, self.r*2), pygame.SRCALPHA)
pygame.draw.circle(s, (*self.color, a), (self.r, self.r), self.r)
surf.blit(s, (self.x - self.r, self.y - self.r))
粒子初始化时随机设定水平速度与向上的初速度,update 中模拟重力(每帧 vy += 0.2),使粒子呈抛物线轨迹散开;life 从 1.0 每帧减少 0.04,约 25 帧(0.4 秒)后消亡,并同步映射到 alpha 透明度,产生自然淡出效果。
命中时在判定线位置一次性生成 12 颗粒子,颜色与轨道音符色一致,强化每条轨道的视觉辨识度:
for _ in range(12):
particles.append(Particle(lx, JUDGE_Y, C_NOTE[li]))
主循环中用列表推导式原地过滤消亡粒子,保持写法简洁:
particles = [p for p in particles if p.update()]
音符生成与节拍调度
def spawn_note(lane, speed):
return {"lane": lane, "y": -NOTE_H, "speed": speed}
音符以字典形式存储,y 初始为 -NOTE_H(屏幕顶部之外),避免生成瞬间可见的跳变。
节拍调度在主循环中通过 frame_time 累计游戏时间控制:
if frame_time >= next_beat:
lane_count = random.randint(1, 2)
lanes_to_spawn = random.sample(range(LANES), lane_count)
for l in lanes_to_spawn:
notes.append(spawn_note(l, speed))
total_notes += 1
next_beat = frame_time + beat_interval * random.choice([0.5, 1.0, 1.0, 1.5])
speed = NOTE_SPD + frame_time * 0.03
每拍随机决定本次生成 1 或 2 个音符,再从四条轨道中无重复地抽取(random.sample 保证同一时刻同一轨道不出现两个音符)。下一拍间隔在 0.5/1.0/1.5 拍中随机选取,其中 1.0 拍权重更高,兼顾规律感与变化性。速度公式 NOTE_SPD + frame_time * 0.03 使难度随时间平滑线性递增。
判定系统
for li, k in enumerate(KEYS):
if event.key == k:
key_press_alpha[li] = 1.0
targets = [n for n in notes if n["lane"] == li]
if targets:
closest = min(targets, key=lambda n: abs(n["y"] - JUDGE_Y))
dist = abs(closest["y"] - JUDGE_Y)
if dist < 50:
rating = "Perfect" if dist < 20 else "Good"
notes.remove(closest)
sc = 300 if rating == "Perfect" else 100
combo += 1
score += sc * (1 + combo // 10)
...
按键触发时,先筛选出当前轨道所有音符,再取距判定线最近的一个作为目标(避免多音符同轨时误判),按偏差距离分级:<20px 为 Perfect,<50px 为 Good,超出范围视为空按(不扣分但不得分)。超过判定线 60px 的音符在 update 中自动判为 Miss 并扣血:
if n["y"] > JUDGE_Y + 60:
notes.remove(n)
stats["Miss"] += 1
combo = 0
hp -= 10
连击加分公式 sc * (1 + combo // 10) 使连击每满 10 次倍率递增 1,高连击时分数增长显著加速,提供充足的追分动力。
绘制方法
按键高亮与轨道
for i in range(LANES):
lx = OFFSET_X + i * (LANE_W + LANE_GAP)
col = tuple(min(255, c + int(key_press_alpha[i] * 40)) for c in C_LANE)
pygame.draw.rect(screen, col, (lx, 60, LANE_W, H-160), border_radius=6)
kc = tuple(min(255, c + int(key_press_alpha[i] * 80)) for c in C_KEY[i])
kr = pygame.Rect(lx, JUDGE_Y + NOTE_H + 6, LANE_W, 38)
pygame.draw.rect(screen, kc, kr, border_radius=8)
key_press_alpha 在按键时置 1.0,每帧减少 0.15(约 7 帧淡出),轨道背景色和键位按钮色均叠加该值对应的亮度偏移,形成按下时一亮随即渐暗的视觉反馈,而无需单独管理高亮状态。
音符渲染
r = pygame.Rect(lx + 4, n["y"], LANE_W - 8, NOTE_H)
pygame.draw.rect(screen, C_NOTE[i], r, border_radius=6)
pygame.draw.rect(screen, tuple(min(255, c+60) for c in C_NOTE[i]),
(r.x+4, r.y+3, r.w-8, 5), border_radius=3)
音符两侧各缩进 4px 与轨道边缘留出间距;顶部叠加一条更亮的高光条(颜色各通道 +60,宽度更窄),模拟光泽感,使平面矩形显得更有立体质感。
判定文字动画
if judge_timer > 0:
alpha = min(255, judge_timer * 8)
jt = FONT_MD.render(judge_text, True, judge_color)
screen.blit(jt, jt.get_rect(centerx=W//2, y=JUDGE_Y - 50))
judge_timer -= 1
judge_timer 在命中时设为 40(Perfect/Good)或 30(Miss),每帧 -1 直至归零,文字随之淡出。显示位置固定在判定线上方,不遮挡音符下落区域。
HP 条
hp_w = int((W - 40) * hp / 100)
pygame.draw.rect(screen, (60, 30, 30), (20, H-40, W-40, 16), border_radius=8)
pygame.draw.rect(screen, (220,80,80) if hp < 30 else (80,200,100),
(20, H-40, hp_w, 16), border_radius=8)
先绘制深红色底条(满宽),再绘制随 HP 值缩短的前景条。HP 低于 30 时前景色由绿转红,给玩家直观的危险警示。
游戏结束遮罩
if game_over:
ov = pygame.Surface((W, H), pygame.SRCALPHA)
ov.fill((0, 0, 0, 160))
screen.blit(ov, (0, 0))
box = pygame.Rect(W//2-170, H//2-110, 340, 230)
pygame.draw.rect(screen, (25, 20, 50), box, border_radius=16)
pygame.draw.rect(screen, C_PERFECT, box, 2, border_radius=16)
lines = [
(FONT_LG, "GAME OVER", C_MISS, -70),
(FONT_MD, f"分数: {score:,}", C_TEXT, -20),
(FONT_SM, f"Perfect/Good/Miss 统计", C_GREY, 20),
(FONT_SM, f"最高连击: {max_combo}", C_PERFECT, 50),
(FONT_SM, "按 R 重新开始", C_TEXT, 90),
]
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))
结束弹窗通过 lines 列表统一管理各行文字的字体、内容、颜色和垂直偏移,便于增减条目而不影响整体布局。
绘制顺序为:背景 → 轨道 → 判定线 → 音符 → 粒子 → UI(分数/连击/HP/判定文字)→ 游戏结束遮罩,严格保证层次正确。
主循环 main:
while True:
dt = clock.tick(FPS) / 1000.0
frame_time += dt
# 事件处理 ...
# 逻辑更新 ...
# 绘制 ...
pygame.display.flip()
clock.tick(FPS) 返回距上次调用的毫秒数,转换为秒后累加到 frame_time,所有时间相关逻辑(节拍调度、速度计算)均基于此,与帧率无关,保证不同性能机器上游戏节奏一致。
全部代码
"""
节奏点击 — 简化音游
下落音符到达判定线时按对应键(D F J K)
评分:Perfect / Good / Miss
"""
import pygame
import sys
import random
import math
pygame.init()
pygame.mixer.init(frequency=44100, size=-16, channels=2, buffer=512)
W, H = 480, 680
FPS = 60
# 轨道设置
LANES = 4
LANE_W = 90
LANE_GAP = 10
TOTAL_W = LANES * LANE_W + (LANES-1) * LANE_GAP
OFFSET_X = (W - TOTAL_W) // 2
JUDGE_Y = H - 110
NOTE_H = 22
NOTE_SPD = 5 # 像素/帧(基础速度)
KEYS = [pygame.K_d, pygame.K_f, pygame.K_j, pygame.K_k]
KEY_LABELS = ["D", "F", "J", "K"]
C_BG = (15, 12, 30)
C_LANE = (30, 25, 55)
C_LANE_HL = (50, 40, 90)
C_NOTE = [(220, 80, 120), (80, 180, 220), (120, 220, 100), (220, 180, 60)]
C_JUDGE = (255, 255, 255)
C_PERFECT = (255, 230, 50)
C_GOOD = (100, 220, 255)
C_MISS = (200, 60, 60)
C_TEXT = (220, 220, 255)
C_GREY = (100, 100, 130)
C_KEY = [(180, 50, 90), (50, 140, 180), (80, 180, 70), (180, 140, 40)]
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"
FONT_LG = pygame.font.Font(CHINESE_FONT_PATH, 36)
FONT_MD = pygame.font.Font(CHINESE_FONT_PATH, 22)
FONT_SM = pygame.font.Font(CHINESE_FONT_PATH, 16)
FONT_SC = pygame.font.Font(CHINESE_FONT_PATH, 48)
# ── 音效生成 ─────────────────────────────────────────────────────────
def gen_beep(freq=440, duration=0.08, vol=0.4):
sr = 44100
n = int(sr * duration)
buf = bytearray(n * 4)
for i in range(n):
t = i / sr
v = math.sin(2 * math.pi * freq * t)
env = 1.0 - i / n
s = int(v * env * vol * 32767)
s = max(-32768, min(32767, s))
buf[i*4] = s & 0xFF
buf[i*4+1] = (s >> 8) & 0xFF
buf[i*4+2] = s & 0xFF
buf[i*4+3] = (s >> 8) & 0xFF
return pygame.sndarray.make_sound(
pygame.sndarray.array(pygame.mixer.Sound(buffer=bytes(buf)))
) if False else _make_sound(buf, sr, n)
def _make_sound(buf, sr, n):
import numpy as np
arr = np.frombuffer(bytes(buf), dtype=np.int16).reshape((n, 2))
snd = pygame.sndarray.make_sound(arr)
return snd
try:
import numpy as np
HIT_SOUNDS = [gen_beep(f) for f in [330, 392, 440, 523]]
except ImportError:
HIT_SOUNDS = [None] * 4
# ── 粒子系统 ─────────────────────────────────────────────────────────
class Particle:
def __init__(self, x, y, color):
self.x, self.y = x, y
self.vx = random.uniform(-3, 3)
self.vy = random.uniform(-5, -1)
self.life = 1.0
self.color = color
self.r = random.randint(3, 7)
def update(self):
self.x += self.vx
self.y += self.vy
self.vy += 0.2
self.life -= 0.04
return self.life > 0
def draw(self, surf):
a = int(self.life * 255)
s = pygame.Surface((self.r*2, self.r*2), pygame.SRCALPHA)
pygame.draw.circle(s, (*self.color, a), (self.r, self.r), self.r)
surf.blit(s, (self.x - self.r, self.y - self.r))
# ── 音符生成器 ───────────────────────────────────────────────────────
def spawn_note(lane, speed):
return {"lane": lane, "y": -NOTE_H, "speed": speed}
# ── 主循环 ────────────────────────────────────────────────────────────
def main():
screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("节奏点击")
clock = pygame.time.Clock()
notes = []
particles = []
score = 0
combo = 0
max_combo = 0
stats = {"Perfect": 0, "Good": 0, "Miss": 0}
game_over = False
total_notes = 0
key_press_alpha = [0.0] * LANES # 按键高亮
# 生成节拍时间表(BPM=120,变速)
bpm = 120
beat_interval = 60 / bpm # 秒/拍
next_beat = 0.5
frame_time = 0.0
speed = NOTE_SPD
judge_text = ""
judge_color = C_PERFECT
judge_timer = 0
hp = 100 # 血量
while True:
dt = clock.tick(FPS) / 1000.0
frame_time += dt
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_ESCAPE:
pygame.quit(); sys.exit()
if event.key == pygame.K_r and game_over:
main(); return
if not game_over:
for li, k in enumerate(KEYS):
if event.key == k:
key_press_alpha[li] = 1.0
# 找最近的音符
targets = [n for n in notes if n["lane"] == li]
if targets:
closest = min(targets, key=lambda n: abs(n["y"] - JUDGE_Y))
dist = abs(closest["y"] - JUDGE_Y)
if dist < 50:
rating = "Perfect" if dist < 20 else "Good"
notes.remove(closest)
sc = 300 if rating == "Perfect" else 100
combo += 1
max_combo = max(max_combo, combo)
score += sc * (1 + combo // 10)
stats[rating] += 1
judge_text = rating
judge_color = C_PERFECT if rating == "Perfect" else C_GOOD
judge_timer = 40
lx = OFFSET_X + li*(LANE_W+LANE_GAP) + LANE_W//2
for _ in range(12):
particles.append(Particle(lx, JUDGE_Y, C_NOTE[li]))
if HIT_SOUNDS[li]:
HIT_SOUNDS[li].play()
if not game_over:
# 生成音符
if frame_time >= next_beat:
lane_count = random.randint(1, 2)
lanes_to_spawn = random.sample(range(LANES), lane_count)
for l in lanes_to_spawn:
notes.append(spawn_note(l, speed))
total_notes += 1
next_beat = frame_time + beat_interval * random.choice([0.5, 1.0, 1.0, 1.5])
# 加速
speed = NOTE_SPD + frame_time * 0.03
# 更新音符
for n in notes[:]:
n["y"] += n["speed"]
if n["y"] > JUDGE_Y + 60:
notes.remove(n)
stats["Miss"] += 1
combo = 0
judge_text = "MISS"
judge_color = C_MISS
judge_timer = 30
hp -= 10
if hp <= 0:
game_over = True
# 更新粒子
particles = [p for p in particles if p.update()]
# 按键高亮淡出
for i in range(LANES):
key_press_alpha[i] = max(0, key_press_alpha[i] - 0.15)
if judge_timer > 0:
judge_timer -= 1
# ── 绘制 ──────────────────────────────────────────────────────
screen.fill(C_BG)
# 轨道
for i in range(LANES):
lx = OFFSET_X + i*(LANE_W+LANE_GAP)
col = tuple(min(255, c + int(key_press_alpha[i]*40)) for c in C_LANE)
pygame.draw.rect(screen, col, (lx, 60, LANE_W, H-160), border_radius=6)
# 键位标签
kc = tuple(min(255, c+int(key_press_alpha[i]*80)) for c in C_KEY[i])
kr = pygame.Rect(lx, JUDGE_Y+NOTE_H+6, LANE_W, 38)
pygame.draw.rect(screen, kc, kr, border_radius=8)
kt = FONT_MD.render(KEY_LABELS[i], True, C_TEXT)
screen.blit(kt, kt.get_rect(center=kr.center))
# 判定线
pygame.draw.rect(screen, C_JUDGE, (OFFSET_X-4, JUDGE_Y-2, TOTAL_W+8, 5), border_radius=3)
# 音符
for n in notes:
i = n["lane"]
lx = OFFSET_X + i*(LANE_W+LANE_GAP)
r = pygame.Rect(lx+4, n["y"], LANE_W-8, NOTE_H)
pygame.draw.rect(screen, C_NOTE[i], r, border_radius=6)
# 高光
pygame.draw.rect(screen, tuple(min(255,c+60) for c in C_NOTE[i]),
(r.x+4, r.y+3, r.w-8, 5), border_radius=3)
# 粒子
for p in particles:
p.draw(screen)
# UI
sc_t = FONT_LG.render(f"{score:,}", True, C_TEXT)
screen.blit(sc_t, sc_t.get_rect(centerx=W//2, y=8))
combo_t = FONT_MD.render(f"× {combo}" if combo > 1 else "", True, C_PERFECT)
screen.blit(combo_t, combo_t.get_rect(x=10, y=50))
# HP条
hp_w = int((W-40) * hp / 100)
pygame.draw.rect(screen, (60,30,30), (20, H-40, W-40, 16), border_radius=8)
pygame.draw.rect(screen, (220,80,80) if hp < 30 else (80,200,100),
(20, H-40, hp_w, 16), border_radius=8)
# 判定文字
if judge_timer > 0:
alpha = min(255, judge_timer * 8)
jt = FONT_MD.render(judge_text, True, judge_color)
screen.blit(jt, jt.get_rect(centerx=W//2, y=JUDGE_Y-50))
# 时间/进度
ft_t = FONT_SM.render(f"⏱ {int(frame_time)}s 速度×{speed/NOTE_SPD:.1f}", True, C_GREY)
screen.blit(ft_t, (10, H-60))
# 游戏结束
if game_over:
ov = pygame.Surface((W, H), pygame.SRCALPHA)
ov.fill((0,0,0,160))
screen.blit(ov, (0,0))
box = pygame.Rect(W//2-170, H//2-110, 340, 230)
pygame.draw.rect(screen, (25,20,50), box, border_radius=16)
pygame.draw.rect(screen, C_PERFECT, box, 2, border_radius=16)
lines = [
(FONT_LG, "GAME OVER", C_MISS, -70),
(FONT_MD, f"分数: {score:,}", C_TEXT, -20),
(FONT_SM, f"Perfect:{stats['Perfect']} Good:{stats['Good']} Miss:{stats['Miss']}", C_GREY, 20),
(FONT_SM, f"最高连击: {max_combo}", C_PERFECT, 50),
(FONT_SM, "按 R 重新开始", C_TEXT, 90),
]
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))
pygame.display.flip()
if __name__ == "__main__":
main()
以上就是Python使用Pygame实现的简易节奏点击游戏的详细内容,更多关于Python Pygame节奏点击游戏的资料请关注脚本之家其它相关文章!
相关文章
win10下tensorflow和matplotlib安装教程
这篇文章主要为大家详细介绍了win10下tensorflow和matplotlib安装教程,具有一定的参考价值,感兴趣的小伙伴们可以参考一下2018-09-09
命令行传递参数argparse.ArgumentParser的使用解析
这篇文章主要介绍了命令行传递参数argparse.ArgumentParser的使用解析,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教2023-02-02


最新评论