ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

用 Python + Pygame 从零手写一个 Flappy Bird(附完整代码+粒子特效)

用 Python + Pygame 从零手写一个 Flappy Bird(附完整代码+粒子特效)

# 用 Python + Pygame 从零手写一个 Flappy Bird(附完整代码+粒子特效)

![运行效果](运行效果.gif)

> 只用了 **400 行纯 Python**,就做出了带视差背景、粒子爆炸、屏幕震动、小鸟旋转的 Flappy Bird。文末附完整可运行代码和打包 exe 的方法,建议收藏。

---

## 一、先看效果

这个版本和网上的"极简复刻"不一样,它带有:

| 特效 | 说明 |
|---|---|
| 🌄 视差背景 | 远山 / 近丘 / 地面三层不同速度滚动,有纵深感 |
| ✨ 粒子系统 | 翅膀拍动喷白粒、死亡爆炸 28 粒、得分爆金粒 |
| 💥 屏幕震动 + 白闪 | 撞管瞬间的打击感 |
| 🐦 小鸟旋转 + 残影 | 上升抬头、俯冲低头,还拖着尾迹 |

**核心机制不变**:空格 / 鼠标点击控制小鸟上下翻飞,穿过管道缝隙得分。

## 二、环境准备

```bash
pip install pygame
```

只需要这一个依赖,Python 3.8+ 即可。

## 三、整体框架:游戏循环

所有 Pygame 游戏的骨架都是一样的——**死循环里不断"更新逻辑 → 绘制画面"**:

```python
def run(self):
while True:
self.clock.tick(FPS) # 锁定 60 帧
for ev in pygame.event.get(): # 处理事件
if ev.type == pygame.QUIT:
pygame.quit(); sys.exit()
if ev.type == pygame.KEYDOWN:
if ev.key == pygame.K_r: self.reset(); continue
if ev.key in (pygame.K_SPACE, pygame.K_UP, pygame.K_w):
if self.state == "dead" and self.dtimer > 22: self.restart()
else: self.flap()
if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
if self.state == "dead" and self.dtimer > 22: self.restart()
else: self.flap()
self.update() # 更新逻辑
self.draw() # 绘制画面
```

几个设计点:

- **状态机**:游戏有 `ready / play / dead` 三种状态,`update()` 里按状态分支处理,逻辑清晰不混乱。
- **`update()` 和 `draw()` 分离**:逻辑和渲染解耦,这是游戏代码能长期维护的关键。
- **操作统一**:空格、W、↑、鼠标左键都能触发 `flap()`,死亡后按 R 或任意键重开。

## 四、小鸟物理:重力 + 跳跃 + 旋转

### 1. 重力与跳跃

物理学就两个公式:**每帧加速度 → 累加速度 → 改变位置**。

```python
# 常量
GRAVITY = 0.5 # 重力加速度
FLAP_FORCE = -8.5 # 每次扇翅的上升力(负值=向上)
```

```python
# 每帧更新(play 状态)
self.bv += GRAVITY # 速度 += 重力
self.by += self.bv # 位置 += 速度
```

```python
# 扇翅:直接把速度设成上升力
def flap(self):
if self.state == "ready":
self.state = "play"; self.bv = FLAP_FORCE; self.flap_t = 8
self.ptimer = 55; self.trail = []; return
if self.state == "play":
self.bv = FLAP_FORCE; self.flap_t = 8
```

### 2. 旋转动画(最容易忽略的细节)

光有上下移动太生硬,真实的小鸟是**上升时抬头、下落时低头**。这里用一个「目标角度 + 缓动」实现平滑转向:

```python
# 目标角度由当前速度决定:速度越负(向上)→ 抬头;越正(向下)→ 低头
tgt = max(-35, min(80, -self.bv * 3.5))
self.bang += (tgt - self.bang) * 0.18 # 每帧向目标角度靠近 18%
```

`* 0.18` 是缓动系数——角度永远追着目标走但不会瞬间到达,于是产生了自然的回摆效果。绘制时用 `pygame.transform.rotate` 旋转即可。

## 五、管道系统:生成、移动、计分

### 1. 定时生成

用计时器 `ptimer` 控制管道间隔,每次在屏幕右侧随机生成一对(上管 + 下管):

