贪吃蛇(Snake)是经典的街机游戏,也是学习游戏开发、面向对象编程和事件循环的绝佳入门项目。本文将带你从零开始,使用 Python 的标准库 pygame 构建一个功能完整的贪吃蛇游戏。

核心架构设计

在编写代码之前,我们需要明确游戏的几个核心组件:

游戏画布 (Canvas):使用 pygame 创建窗口,负责绘制所有图形。

蛇 (Snake):

  • 由一个列表存储坐标点组成,列表第一个元素是蛇头。
  • 需要处理移动、生长、碰撞检测。

食物 (Food):

  • 在画布上随机生成。
  • 蛇吃到食物后,蛇身长度+1,分数+1,食物刷新。

游戏循环 (Game Loop):

  • 处理事件(键盘输入)。
  • 更新游戏状态(蛇移动、碰撞检测)。
  • 绘制画面(清空画布、画蛇、画食物、画分数)。

游戏状态机:区分“进行中”、“暂停”、“游戏结束”等状态。

准备工作

首先,你需要安装 pygame 库:

pip install pygame

完整代码实现

以下是一个结构清晰、包含注释的完整贪吃蛇游戏代码。你可以直接保存为 snake.py 运行。

import pygame
import random
import sys

# -------------------------- 常量定义 --------------------------
FPS = 60  # 帧率
GRID_SIZE = 20  # 网格大小
WIDTH = 800  # 窗口宽度
HEIGHT = 600  # 窗口高度
ROWS = HEIGHT // GRID_SIZE
COLS = WIDTH // GRID_SIZE

# 颜色定义 (R, G, B)
BLACK = (0, 0, 0)
WHITE = (255, 255, 255)
GREEN = (0, 128, 0)
RED = (255, 0, 0)
BRIGHT_GREEN = (0, 255, 0)
YELLOW = (255, 255, 0)
GRAY = (80, 80, 80)

