Pygame 小游戏——贪吃蛇

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()

附:文章说明

本文仅为个人理解,若有不当之处,欢迎指正~

相关推荐
大数据魔法师5 小时前
Streamlit(二十三)- 教程(二)- 动态导航
python·web
心中有国也有家8 小时前
GE图引擎深度解析——CANN的计算图优化与执行引擎
人工智能·pytorch·python·学习·numpy
卷毛的技术笔记9 小时前
告别硬编码!Spring AI Alibaba 实现 AI Agent 智能工具调用(Tool Calling)
java·人工智能·后端·python·spring·ai编程
编程大师哥9 小时前
匿名函数 lambda + 高阶函数
java·python·算法
vb2008119 小时前
FastAPI APIRouter
开发语言·python
adrninistrat0r10 小时前
Java调用链MCP分析工具
java·python·ai编程
杨充10 小时前
1.3 浮点型数据设计灵魂
开发语言·python·算法
meilindehuzi_a11 小时前
深入浅出数据结构:Python 字典(Dict)与集合(Set)的哈希表底层全链路追踪
数据结构·python·散列表
Lucas凉皮11 小时前
20243408 2025-2026-2 《Python程序设计》综合实践报告
python·实验报告