```python
self.ptimer += 1
if self.ptimer >= 82: # 每 82 帧生成一对
self.ptimer = 0
gy = random.randint(85, H - GROUND_H - 85 - PIPE_GAP)
self.pipes.append({'x': W + 10, 'gy': gy, 'ok': False})
for p in self.pipes: p['x'] -= PIPE_SPEED # 统一左移
self.pipes = [p for p in self.pipes if p['x'] > -PIPE_W - 10] # 出屏即回收
```

管道用一个 dict 存 **位置 `x` + 缺口中心 `gy` + 计分标记 `ok`**,数据比 OOP 的类更轻量。

### 2. 计分:防重复是精髓

`ok` 字段是计分的关键——每根管道**只会被计一次分**,用 `p['x'] + PIPE_W < self.bx` 判断"管道已完全经过小鸟":

```python
for p in self.pipes:
if not p['ok'] and p['x'] + PIPE_W < self.bx:
p['ok'] = True; self.score += 1
if self.score > self.high_score: self.high_score = self.score
# 顺便在屏幕顶部爆出一串金色粒子庆祝
for _ in range(10):
self.sparts.append({
'x': self.bx + random.randint(-10, 30),
'y': 40 + random.randint(-10, 10),
'vx': random.uniform(-3, 3), 'vy': random.uniform(-5, -1),
'life': 25, 'size': random.randint(2, 4),
'color': random.choice([C_GOLD, C_WHITE, (255, 255, 150)])
})
```

### 3. 碰撞检测:四重判定更真实

小鸟本体是一个 Rect,管道则拆成**4 个判定区**:上管身、下管身、上下管口的"凸缘"。凸缘比管身宽 8px、高 22px,这样撞到管口边缘也会判死,手感更严谨:

```python
br = pygame.Rect(self.bx - 11, self.by - 9, 22, 18)
if self.by + 9 >= H - GROUND_H or self.by - 9 <= 0: # 撞地 / 撞顶
self._die(); return
for p in self.pipes:
px, gy = p['x'], p['gy']
top = pygame.Rect(px, 0, PIPE_W, gy) # 上管身
bot = pygame.Rect(px, gy + PIPE_GAP, PIPE_W, H) # 下管身
top_c = pygame.Rect(px - 4, gy - 20, PIPE_W + 8, 22) # 上管口凸缘
bot_c = pygame.Rect(px - 4, gy + PIPE_GAP - 2, PIPE_W + 8, 22) # 下管口凸缘
if br.colliderect(top) or br.colliderect(bot) or \
br.colliderect(top_c) or br.colliderect(bot_c):
self._die(); return
```

## 六、🔥 特效进阶(这个版本的重头戏)

下面的几个技巧,能把"能玩的游戏"升级成"有手感的游戏"。

### 1. 视差背景:三层滚动造纵深

远山、近丘、地面分别以不同速度移动,人眼会产生"景深"错觉。核心是把一组随机点画成多边形山脊,再整体偏移:

```python
def _draw_parallax(self, pts, color, offset, brightness):
"""画一条可无缝循环的山脊线"""
c = tuple(int(v * brightness) for v in color)
strip_w = max(x for x, _ in pts) + 80
poly = []
for px, py in pts:
x = (px - offset) % strip_w - 40
poly.append((x, py))
poly.sort(key=lambda p: p[0])
full = [(0, H - GROUND_H)] + poly + [(W, H - GROUND_H)]
if len(full) >= 3:
pygame.draw.polygon(self.screen, c, [(int(x), int(y)) for x, y in full])
```

配合 `%` 取模实现**无缝循环**——山脊滚出屏幕左侧后从右侧原样接回来,肉眼看不出来。滚动速度由三处变量控制:

| 图层 | 速度 | 深度感 |
|---|---|---|
| 远山 `mtn_x` | `+0.25` | 最远,最慢 |
| 近丘 `hill_x` | `+0.55` | 中间 |
| 地面 `gx` | `-2`(每 48px 重复) | 最近,最快 |

### 2. 粒子系统:一份数据,三种用途

粒子本质就是"**一群带速度、带生命值的小圆点**",结构很简单:

```python
def _add_part(self, x, y, vx, vy, life, color, size):
self.parts.append({'x': x, 'y': y, 'vx': vx, 'vy': vy,
'life': life, 'color': color, 'size': size})
```

每帧统一更新:位置 += 速度、速度 += 重力、生命 -= 1,生命耗尽就移除:

```python
for p in self.parts:
p['x'] += p['vx']; p['y'] += p['vy']; p['vy'] += 0.15; p['life'] -= 1
self.parts = [p for p in self.parts if p['life'] > 0]
```

同一套机制驱动了三种效果:

- **翅膀拍动**:每次 `flap()` 往身后喷 3 粒小白点
- **死亡爆炸**:`_die()` 时沿 360° 均匀撒出 28 粒彩色碎片,带随机速度
- **得分庆祝**:计分时在顶部爆 10 粒金星

### 3. 屏幕震动 + 白闪 + 残影:打击感三件套

```python
# 震动:绘制前给所有物体加随机偏移
if self.shake > 0:
ox = random.randint(-self.shake_s, self.shake_s)
oy = random.randint(-self.shake_s, self.shake_s)
```

```python
# 白闪:半透明白色全屏覆盖,逐帧淡出
if self.flash > 0:
s = pygame.Surface((W, H), pygame.SRCALPHA)
s.fill((255, 255, 255, 30 * self.flash))
self.screen.blit(s, (0, 0)); self.flash -= 1
```

```python
# 残影:记录最近 10 帧位置,画成渐小渐透明的圆
self.trail.append((self.bx, self.by))
if len(self.trail) > 10: self.trail.pop(0)
```

撞管瞬间 `self.flash = 8; self.shake = 10`,一击即爆、屏幕震颤——玩家能明显"感觉到"自己死了。

## 七、完整代码(可直接运行)