# -------------------------- 游戏类 --------------------------
class SnakeGame:
    def __init__(self):
        pygame.init()
        self.screen = pygame.display.set_mode((WIDTH, HEIGHT))
        pygame.display.set_caption("贪吃蛇 Python 版")
        self.clock = pygame.time.Clock()
        self.font = pygame.font.SysFont('Arial', 36)
        
        # 游戏状态变量
        self.reset_game()
        self.running = True

    def reset_game(self):
        """重置游戏状态"""
        # 蛇初始位置:画布中心,长度为3
        self.snake = [
            [COLS // 2, ROWS // 2],
            [COLS // 2 - 1, ROWS // 2],
            [COLS // 2 - 2, ROWS // 2]
        ]
        self.direction = 'RIGHT'
        self.change_direction_to = 'RIGHT'
        self.score = 0
        self.game_over = False
        self.generate_food()

    def generate_food(self):
        """在随机位置生成食物,确保不与蛇身重叠"""
        while True:
            food_pos = [random.randint(0, COLS - 1), random.randint(0, ROWS - 1)]
            if food_pos not in self.snake:
                self.food = food_pos
                break

    def handle_events(self):
        """处理键盘输入和其他事件"""
        for event in pygame.event.get():
            if event.type == pygame.QUIT:
                self.running = False
                sys.exit()
            
            # 键盘控制
            if event.type == pygame.KEYDOWN:
                if event.key == pygame.K_UP:
                    if self.direction != 'DOWN':  # 防止直接反向
                        self.change_direction_to = 'UP'
                elif event.key == pygame.K_DOWN:
                    if self.direction != 'UP':
                        self.change_direction_to = 'DOWN'
                elif event.key == pygame.K_LEFT:
                    if self.direction != 'RIGHT':
                        self.change_direction_to = 'LEFT'
                elif event.key == pygame.K_RIGHT:
                    if self.direction != 'LEFT':
                        self.change_direction_to = 'RIGHT'
                elif event.key == pygame.K_r:
                    # 按 R 键重启
                    self.reset_game()

    def move_snake(self):
        """蛇的移动逻辑"""
        # 1. 更新方向
        self.direction = self.change_direction_to
        
        # 2. 计算新蛇头位置
        head_x, head_y = self.snake[0]
        if self.direction == 'UP':
            new_head = [head_x, head_y - 1]
        elif self.direction == 'DOWN':
            new_head = [head_x, head_y + 1]
        elif self.direction == 'LEFT':
            new_head = [head_x - 1, head_y]
        else:  # RIGHT
            new_head = [head_x + 1, head_y]

        # 3. 检查是否撞墙
        if new_head[0] < 0 or new_head[0] >= COLS or new_head[1] < 0 or new_head[1] >= ROWS:
            self.game_over = True
            return

        # 4. 检查是否撞到自己
        if new_head in self.snake:
            self.game_over = True
            return

        # 5. 移动蛇:将新头加入列表开头
        self.snake.insert(0, new_head)

        # 6. 检查是否吃到食物
        if new_head == self.food:
            self.score += 1
            self.generate_food()
        else:
            # 没吃到食物,移除尾巴,保持长度不变
            self.snake.pop()

    def draw_grid(self):
        """绘制背景网格(可选,增加视觉效果)"""
        for col in range(COLS):
            for row in range(ROWS):
                if (col + row) % 2 == 0:
                    rect = pygame.Rect(col * GRID_SIZE, row * GRID_SIZE, GRID_SIZE, GRID_SIZE)
                    pygame.draw.rect(self.screen, GRAY, rect, 0, 1) # 只画边框
                    # 或者用非常暗的颜色填充:
                    # pygame.draw.rect(self.screen, (20, 20, 20), rect)

    def draw_elements(self):
        """绘制蛇、食物和分数"""
        # 绘制蛇
        for segment in self.snake:
            x, y = segment[0], segment[1]
            rect = pygame.Rect(x * GRID_SIZE, y * GRID_SIZE, GRID_SIZE - 1, GRID_SIZE - 1)
            if segment == self.snake[0]:
                # 蛇头用亮绿色
                pygame.draw.rect(self.screen, BRIGHT_GREEN, rect, border_radius=5)
            else:
                # 蛇身用深绿色
                pygame.draw.rect(self.screen, GREEN, rect, border_radius=5)

        # 绘制食物
        fx, fy = self.food
        food_rect = pygame.Rect(fx * GRID_SIZE, fy * GRID_SIZE, GRID_SIZE - 1, GRID_SIZE - 1)
        pygame.draw.circle(self.screen, RED, food_rect.center, GRID_SIZE // 2 - 1)

        # 绘制分数
        score_text = self.font.render(f"Score: {self.score}", True, YELLOW)
        self.screen.blit(score_text, (10, 10))

    def draw_game_over(self):
        """绘制游戏结束界面"""
        overlay = pygame.Surface((WIDTH, HEIGHT))
        overlay.set_alpha(180)  # 半透明背景
        self.screen.blit(overlay, (0, 0))
        
        go_text = self.font.render("Game Over!", True, RED)
        score_text = self.font.render(f"Final Score: {self.score}", True, WHITE)
        restart_text = self.font.render("Press R to Restart", True, WHITE)
        
        # 居中显示
        self.screen.blit(go_text, (WIDTH//2 - go_text.get_width()//2, HEIGHT//2 - 60))
        self.screen.blit(score_text, (WIDTH//2 - score_text.get_width()//2, HEIGHT//2))
        self.screen.blit(restart_text, (WIDTH//2 - restart_text.get_width()//2, HEIGHT//2 + 60))

    def update(self):
        """更新游戏状态"""
        if not self.game_over:
            self.move_snake()

    def render(self):
        """渲染画面"""
        self.screen.fill(BLACK)
        self.draw_grid()
        self.draw_elements()
        
        if self.game_over:
            self.draw_game_over()
            
        pygame.display.flip()

    def run(self):
        """主游戏循环"""
        # 控制游戏速度,随着分数增加可以适度提高
        game_speed = 10  # 每秒移动10格
        last_move_time = pygame.time.get_ticks()
        
        while self.running:
            self.handle_events()
            
            # 控制蛇的移动频率,而不是每帧都移动
            current_time = pygame.time.get_ticks()
            if current_time - last_move_time >= 1000 // game_speed:
                self.update()
                last_move_time = current_time
                
            self.render()
            self.clock.tick(FPS)

        pygame.quit()

if __name__ == "__main__":
    game = SnakeGame()
    game.run()

代码关键部分解析

坐标系与网格系统

游戏使用网格系统(Grid System)而非像素坐标。

  • COLS = WIDTH // GRID_SIZE 和 ROWS = HEIGHT // GRID_SIZE 计算出了屏幕有多少个格子。
  • 蛇的位置存储为 [col, row] 索引。
  • 绘制时,通过 x * GRID_SIZE 将逻辑坐标转换为像素坐标。 优点:逻辑简单,碰撞检测只需判断索引是否相等,效率高。

蛇的移动逻辑

在 move_snake 方法中:

  • 计算新蛇头位置。
  • 插入新头:self.snake.insert(0, new_head)。
  • 判断是否吃食物:
    • 如果 new_head == self.food,分数增加,食物刷新,不移除尾巴(蛇变长)。
    • 否则,self.snake.pop() 移除尾巴(蛇长度不变,整体前移)。

方向控制与防反转

change_direction_to 与 direction 分离是为了处理缓冲。

  • 在高速移动时,如果用户快速按两个键(例如从向右转到向下再转到向左),如果直接改变方向,可能导致蛇头瞬间撞进自己身体。
  • 通过 if self.direction != 'DOWN' 等检查,确保蛇不能直接180度掉头。

游戏速度控制

注意主循环 run 中的逻辑:

if current_time - last_move_time >= 1000 // game_speed:
    self.update()
    last_move_time = current_time

pygame 的 clock.tick(FPS) 保证画面以 60 FPS 刷新,但蛇的逻辑更新速度由 game_speed 控制(这里设为每秒10步)。这样即使电脑很快,蛇也不会飞,保证了公平性和可玩性。

如何扩展你的游戏?

  • 障碍物:在地图中加入固定障碍格,蛇碰到也游戏结束。
  • 墙壁穿透:当蛇头到达边缘时,从另一侧出现(经典贪吃蛇模式)。
  • 特殊食物:金色食物加速或减速,限时消失。
  • 音效:使用 pygame.mixer 加载吃食物、游戏结束的音效。
  • 最高分记录:使用 json 或 pickle 将最高分保存到本地文件。

常见问题

  • 蛇移动太快/太慢? 修改 game_speed 变量。数值越大,移动越快。
  • 窗口闪退? 检查 pygame.display.flip() 是否在 while 循环内。确保 pygame.init() 被调用。
  • 方向控制失效? 检查是否忘记判断反向输入。

总结

通过这个项目,你不仅学会了一个游戏,还掌握了:

  • pygame 的基本使用(窗口、事件、绘图、时钟)。
  • 面向对象程序设计(封装游戏逻辑)。
  • 游戏循环(Update-Render Loop)。
  • 状态管理与碰撞检测。

你可以在此基础上继续修改代码,加入自己的创意,打造一个独一无二的贪吃蛇版本!