Pygame 小游戏------贪吃蛇
项目概述
本文通过 Pygame 实现一个经典贪吃蛇游戏(Snake)。
在这个游戏中,玩家控制一条在网格上不断移动的蛇,吃掉食物来增加身体长度并累积分数。地图上随机出现限时金色奖励食物,得分越高关卡越高,蛇的移动速度随之加快。碰到边界或自身即游戏失败,目标是在不撞墙、不咬到自己的情况下尽可能多地吃到食物。其中:
- 移动控制:方向键 / W、A、S、D 键控制蛇的移动方向;不能直接反向移动(防止立即自咬)。
- 普通食物 :红色圆点,每吃一个得
10 × 当前关卡分,蛇身增长一格,同时有概率触发奖励食物出现。 - 奖励食物 :金色闪烁星形食物,每个得
50 × 当前关卡分,限时存在(约 8 个移动周期),超时自动消失。 - 关卡递进:得分每满 100 分自动升一级(最高 10 级),移动速度随关卡线性提升(初始 8 FPS,每级 +2)。
- 键盘操作:方向键 / WASD:控制方向;R:重新开始。
- 胜利/失败条件:蛇头撞到边界或碰到自身即游戏结束;游戏无上限关卡,挑战最高分。

游戏实现
初始化与基础设置
游戏启动时初始化 Pygame 并定义屏幕尺寸、网格参数和颜色常量。
python
pygame.init()
WIDTH, HEIGHT = 540, 540
COLS, ROWS = 18, 18
CELL = WIDTH // COLS
screen = pygame.display.set_mode((WIDTH, HEIGHT + 60))
pygame.display.set_caption("Snake")
clock = pygame.time.Clock()
游戏区域为 540×540 像素的 18×18 网格,每格 30×30 像素;屏幕额外留出顶部 60 像素作为 HUD 信息栏。
颜色定义
python
DARK_BG = (10, 15, 13) # 游戏区深色背景
GRID_C = (14, 26, 20) # 网格线颜色
GREEN1 = (93, 202, 165) # 蛇头颜色
GREEN2 = (29, 158, 117) # 蛇身基础色
RED = (226, 75, 74) # 普通食物
GOLD = (250, 199, 117) # 奖励食物
WHITE = (220, 220, 220) # HUD 主文字
GRAY = (100, 120, 110) # HUD 次要文字
字体加载
python
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"
font_sm = pygame.font.Font(CHINESE_FONT_PATH, 22)
font_md = pygame.font.Font(CHINESE_FONT_PATH, 32)
font_big = pygame.font.Font(CHINESE_FONT_PATH, 56)
直接加载系统中文字体文件,保证中文 HUD 文字正常显示。跨平台时可替换为 pygame.font.SysFont(None, size)。
核心类设计
游戏主类(Game)
本游戏将所有逻辑集中在 Game 类中,通过状态变量管理蛇、食物、关卡和分数。
构造函数 __init__:
python
def __init__(self):
self.best = 0
self.reset()
best 最高分在游戏对象生命周期内持久保留,重开后仍显示历史最高分。
重置方法 reset:
python
def reset(self):
self.snake = [(9, 9), (8, 9), (7, 9)] # 初始蛇身(列, 行)坐标列表
self.direction = (1, 0) # 当前移动方向(向右)
self.next_dir = (1, 0) # 下一帧生效方向(防止中间帧连续输入导致反向)
self.score = 0
self.level = 1
self.speed = 8 # 初始移动频率(FPS tick)
self.food = self._spawn()
self.bonus = None
self.bonus_ttl = 0
self.game_over = False
self.frame = 0
使用 next_dir 缓存输入而非直接修改 direction,避免在同一帧内连续按两个方向键时穿越自身的经典 Bug。
食物生成 _spawn:
python
def _spawn(self, exclude=None):
cells = [(c, r) for c in range(COLS) for r in range(ROWS)
if (c, r) not in self.snake]
if exclude and exclude in cells:
cells.remove(exclude)
return random.choice(cells)
先从全部网格中排除蛇身占据的格子,再随机取一个作为食物位置。exclude 参数可在生成奖励食物时同时排除普通食物位置,防止两者重叠。
奖励食物触发 _try_spawn_bonus:
python
def _try_spawn_bonus(self):
if self.bonus is None and random.random() < 0.35:
self.bonus = self._spawn(self.food)
self.bonus_ttl = self.speed * 8
每次吃到普通食物后,以 35% 概率尝试生成奖励食物。bonus_ttl(生命帧数)设为当前速度的 8 倍,使奖励食物的可见时间在不同关卡下大致等效于"8 步内"。
事件处理 handle_events
python
def handle_events(self):
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:
self.reset()
dirs = {
pygame.K_UP: (0, -1), pygame.K_w: (0, -1),
pygame.K_DOWN: (0, 1), pygame.K_s: (0, 1),
pygame.K_LEFT: (-1, 0), pygame.K_a: (-1, 0),
pygame.K_RIGHT: (1, 0), pygame.K_d: (1, 0),
}
if event.key in dirs:
nd = dirs[event.key]
if (nd[0] + self.direction[0], nd[1] + self.direction[1]) != (0, 0):
self.next_dir = nd
反向检测逻辑:若新方向向量与当前方向向量之和为 (0, 0),说明是正反方向(如左+右),则忽略该输入,防止蛇直接 180° 掉头咬到自身第二节身体。
核心更新 update
update 方法集中处理蛇的移动、碰撞检测、吃食物和关卡晋升逻辑:
python
def update(self):
if self.game_over:
return
self.direction = self.next_dir
hx = self.snake[0][0] + self.direction[0]
hy = self.snake[0][1] + self.direction[1]
# ① 碰壁或碰自身 → 游戏结束
if not (0 <= hx < COLS and 0 <= hy < ROWS) or (hx, hy) in self.snake:
self.game_over = True
if self.score > self.best:
self.best = self.score
return
head = (hx, hy)
self.snake.insert(0, head)
grew = False
# ② 吃到普通食物
if head == self.food:
self.score += 10 * self.level
self.food = self._spawn()
self._try_spawn_bonus()
grew = True
# ③ 吃到奖励食物
elif self.bonus and head == self.bonus:
self.score += 50 * self.level
self.bonus = None
self.bonus_ttl = 0
grew = True
# 未吃到食物则去除尾部(维持蛇长)
if not grew:
self.snake.pop()
# ④ 奖励食物计时
if self.bonus:
self.bonus_ttl -= 1
if self.bonus_ttl <= 0:
self.bonus = None
# ⑤ 关卡晋升
new_level = self.score // 100 + 1
if new_level > self.level:
self.level = min(new_level, 10)
self.speed = 8 + (self.level - 1) * 2
if self.score > self.best:
self.best = self.score
self.frame += 1
蛇的移动通过"头插尾删"实现:每帧在列表头部插入新头部坐标,若未吃到食物则同时删除尾部坐标,从而在不复制整条蛇的情况下高效模拟移动。
绘制方法 draw
HUD 信息栏:
python
pygame.draw.rect(screen, (18, 28, 22), (0, 0, WIDTH, 60))
pygame.draw.line(screen, GREEN2, (0, 60), (WIDTH, 60), 1)
screen.blit(font_sm.render(f"SCORE {self.score}", True, WHITE), (14, 18))
screen.blit(font_sm.render(f"BEST {self.best}", True, GRAY), (14, 38))
screen.blit(font_sm.render(f"LV {self.level}", True, GREEN1), (WIDTH - 90, 18))
screen.blit(font_sm.render("R=重来", True, GRAY), (WIDTH - 90, 38))
网格背景:
python
for r in range(ROWS):
for c in range(COLS):
pygame.draw.rect(screen, GRID_C,
(c * CELL, 60 + r * CELL, CELL, CELL), 1)
逐格绘制单像素边框,形成淡色网格,不遮挡游戏元素。
蛇的绘制:
python
for i, (c, r) in enumerate(self.snake):
rect = pygame.Rect(c * CELL + 2, 60 + r * CELL + 2, CELL - 4, CELL - 4)
color = GREEN1 if i == 0 else (
20, int(80 + (1 - i / len(self.snake)) * 100), 60)
pygame.draw.rect(screen, color, rect, border_radius=4)
蛇头用亮绿色 GREEN1 突出显示;蛇身颜色随节序线性变暗(绿色通道从 180 渐降至 80),形成由头到尾的渐变视觉效果。每节缩进 2px 并加圆角,避免视觉上连成一片。
蛇眼绘制:
python
if i == 0:
dx, dy = self.direction
ex = c * CELL + CELL // 2 + dy * 5
ey = 60 + r * CELL + CELL // 2 + dx * 5 - abs(dy) * 4
pygame.draw.circle(screen, DARK_BG, (ex - dy * 4, ey + dx * 4), 2)
pygame.draw.circle(screen, DARK_BG, (ex + dy * 4, ey - dx * 4), 2)
眼睛位置通过当前方向向量 (dx, dy) 动态计算,始终出现在蛇头朝向一侧的两边,随移动方向自动旋转,赋予蛇头表情感。
奖励食物闪烁效果:
python
if self.bonus:
alpha = 180 + int(60 * abs(pygame.time.get_ticks() % 600 / 300 - 1))
surf = pygame.Surface((CELL, CELL), pygame.SRCALPHA)
pygame.draw.circle(surf, (*GOLD, alpha), (CELL // 2, CELL // 2), CELL // 2 - 3)
screen.blit(surf, (bc * CELL, 60 + br * CELL))
star = font_sm.render("★", True, (255, 230, 80))
screen.blit(star, (bx - star.get_width() // 2, by - star.get_height() // 2))
利用 pygame.time.get_ticks() 取模生成周期为 600ms 的三角波,将透明度在 180~240 之间往复变化,实现自然的心跳闪烁效果,吸引玩家注意。
绘制顺序为:HUD → 网格 → 蛇 → 食物 → 奖励食物 → 结束遮罩,严格保证层次正确。
主循环 run:
python
def run(self):
while True:
self.handle_events()
self.update()
self.draw()
clock.tick(self.speed if not self.game_over else 30)
游戏进行中以 self.speed(8~26 FPS)驱动主循环,控制蛇的移动节奏;游戏结束后切换为固定 30 FPS,保持结束画面流畅渲染而不占用过多 CPU。
全部代码
python
import pygame
import random
import sys
pygame.init()
WIDTH, HEIGHT = 540, 540
COLS, ROWS = 18, 18
CELL = WIDTH // COLS
BLACK = (0, 0, 0)
DARK_BG = (10, 15, 13)
GRID_C = (14, 26, 20)
GREEN1 = (93, 202, 165) # 蛇头
GREEN2 = (29, 158, 117) # 蛇身
RED = (226, 75, 74) # 食物
GOLD = (250, 199, 117) # 奖励食物
WHITE = (220, 220, 220)
GRAY = (100, 120, 110)
CHINESE_FONT_PATH = r"C:/Windows/Fonts/simsun.ttc"
# 使用具体字体文件(如果系统有):
font_sm = pygame.font.Font(CHINESE_FONT_PATH, 22)
font_md = pygame.font.Font(CHINESE_FONT_PATH, 32)
font_big = pygame.font.Font(CHINESE_FONT_PATH, 56)
screen = pygame.display.set_mode((WIDTH, HEIGHT + 60))
pygame.display.set_caption("Snake")
clock = pygame.time.Clock()
class Game:
def __init__(self):
self.best = 0
self.reset()
def reset(self):
self.snake = [(9, 9), (8, 9), (7, 9)]
self.direction = (1, 0)
self.next_dir = (1, 0)
self.score = 0
self.level = 1
self.speed = 8 # FPS tick rate
self.food = self._spawn()
self.bonus = None
self.bonus_ttl = 0
self.game_over = False
self.frame = 0
def _spawn(self, exclude=None):
cells = [(c, r) for c in range(COLS) for r in range(ROWS)
if (c, r) not in self.snake]
if exclude and exclude in cells:
cells.remove(exclude)
return random.choice(cells)
def _try_spawn_bonus(self):
if self.bonus is None and random.random() < 0.35:
self.bonus = self._spawn(self.food)
self.bonus_ttl = self.speed * 8 # 可见帧数
def handle_events(self):
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:
self.reset()
dirs = {
pygame.K_UP: (0, -1), pygame.K_w: (0, -1),
pygame.K_DOWN: (0, 1), pygame.K_s: (0, 1),
pygame.K_LEFT: (-1, 0), pygame.K_a: (-1, 0),
pygame.K_RIGHT: (1, 0), pygame.K_d: (1, 0),
}
if event.key in dirs:
nd = dirs[event.key]
if (nd[0] + self.direction[0], nd[1] + self.direction[1]) != (0, 0):
self.next_dir = nd
def update(self):
if self.game_over:
return
self.direction = self.next_dir
hx = self.snake[0][0] + self.direction[0]
hy = self.snake[0][1] + self.direction[1]
if not (0 <= hx < COLS and 0 <= hy < ROWS) or (hx, hy) in self.snake:
self.game_over = True
if self.score > self.best:
self.best = self.score
return
head = (hx, hy)
self.snake.insert(0, head)
grew = False
if head == self.food:
self.score += 10 * self.level
self.food = self._spawn()
self._try_spawn_bonus()
grew = True
elif self.bonus and head == self.bonus:
self.score += 50 * self.level
self.bonus = None
self.bonus_ttl = 0
grew = True
if not grew:
self.snake.pop()
if self.bonus:
self.bonus_ttl -= 1
if self.bonus_ttl <= 0:
self.bonus = None
new_level = self.score // 100 + 1
if new_level > self.level:
self.level = min(new_level, 10)
self.speed = 8 + (self.level - 1) * 2
if self.score > self.best:
self.best = self.score
self.frame += 1
def draw(self):
# --- HUD strip ---
screen.fill((8, 12, 10))
pygame.draw.rect(screen, (18, 28, 22), (0, 0, WIDTH, 60))
pygame.draw.line(screen, GREEN2, (0, 60), (WIDTH, 60), 1)
screen.blit(font_sm.render(f"SCORE {self.score}", True, WHITE), (14, 18))
screen.blit(font_sm.render(f"BEST {self.best}", True, GRAY), (14, 38))
screen.blit(font_sm.render(f"LV {self.level}", True, GREEN1), (WIDTH - 90, 18))
screen.blit(font_sm.render("R=重来", True, GRAY), (WIDTH - 90, 38))
# --- Grid ---
for r in range(ROWS):
for c in range(COLS):
pygame.draw.rect(screen, GRID_C,
(c * CELL, 60 + r * CELL, CELL, CELL), 1)
# --- Snake ---
for i, (c, r) in enumerate(self.snake):
rect = pygame.Rect(c * CELL + 2, 60 + r * CELL + 2, CELL - 4, CELL - 4)
color = GREEN1 if i == 0 else (
20, int(80 + (1 - i / len(self.snake)) * 100), 60)
pygame.draw.rect(screen, color, rect, border_radius=4)
if i == 0:
# 眼睛
dx, dy = self.direction
ex = c * CELL + CELL // 2 + dy * 5
ey = 60 + r * CELL + CELL // 2 + dx * 5 - abs(dy) * 4
pygame.draw.circle(screen, DARK_BG, (ex - dy * 4, ey + dx * 4), 2)
pygame.draw.circle(screen, DARK_BG, (ex + dy * 4, ey - dx * 4), 2)
# --- Food ---
fc, fr = self.food
cx, cy = fc * CELL + CELL // 2, 60 + fr * CELL + CELL // 2
pygame.draw.circle(screen, RED, (cx, cy), CELL // 2 - 3)
pygame.draw.circle(screen, (255, 150, 148), (cx - 2, cy - 2), 3)
# --- Bonus ---
if self.bonus:
bc, br = self.bonus
bx, by = bc * CELL + CELL // 2, 60 + br * CELL + CELL // 2
alpha = 180 + int(60 * abs(pygame.time.get_ticks() % 600 / 300 - 1))
surf = pygame.Surface((CELL, CELL), pygame.SRCALPHA)
pygame.draw.circle(surf, (*GOLD, alpha), (CELL // 2, CELL // 2), CELL // 2 - 3)
screen.blit(surf, (bc * CELL, 60 + br * CELL))
star = font_sm.render("★", True, (255, 230, 80))
screen.blit(star, (bx - star.get_width() // 2, by - star.get_height() // 2))
# --- Overlays ---
if self.game_over:
overlay = pygame.Surface((WIDTH, HEIGHT), pygame.SRCALPHA)
overlay.fill((10, 15, 13, 180))
screen.blit(overlay, (0, 60))
t1 = font_big.render("GAME OVER", True, RED)
t2 = font_sm.render(f"得分 {self.score} 最高 {self.best}", True, WHITE)
t3 = font_sm.render("按 R 重新开始", True, GRAY)
screen.blit(t1, (WIDTH // 2 - t1.get_width() // 2, 60 + HEIGHT // 2 - 80))
screen.blit(t2, (WIDTH // 2 - t2.get_width() // 2, 60 + HEIGHT // 2 + 10))
screen.blit(t3, (WIDTH // 2 - t3.get_width() // 2, 60 + HEIGHT // 2 + 50))
pygame.display.flip()
def run(self):
while True:
self.handle_events()
self.update()
self.draw()
clock.tick(self.speed if not self.game_over else 30)
if __name__ == "__main__":
Game().run()
附:文章说明
本文仅为个人理解,若有不当之处,欢迎指正~