```python
"""
Flappy Bird — Pygame
"""
import pygame
import random
import sys
import math

# ============================================================
W, H = 400, 600
FPS = 60
GRAVITY = 0.5
FLAP_FORCE = -8.5
PIPE_W = 58
PIPE_GAP = 145
PIPE_SPEED = 3
GROUND_H = 80

# Colors
C_SKY_TOP = (78, 175, 235)
C_SKY_BOT = (145, 210, 245)
C_PIPE = (80, 190, 65)
C_PIPE_SH = (60, 155, 48)
C_PIPE_HI = (115, 215, 100)
C_PIPE_CA = (70, 170, 55)
C_GROUND_G = (85, 175, 55)
C_GROUND_D = (65, 140, 40)
C_DIRT = (125, 90, 55)
C_DIRT_D = (100, 70, 40)
C_WHITE = (255, 255, 255)
C_BLACK = (0, 0, 0)
C_GOLD = (255, 215, 55)
C_SILVER = (200, 200, 215)
C_RED = (240, 75, 75)
C_BIRD_Y = (245, 215, 60)
C_BIRD_Y2 = (250, 235, 130)
C_BIRD_W = (215, 180, 40)
C_BIRD_BEAK = (225, 105, 45)
C_MTN1 = (90, 140, 80)
C_MTN2 = (70, 115, 60)
C_HILL = (80, 150, 65)

# ============================================================
def lerp_color(a, b, t):
return tuple(int(x + (y - x) * t) for x, y in zip(a, b))

def make_clouds():
return [{'x': random.randint(0, W), 'y': random.randint(30, 200),
'w': random.randint(55, 120), 'speed': random.uniform(0.15, 0.4)}
for _ in range(5)]

def make_mountains():
"""Return list of (x, peak_y) for one repeatable mountain strip."""
pts = []
x = -40
while x < W + 120:
pts.append((x, H - GROUND_H - random.randint(50, 120)))
x += random.randint(35, 70)
return pts

def make_hills():
pts = []
x = -30
while x < W + 100:
pts.append((x, H - GROUND_H - random.randint(25, 55)))
x += random.randint(20, 45)
return pts

# ============================================================
class FlappyBird:
def __init__(self):
pygame.init()
self.screen = pygame.display.set_mode((W, H))
pygame.display.set_caption("Flappy Bird")
self.clock = pygame.time.Clock()
self.font_lg = pygame.font.SysFont("consolas", 44, bold=True)
self.font_md = pygame.font.SysFont("consolas", 26, bold=True)
self.font_sm = pygame.font.SysFont("consolas", 17)

self.high_score = 0
self.clouds = make_clouds()
self.mtn_pts = make_mountains()
self.hill_pts = make_hills()
self.stars = [(random.randint(0, W), random.randint(0, H - GROUND_H - 30))
for _ in range(50)]
self.reset()

def reset(self):
self.bx = 80.0
self.by = H / 2
self.bv = 0.0
self.bang = 0.0
self.flap_t = 0
self.pipes = []
self.score = 0
self.state = "ready" # ready / play / dead
self.dtimer = 0
self.flash = 0
self.shake = 0
self.shake_s = 0
self.parts = [] # particles
self.sparts = [] # score particles
self.trail = []
self.gx = 0.0
self.mtn_x = 0.0
self.hill_x = 0.0
self.ptimer = 0
self.t = 0.0

# ---- helpers ----
def _add_part(self, x, y, vx, vy, life, color, size):
self.parts.append({'x':x,'y':y,'vx':vx,'vy':vy,
'life':life,'color':color,'size':size})

# ---- update ----
def update(self):
self.t += 1
for c in self.clouds:
c['x'] -= c['speed']
if c['x'] + c['w'] < -20:
c['x'] = W + random.randint(10, 60)
c['y'] = random.randint(30, 200)

if self.state != "dead":
self.gx = (self.gx - 2) % 48
self.mtn_x = (self.mtn_x + 0.25)
self.hill_x = (self.hill_x + 0.55)

if self.shake > 0:
self.shake -= 1

# particles always tick
for p in self.parts:
p['x'] += p['vx']; p['y'] += p['vy']; p['vy'] += 0.15; p['life'] -= 1
self.parts = [p for p in self.parts if p['life'] > 0]
for p in self.sparts:
p['x'] += p['vx']; p['y'] += p['vy']; p['vy'] += 0.1; p['life'] -= 1
self.sparts = [p for p in self.sparts if p['life'] > 0]

if self.state == "ready":
self.by = H/2 + 10 * math.sin(self.t * 0.06)
return

if self.state == "dead":
self.bv += GRAVITY
self.by += self.bv
self.bang = min(90, self.bang + 4)
gy = H - GROUND_H - 14
if self.by > gy:
self.by = gy; self.bv = 0
self.dtimer += 1
return

# ---- playing ----
self.bv += GRAVITY
self.by += self.bv
tgt = max(-35, min(80, -self.bv * 3.5))
self.bang += (tgt - self.bang) * 0.18
if self.flap_t > 0: self.flap_t -= 1

self.trail.append((self.bx, self.by))
if len(self.trail) > 10: self.trail.pop(0)

# pipes
self.ptimer += 1
if self.ptimer >= 82:
self.ptimer = 0
gy = random.randint(85, H - GROUND_H - 85 - PIPE_GAP)
self.pipes.append({'x': W + 10, 'gy': gy, 'ok': False})
for p in self.pipes: p['x'] -= PIPE_SPEED
self.pipes = [p for p in self.pipes if p['x'] > -PIPE_W - 10]

# score
for p in self.pipes:
if not p['ok'] and p['x'] + PIPE_W < self.bx:
p['ok'] = True; self.score += 1
if self.score > self.high_score: self.high_score = self.score
for _ in range(10):
self.sparts.append({
'x': self.bx + random.randint(-10, 30),
'y': 40 + random.randint(-10, 10),
'vx': random.uniform(-3, 3), 'vy': random.uniform(-5, -1),
'life': 25, 'size': random.randint(2, 4),
'color': random.choice([C_GOLD, C_WHITE, (255,255,150)])
})

# collision
br = pygame.Rect(self.bx - 11, self.by - 9, 22, 18)
if self.by + 9 >= H - GROUND_H or self.by - 9 <= 0:
self._die(); return
for p in self.pipes:
px, gy = p['x'], p['gy']
top = pygame.Rect(px, 0, PIPE_W, gy)
bot = pygame.Rect(px, gy + PIPE_GAP, PIPE_W, H)
top_c = pygame.Rect(px - 4, gy - 20, PIPE_W + 8, 22)
bot_c = pygame.Rect(px - 4, gy + PIPE_GAP - 2, PIPE_W + 8, 22)
if br.colliderect(top) or br.colliderect(bot) or \
br.colliderect(top_c) or br.colliderect(bot_c):
self._die(); return

def _die(self):
self.state = "dead"; self.flash = 8; self.shake = 10; self.shake_s = 5; self.bv = -5
for _ in range(28):
a = random.uniform(0, 6.28)
s = random.uniform(2, 8)
self._add_part(self.bx, self.by,
math.cos(a)*s, math.sin(a)*s - 2,
random.randint(20, 42),
random.choice([C_BIRD_Y, C_BIRD_W, C_BIRD_BEAK, C_WHITE, C_RED]),
random.randint(2, 5))

def flap(self):
if self.state == "ready":
self.state = "play"; self.bv = FLAP_FORCE; self.flap_t = 8
self.ptimer = 55; self.trail = []; return
if self.state == "play":
self.bv = FLAP_FORCE; self.flap_t = 8
for _ in range(3):
self._add_part(self.bx - 6, self.by + 8,
random.uniform(-2, -0.5), random.uniform(1, 3),
random.randint(8, 15), C_WHITE, random.randint(1, 3))

def restart(self):
if self.state == "dead": self.reset()

# ---- drawing ----
def draw(self):
ox = oy = 0
if self.shake > 0:
ox = random.randint(-self.shake_s, self.shake_s)
oy = random.randint(-self.shake_s, self.shake_s)

# sky gradient
for r in range(H - GROUND_H):
c = lerp_color(C_SKY_TOP, C_SKY_BOT, r / (H - GROUND_H))
pygame.draw.line(self.screen, c, (0, r), (W, r))

# sun
self._draw_sun()

# clouds
for c in self.clouds:
self._draw_cloud(c)

# mountains (parallax)
self._draw_parallax(self.mtn_pts, C_MTN1, self.mtn_x % (W + 120), 0.75)
self._draw_parallax(self.hill_pts, C_HILL, self.hill_x % (W + 100), 0.85)
# second layer offset
self._draw_parallax(self.mtn_pts, C_MTN2, (self.mtn_x + 80) % (W + 120), 0.65)

# pipes
for p in self.pipes:
self._draw_pipe(p['x'] + ox, p['gy'] + oy)

# trail
for i, (tx, ty) in enumerate(self.trail):
a = int(35 * i / len(self.trail)) if self.trail else 0
r = int(2 + 3 * i / len(self.trail)) if self.trail else 2
if a > 5:
s = pygame.Surface((r*2, r*2), pygame.SRCALPHA)
pygame.draw.circle(s, (245, 215, 60, a), (r, r), r)
self.screen.blit(s, (int(tx) - r + ox, int(ty) - r + oy))

# ground
self._draw_ground(ox)

# particles
for p in self.sparts + self.parts:
pygame.draw.circle(self.screen, p['color'],
(int(p['x'] + ox), int(p['y'] + oy)), max(1, p['size']))

# bird
self._draw_bird(ox, oy)

# flash
if self.flash > 0:
s = pygame.Surface((W, H), pygame.SRCALPHA)
s.fill((255, 255, 255, 30 * self.flash))
self.screen.blit(s, (0, 0)); self.flash -= 1

# score
self._text_center(str(self.score), self.font_lg, C_WHITE, 48, shadow=True)

# ready
if self.state == "ready":
self._text_center("Flappy Bird", self.font_lg, C_WHITE, H//2 - 55, shadow=True)
self._text_center("Click / Space", self.font_sm, C_WHITE, H//2 + 15)
if self.high_score > 0:
self._text_center(f"Best: {self.high_score}", self.font_sm, C_GOLD, H//2 + 42)

# game over
if self.state == "dead" and self.dtimer > 22:
ov = pygame.Surface((W, H), pygame.SRCALPHA)
ov.fill((0, 0, 0, min(90, (self.dtimer - 22) * 3)))
self.screen.blit(ov, (0, 0))
self._text_center("GAME OVER", self.font_lg, C_RED, H//2 - 50, shadow=True)
self._text_center(f"Score: {self.score}", self.font_md, C_WHITE, H//2 - 5)
if self.score == self.high_score and self.score > 0:
self._text_center("NEW BEST!", self.font_sm, C_GOLD, H//2 + 30)
else:
self._text_center(f"Best: {self.high_score}", self.font_sm, C_SILVER, H//2 + 30)
self._text_center("Click / Space", self.font_sm, C_WHITE, H//2 + 60)

pygame.display.flip()

def _text_center(self, txt, font, color, y, shadow=False):
s = font.render(txt, True, color)
r = s.get_rect(center=(W//2, y))
if shadow:
self.screen.blit(font.render(txt, True, C_BLACK), (r.x+2, r.y+2))
self.screen.blit(s, r)

def _draw_sun(self):
cx, cy = int(W * 0.8), 55
t = pygame.time.get_ticks() / 1000
for i in range(10):
a = t / 2 + i * math.pi / 5
ex = cx + int(math.cos(a) * 32)
ey = cy + int(math.sin(a) * 32)
pygame.draw.line(self.screen, (255, 235, 120), (cx, cy), (ex, ey), 2)
pygame.draw.circle(self.screen, (255, 245, 120), (cx, cy), 18)
pygame.draw.circle(self.screen, (255, 250, 190), (cx - 3, cy - 3), 14)

def _draw_cloud(self, c):
s = pygame.Surface((c['w'], 36), pygame.SRCALPHA)
w = c['w']
col = (255, 255, 255, 80)
pygame.draw.ellipse(s, col, (0, 10, w//2, 22))
pygame.draw.ellipse(s, col, (w//4, 0, w//2, 30))
pygame.draw.ellipse(s, col, (w//2, 8, w//2, 24))
self.screen.blit(s, (int(c['x']), c['y']))

def _draw_parallax(self, pts, color, offset, brightness):
"""Draw a mountain/hill strip that repeats."""
c = tuple(int(v * brightness) for v in color)
strip_w = max(x for x, _ in pts) + 80
poly = []
for px, py in pts:
x = (px - offset) % strip_w - 40
poly.append((x, py))
poly.sort(key=lambda p: p[0])
full = [(0, H - GROUND_H)] + poly + [(W, H - GROUND_H)]
if len(full) >= 3:
pygame.draw.polygon(self.screen, c, [(int(x), int(y)) for x, y in full])

def _draw_pipe(self, px, gy):
px, gy = int(px), int(gy)
bot_y = gy + PIPE_GAP
# top pipe
pygame.draw.rect(self.screen, C_PIPE, (px, 0, PIPE_W, gy))
pygame.draw.rect(self.screen, C_PIPE_HI, (px + 5, 0, 7, gy))
pygame.draw.rect(self.screen, C_PIPE_SH, (px + PIPE_W - 9, 0, 9, gy))
tc = pygame.Rect(px - 4, gy - 20, PIPE_W + 8, 22)
pygame.draw.rect(self.screen, C_PIPE_CA, tc)
pygame.draw.rect(self.screen, C_PIPE_HI, (tc.x+4, tc.y+2, 7, tc.h-4))
pygame.draw.rect(self.screen, C_PIPE_SH, (tc.right-9, tc.y+2, 7, tc.h-4))
pygame.draw.rect(self.screen, C_PIPE_SH, tc, 2)
# bottom pipe
pygame.draw.rect(self.screen, C_PIPE, (px, bot_y, PIPE_W, H - bot_y))
pygame.draw.rect(self.screen, C_PIPE_HI, (px + 5, bot_y, 7, H - bot_y))
pygame.draw.rect(self.screen, C_PIPE_SH, (px + PIPE_W - 9, bot_y, 9, H - bot_y))
bc = pygame.Rect(px - 4, bot_y - 2, PIPE_W + 8, 22)
pygame.draw.rect(self.screen, C_PIPE_CA, bc)
pygame.draw.rect(self.screen, C_PIPE_HI, (bc.x+4, bc.y+2, 7, bc.h-4))
pygame.draw.rect(self.screen, C_PIPE_SH, (bc.right-9, bc.y+2, 7, bc.h-4))
pygame.draw.rect(self.screen, C_PIPE_SH, bc, 2)

def _draw_ground(self, ox):
gy = H - GROUND_H
pygame.draw.rect(self.screen, C_GROUND_G, (0, gy, W, 15))
pygame.draw.rect(self.screen, C_GROUND_D, (0, gy + 15, W, 10))
pygame.draw.rect(self.screen, C_DIRT, (0, gy + 25, W, 30))
pygame.draw.rect(self.screen, C_DIRT_D, (0, gy + 55, W, GROUND_H - 55))
for x in range(-48, W + 48, 10):
gx = x + int(self.gx) + ox
pygame.draw.line(self.screen, (95, 185, 60), (gx, gy), (gx + 2, gy - 5), 2)

def _draw_bird(self, ox, oy):
x, y = int(self.bx + ox), int(self.by + oy)
surf = pygame.Surface((32, 24), pygame.SRCALPHA)
pygame.draw.ellipse(surf, C_BIRD_Y, (2, 3, 26, 18))
pygame.draw.ellipse(surf, C_BIRD_Y2, (6, 11, 18, 9))
wy = 9 if self.flap_t > 4 else (13 if self.flap_t > 0 else 12)
pygame.draw.ellipse(surf, C_BIRD_W, (1, wy, 17, 9))
pygame.draw.circle(surf, C_WHITE, (22, 7), 4)
pygame.draw.circle(surf, C_BLACK, (24, 7), 2)
pygame.draw.polygon(surf, C_BIRD_BEAK, [(26, 9), (32, 12), (26, 15)])
pygame.draw.ellipse(surf, C_BLACK, (2, 3, 26, 18), 1)
rot = pygame.transform.rotate(surf, self.bang)
rect = rot.get_rect(center=(x, y))
self.screen.blit(rot, rect)

# ---- main ----
def run(self):
while True:
self.clock.tick(FPS)
for ev in pygame.event.get():
if ev.type == pygame.QUIT:
pygame.quit(); sys.exit()
if ev.type == pygame.KEYDOWN:
if ev.key == pygame.K_r: self.reset(); continue
if ev.key in (pygame.K_SPACE, pygame.K_UP, pygame.K_w):
if self.state == "dead" and self.dtimer > 22: self.restart()
else: self.flap()
if ev.type == pygame.MOUSEBUTTONDOWN and ev.button == 1:
if self.state == "dead" and self.dtimer > 22: self.restart()
else: self.flap()
self.update(); self.draw()

if __name__ == "__main__":
FlappyBird().run()
```

把上面代码保存为 `flappy_bird.py`,直接运行即可。**无需任何图片素材**——小鸟、管道、背景全部用 Pygame 的绘图 API 画出来的。

## 八、打包成 exe(发给朋友玩)

用 PyInstaller 一键打包:

```bash
pip install pyinstaller
pyinstaller -F -w -n flappy-bird flappy_bird.py
```

- `-F`:打包成单个 exe
- `-w`:不显示黑色控制台窗口

打包完成后 exe 在 `dist/` 目录,双击即玩,朋友电脑上不需要装 Python。

## 九、还可以怎么玩?

这个框架已经是"麻雀虽小五脏俱全",顺着往下加功能门槛很低:

| 扩展方向 | 实现思路 |
|---|---|
| 🔊 音效 | 扇翅/得分/撞管各加一个 `pygame.mixer.Sound` |
| 📈 难度递增 | 管道速度 `PIPE_SPEED` 随分数动态提升,间距 `PIPE_GAP` 逐步收紧 |
| 🏆 本地排行榜 | 把最高分写入 JSON 文件,做 Top5 榜单 |
| 🌈 换肤 | 天空渐变颜色、管道配色做成可配置参数,一键换主题 |
| 🎬 开场动画 | 加一个标题菜单 + 过渡动画 |

## 十、小结

这篇文章里最值得收藏的其实不是 Flappy Bird 本身,而是四个能复用到任何游戏里的技巧:

1. **状态机**(ready / play / dead)——让游戏逻辑不纠缠
2. **视差滚动**——廉价但效果最好的"高级感"来源
3. **粒子系统**——一份数据驱动所有特效
4. **打击感三件套**(震动 + 白闪 + 缓动)——决定手感好坏的分水岭

用同样的思路,你还能做出马里奥、飞机大战、打砖块……欢迎评论区交流,或告诉我你想看哪个游戏的教程,点赞多就安排 😄

> 觉得有用记得 **点赞 + 收藏**,你的支持是我更新的动力!

返回列表