1. 项目概述:为什么用Python写贪吃蛇是绝佳的入门项目
如果你刚开始学Python,或者已经学了一阵子语法,但总觉得那些变量、循环、函数离“做出一个东西”还很遥远,那么我强烈建议你动手写一个贪吃蛇游戏。这不是一个简单的练习,而是一个能把你学过的零散知识点串成一条线的绝佳实战项目。我自己带过不少新手,发现很多人卡在“知道语法但不知道怎么写程序”的阶段,而贪吃蛇恰好能完美解决这个问题——它逻辑清晰、涉及面广、成就感强,200行左右的代码就能让你看到一个活生生的、可交互的程序跑起来。
这个项目能带你做什么?首先,你会真正理解事件驱动编程是怎么回事,不是书本上的概念,而是手指按下键盘,屏幕上的蛇就真的会动。其次,你会亲手实现游戏循环这个核心机制,理解一帧一帧的画面是如何更新出来的。更重要的是,你会遇到并解决一系列非常典型的编程问题:比如如何用列表数据结构表示一条会增长的蛇,如何用坐标进行碰撞检测,如何处理用户的连续按键输入而不让蛇“自杀”。完成之后,你收获的不仅仅是一个小游戏,更是一套解决类似问题的思维模式和代码组织能力。无论你是零基础想找项目练手,还是想巩固Python基础,这个教程都会手把手带你走完全程,我会把每个步骤为什么这么做、坑在哪里都讲清楚。
2. 环境准备与工具选型:搭建你的第一个游戏开发环境
工欲善其事,必先利其器。在写第一行代码之前,我们需要把“厨房”收拾好。对于Python游戏开发,尤其是贪吃蛇这种2D小游戏,Pygame库是公认的、社区支持最完善的选择。它封装了底层SDL库的复杂性,提供了绘制图形、处理声音、管理输入事件的简单接口,让我们能专注于游戏逻辑本身。
2.1 Python与Pygame安装详解
首先确保你安装了Python。去Python官网下载最新稳定版(比如3.8以上版本)即可。安装时务必勾选“Add Python to PATH”,这样才可以在命令行里直接使用python命令。安装完成后,打开你的命令行(Windows上是CMD或PowerShell,Mac/Linux上是Terminal),输入python --version,如果能看到版本号,说明安装成功。
接下来安装Pygame。在命令行里输入以下命令:
pip install pygame如果下载速度慢,可以使用国内镜像源,比如清华的源:
pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple安装完成后,可以写一个简单的测试脚本验证一下。创建一个叫test_pygame.py的文件,内容如下:
import pygame pygame.init() screen = pygame.display.set_mode((800, 600)) pygame.display.set_caption("Pygame Test") running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False screen.fill((0, 0, 0)) # 用黑色填充屏幕 pygame.display.flip() # 更新屏幕显示 pygame.quit()运行这个脚本,如果弹出一个黑色的800x600的窗口,并且点击窗口关闭按钮能正常退出,那么恭喜你,Pygame环境配置成功。
注意:有些初学者会在集成开发环境(IDE)里遇到模块找不到的错误。如果你用PyCharm或VSCode,请确保你当前项目选择的Python解释器,就是你刚才用
pip安装了Pygame的那个。在PyCharm中,可以通过File -> Settings -> Project -> Python Interpreter查看和选择;在VSCode中,可以通过点击左下角的Python版本号进行切换。
2.2 代码编辑器与项目管理
对于这个项目,任何文本编辑器都可以,但我推荐使用VSCode或PyCharm。它们有代码高亮、语法提示和调试功能,能极大提升效率。我个人的习惯是:
- 为这个项目单独创建一个文件夹,例如
python_snake_game。 - 在这个文件夹里创建主程序文件
snake_game.py。 - 使用虚拟环境(可选但推荐)。在项目文件夹下运行
python -m venv venv创建一个虚拟环境,然后激活它。这样项目依赖(Pygame)就被隔离在这个文件夹内,不会影响系统其他Python项目。激活命令:Windows上是venv\Scripts\activate,Mac/Linux上是source venv/bin/activate。
工具准备好后,我们的思路是:先搭建游戏窗口和主循环架子,再一步步把蛇画出来、让它动起来、吃食物、变长,最后处理游戏结束逻辑。我会按照这个顺序,把每个部分的代码和原理都拆开讲透。
3. 游戏框架搭建:理解游戏循环与事件处理的核心
所有实时游戏,从贪吃蛇到3A大作,底层都有一个共同的核心结构——游戏主循环。你可以把它想象成心脏,每一次跳动(循环),游戏世界就更新一次。在这个循环里,我们主要做三件事:处理输入(玩家按了哪个键)、更新状态(蛇的位置、分数等)、渲染输出(把新的画面画到屏幕上)。我们先从搭建这个骨架开始。
3.1 初始化与游戏窗口创建
打开你的snake_game.py文件,我们首先导入必要的模块并初始化Pygame。
import pygame import sys # 初始化pygame的所有模块 pygame.init() # 定义游戏区域的尺寸和窗口总尺寸 GAME_WIDTH, GAME_HEIGHT = 800, 600 # 游戏主画面大小 INFO_HEIGHT = 50 # 下方信息栏高度 SCREEN_WIDTH = GAME_WIDTH SCREEN_HEIGHT = GAME_HEIGHT + INFO_HEIGHT # 创建游戏窗口 screen = pygame.display.set_mode((SCREEN_WIDTH, SCREEN_HEIGHT)) pygame.display.set_caption("Python贪吃蛇 - 从零开始")这里有几个关键点:pygame.init()是必须的,它启动了Pygame的字体、声音等子系统。我们定义了两种尺寸:GAME_WIDTH/HEIGHT是蛇活动的区域,INFO_HEIGHT是留出来显示分数、长度等信息的地方,这样设计界面更清晰。set_mode创建了一个“画布”,我们后续所有绘图都在这个screen对象上进行。
3.2 游戏主循环与事件处理引擎
接下来,我们构建游戏的主循环和事件处理机制。
# 定义颜色,使用RGB元组 BLACK = (0, 0, 0) WHITE = (255, 255, 255) RED = (255, 0, 0) GREEN = (0, 255, 0) BLUE = (0, 120, 255) # 游戏状态控制变量 clock = pygame.time.Clock() # 创建一个时钟对象,用于控制帧率 FPS = 60 # 目标帧率,每秒60帧 running = True # 控制整个游戏是否继续运行 game_paused = False # 游戏是否处于暂停状态 # 游戏主循环 while running: # 1. 处理事件(输入) for event in pygame.event.get(): if event.type == pygame.QUIT: running = False # 点击窗口关闭按钮,退出游戏 elif event.type == pygame.KEYDOWN: if event.key == pygame.K_ESCAPE: running = False # 按下ESC键,退出游戏 elif event.key == pygame.K_SPACE: game_paused = not game_paused # 按下空格键,切换暂停状态 print(f"游戏 {'已暂停' if game_paused else '继续'}") # 2. 更新游戏状态(逻辑) if not game_paused: # 这里将来会放置蛇的移动、碰撞检测等逻辑 pass # 暂时占位 # 3. 渲染(绘制) # 3.1 绘制游戏区域背景(白色) screen.fill(WHITE) # 3.2 绘制信息栏背景(深灰色) info_rect = pygame.Rect(0, GAME_HEIGHT, SCREEN_WIDTH, INFO_HEIGHT) pygame.draw.rect(screen, (50, 50, 50), info_rect) # 3.3 这里将来会绘制蛇、食物、分数等 # ... # 4. 更新屏幕显示 pygame.display.flip() # 5. 控制游戏循环的更新速度 clock.tick(FPS) # 退出游戏 pygame.quit() sys.exit()现在运行代码,你会看到一个窗口,上半部分是白色(游戏区),下半部分是深灰色(信息区)。按空格键,控制台会打印暂停/继续的状态;按ESC或点关闭按钮可以退出。这就是我们游戏的“发动机”。
实操心得:
pygame.event.get()会获取从上一帧到现在发生的所有事件(按键、鼠标点击等),我们必须在一个循环内处理完它们,否则系统会认为程序无响应。clock.tick(FPS)是控制游戏速度的关键,它确保循环每秒最多执行60次,避免在不同性能的电脑上游戏速度差异过大。把逻辑更新(if not game_paused)和渲染分开,是良好的代码结构,便于后续维护和调试。
4. 贪吃蛇核心逻辑实现:从数据结构到运动控制
有了框架,现在我们来创造游戏的主角——蛇。这里会涉及两个核心:如何用数据表示蛇,以及如何根据玩家输入让它移动。这是整个游戏逻辑的基石。
4.1 蛇的数据结构与初始化
在游戏中,蛇是由一连串连续的方块(或圆形)组成的。在代码里,最自然的表示方法就是用列表来存储每一节身体的坐标。蛇头是列表的第一个元素,蛇尾是最后一个。我们还需要一个变量来记录蛇当前移动的方向。
# 在游戏主循环之前,初始化蛇的数据 CELL_SIZE = 20 # 每个游戏网格(蛇身一节、食物)的大小,单位是像素 # 蛇的身体,每个元素是一个[x, y]坐标,代表一节身体中心点的位置 # 初始化为一条长度为4的水平蛇,头在右侧 snake_body = [ [5 * CELL_SIZE, 10 * CELL_SIZE], # 蛇头 [4 * CELL_SIZE, 10 * CELL_SIZE], # 第二节 [3 * CELL_SIZE, 10 * CELL_SIZE], # 第三节 [2 * CELL_SIZE, 10 * CELL_SIZE], # 蛇尾 ] # 蛇的当前移动方向,初始向右 # 用字符串表示方向,便于理解 current_direction = 'RIGHT' # 定义下一个方向,用于处理同一帧内的多次按键 next_direction = 'RIGHT' # 蛇的移动速度(每秒移动多少格),初始较慢 snake_speed = 5.0 # 一个计时器,用于控制蛇实际移动的间隔 move_timer = 0.0这里我使用了网格化的坐标系统。CELL_SIZE=20意味着游戏世界被划分成一个个20x20像素的格子,蛇和食物都对齐这些格子移动和生成。这比使用任意像素坐标要简单得多,能避免蛇身错位,也简化了碰撞检测。snake_body列表初始化了4节身体,水平排列。current_direction和next_direction的分离是关键技巧,它能解决一个经典问题:如果玩家快速连续按“上-左”,没有next_direction缓冲,蛇可能会直接掉头“自杀”。我们会在事件处理中更新next_direction,在移动逻辑中再将其赋给current_direction。
4.2 方向控制与平滑移动逻辑
现在,我们需要在事件处理部分捕获方向键,并更新next_direction。同时,在主循环的更新部分,实现蛇的定时移动。 首先,增强事件处理部分(在主循环的for event in pygame.event.get():块内):
elif event.type == pygame.KEYDOWN: # ... 原有的ESC和空格键处理 ... # 方向键控制 if event.key == pygame.K_UP: if current_direction != 'DOWN': # 不能直接反向移动 next_direction = 'UP' elif event.key == pygame.K_DOWN: if current_direction != 'UP': next_direction = 'DOWN' elif event.key == pygame.K_LEFT: if current_direction != 'RIGHT': next_direction = 'LEFT' elif event.key == pygame.K_RIGHT: if current_direction != 'LEFT': next_direction = 'RIGHT'这里有一个重要的限制逻辑:蛇不能直接掉头。例如,当蛇向右移动时,按左键是无效的,这符合贪吃蛇的基本规则,也防止了玩家误操作导致瞬间死亡。
然后,在主循环的“更新游戏状态”部分(if not game_paused:块内),添加移动逻辑:
# 2. 更新游戏状态 if not game_paused: # 计算上一帧到现在经过的时间(秒) delta_time = clock.get_time() / 1000.0 move_timer += delta_time # 当计时器超过移动间隔时,执行一次移动 move_interval = 1.0 / snake_speed # 计算每次移动的间隔时间 if move_timer >= move_interval: move_timer -= move_interval # 减掉一个间隔,保留余数,使移动更平滑 # 应用缓冲的方向 current_direction = next_direction # 获取当前蛇头坐标 head_x, head_y = snake_body[0] # 根据方向计算新的蛇头坐标 if current_direction == 'UP': new_head = [head_x, head_y - CELL_SIZE] elif current_direction == 'DOWN': new_head = [head_x, head_y + CELL_SIZE] elif current_direction == 'LEFT': new_head = [head_x - CELL_SIZE, head_y] elif current_direction == 'RIGHT': new_head = [head_x + CELL_SIZE, head_y] # 移动蛇的身体:新头插入列表开头,去掉旧尾巴 snake_body.insert(0, new_head) snake_body.pop() # 移除列表最后一个元素(旧的蛇尾)这段代码实现了基于时间的平滑移动。clock.get_time()获取上一帧到这一帧经过的毫秒数,除以1000得到秒(delta_time)。move_timer累加时间,当超过move_interval(例如速度5时,间隔0.2秒)时,蛇才移动一格。这样做的好处是,蛇的移动速度是稳定的每秒5格,不受游戏实际帧率(FPS)波动的影响。移动的本质是:在列表头部插入新的蛇头坐标,并移除尾部的旧坐标,这样列表就整体向前移动了一格。
4.3 绘制蛇身与视觉优化
逻辑有了,我们得把它画出来。在渲染部分添加绘制蛇的代码:
# 3.3 绘制蛇 for index, segment in enumerate(snake_body): # 计算每个身体节所在的矩形区域 segment_rect = pygame.Rect( segment[0] - CELL_SIZE // 2, # 矩形左上角x segment[1] - CELL_SIZE // 2, # 矩形左上角y CELL_SIZE, CELL_SIZE ) # 蛇头用红色,身体用绿色 color = RED if index == 0 else GREEN pygame.draw.rect(screen, color, segment_rect) # 给身体节加一个边框,更有立体感 pygame.draw.rect(screen, BLACK, segment_rect, 1)现在运行程序,你应该能看到一条由红头绿身组成的蛇,按方向键可以控制它移动。你可能已经发现,蛇会直接穿出窗口边界。别急,我们马上来处理碰撞和食物。
踩坑记录:初学者常犯的一个错误是在移动蛇时,直接修改
snake_body列表里每个元素的坐标。比如用循环让每一节都移动到前一节的位置。这种方法容易出错且效率低。而“插入新头,弹出旧尾”的方法既简洁又高效,是标准的实现方式。另外,关于方向缓冲(next_direction),如果不做这个处理,在高速连续按键时,可能会出现current_direction在同一帧内被修改多次,导致蛇头移动方向出现诡异错误,这个坑我早期项目里踩过好几次。
5. 食物生成、碰撞检测与游戏规则
蛇能动只是第一步,一个完整的游戏需要有目标(吃食物)和规则(碰撞死亡)。这部分我们将实现食物的随机生成、蛇与食物的碰撞检测(吃食物)、蛇与边界及自身的碰撞检测(死亡判定),以及吃食物后蛇身变长的逻辑。
5.1 食物的随机生成与绘制
食物本质上也是一个坐标,我们需要在游戏区域内随机生成它,并且要确保不会生成在蛇的身体上。
# 在初始化部分,添加食物相关变量 import random # 记得在文件开头导入random模块 food_pos = None # 食物位置,格式 [x, y] food_color = BLUE # 食物颜色 def generate_food(): """在游戏区域内随机生成一个新的食物位置,确保不在蛇身上""" global food_pos while True: # 随机生成一个网格坐标 grid_x = random.randint(0, (GAME_WIDTH // CELL_SIZE) - 1) grid_y = random.randint(0, (GAME_HEIGHT // CELL_SIZE) - 1) # 将网格坐标转换为像素坐标(网格中心点) candidate_pos = [grid_x * CELL_SIZE + CELL_SIZE // 2, grid_y * CELL_SIZE + CELL_SIZE // 2] # 检查这个位置是否与蛇身任何一节重叠 collision = False for segment in snake_body: if candidate_pos[0] == segment[0] and candidate_pos[1] == segment[1]: collision = True break # 如果没有碰撞,就采用这个位置并退出循环 if not collision: food_pos = candidate_pos break # 游戏开始时生成第一个食物 generate_food()generate_food函数使用一个while True循环来确保生成有效食物。它先随机计算一个网格索引,然后乘以CELL_SIZE并加上一半的CELL_SIZE,得到该网格中心的像素坐标。接着遍历蛇身列表,检查是否重合。如果重合,就继续循环生成新的位置,直到找到一个空位为止。这种方法在蛇身很长时效率会降低,但对于我们这个规模的游戏完全够用。
在渲染部分,添加绘制食物的代码:
# 3.4 绘制食物 if food_pos: food_rect = pygame.Rect( food_pos[0] - CELL_SIZE // 2, food_pos[1] - CELL_SIZE // 2, CELL_SIZE, CELL_SIZE ) pygame.draw.rect(screen, food_color, food_rect) # 可以给食物画个内嵌的小矩形,让它看起来更像一个“苹果” inner_rect = pygame.Rect( food_pos[0] - CELL_SIZE // 4, food_pos[1] - CELL_SIZE // 4, CELL_SIZE // 2, CELL_SIZE // 2 ) pygame.draw.rect(screen, WHITE, inner_rect, 1)5.2 碰撞检测:吃食物与死亡判定
碰撞检测是游戏逻辑的“裁判”。我们需要两种检测:蛇头与食物的碰撞(吃),蛇头与边界或自身的碰撞(死)。
首先,在更新状态的部分(移动蛇之后),添加吃食物的检测:
# 在移动蛇的代码之后,检查是否吃到食物 if food_pos and snake_body[0][0] == food_pos[0] and snake_body[0][1] == food_pos[1]: # 吃到食物! # 1. 蛇身变长:不弹出蛇尾,相当于在尾部保留旧的一节 # 注意:我们已经在前面插入了新头,所以这里不需要再插入,只需不pop()即可 # 但为了逻辑清晰,我们换一种方式:在蛇尾添加新的一节 # 获取当前蛇尾的坐标 tail_x, tail_y = snake_body[-1] # 根据蛇尾和倒数第二节的方向,决定新尾巴的位置 # 这是一个简化处理,更精确的做法是记录每节的移动方向 # 这里我们简单地在蛇尾相同位置添加一节,因为下一帧移动时它会被“拉长” snake_body.append([tail_x, tail_y]) # 2. 生成新的食物 generate_food() # 3. (可选)增加分数或速度 # snake_speed += 0.5 # 每吃一个食物,速度增加一点这里吃食物的逻辑是:比较蛇头坐标snake_body[0]和食物坐标food_pos是否完全相等(因为我们使用了网格对齐)。如果相等,就在蛇尾列表append一个与当前蛇尾坐标相同的新节点。为什么是相同坐标?因为当前帧蛇移动时,我们insert了新头并pop了旧尾。如果我们不pop,蛇就变长了。但为了逻辑清晰(移动和增长分离),我们选择先pop(在移动逻辑里),再在检测到吃食物时append一个。效果是一样的:蛇的总长度增加了一节。
然后,添加死亡检测函数。我们在更新状态部分,移动蛇之后立即调用它:
def check_collisions(): """检测各种碰撞,返回游戏是否结束""" head_x, head_y = snake_body[0] # 1. 检测与墙壁(游戏区域边界)的碰撞 if (head_x < CELL_SIZE // 2 or head_x >= GAME_WIDTH - CELL_SIZE // 2 or head_y < CELL_SIZE // 2 or head_y >= GAME_HEIGHT - CELL_SIZE // 2): return True # 撞墙,游戏结束 # 2. 检测与自身身体的碰撞(从第二节开始检查) for segment in snake_body[1:]: if head_x == segment[0] and head_y == segment[1]: return True # 撞到自己,游戏结束 return False # 没有碰撞 # 在更新状态部分,移动蛇之后调用 if check_collisions(): print("游戏结束!") running = False # 或者可以设置一个 game_over 状态,显示结束画面边界检测判断蛇头是否超出了游戏区域。注意,我们的坐标是身体中心点,所以边界条件是中心点离边缘小于半个身位。自身碰撞检测则遍历蛇身(从索引1开始,跳过蛇头),检查是否有任何一节身体坐标与蛇头坐标重合。
5.3 游戏状态完善与分数显示
现在,让我们加入分数系统和更友好的游戏状态管理。
# 在初始化部分添加 score = 0 game_over = False font = pygame.font.SysFont(None, 36) # 创建一个字体对象,用于显示文字 # 修改游戏主循环中的状态更新部分 if not game_paused and not game_over: # ... 原有的移动和碰撞检测逻辑 ... # 吃到食物的逻辑修改一下,增加分数 if food_pos and snake_body[0][0] == food_pos[0] and snake_body[0][1] == food_pos[1]: snake_body.append([tail_x, tail_y]) # 假设tail_x, tail_y已定义 generate_food() score += 10 # 每得100分,速度略微提升 if score % 100 == 0: snake_speed = min(snake_speed + 0.5, 15.0) # 速度上限设为15 # 碰撞检测 if check_collisions(): game_over = True # 在渲染部分,添加分数和状态显示 # 绘制分数 score_text = font.render(f'Score: {score}', True, WHITE) screen.blit(score_text, (10, GAME_HEIGHT + 15)) # 绘制长度 length_text = font.render(f'Length: {len(snake_body)}', True, WHITE) screen.blit(length_text, (SCREEN_WIDTH - 150, GAME_HEIGHT + 15)) # 如果游戏结束,绘制结束文字 if game_over: # 创建一个半透明的表面覆盖整个屏幕 overlay = pygame.Surface((SCREEN_WIDTH, SCREEN_HEIGHT), pygame.SRCALPHA) overlay.fill((0, 0, 0, 180)) # 黑色,70%透明度 screen.blit(overlay, (0, 0)) game_over_font = pygame.font.SysFont(None, 72) game_over_text = game_over_font.render('GAME OVER', True, RED) text_rect = game_over_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2 - 50)) screen.blit(game_over_text, text_rect) restart_text = font.render('Press R to Restart, ESC to Quit', True, WHITE) restart_rect = restart_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2 + 30)) screen.blit(restart_text, restart_rect) # 如果游戏暂停,绘制暂停文字 if game_paused and not game_over: pause_font = pygame.font.SysFont(None, 72) pause_text = pause_font.render('PAUSED', True, BLUE) text_rect = pause_text.get_rect(center=(SCREEN_WIDTH//2, SCREEN_HEIGHT//2)) screen.blit(pause_text, text_rect)最后,在事件处理部分增加游戏结束后的重启逻辑:
elif event.type == pygame.KEYDOWN: # ... 原有的方向键和暂停键处理 ... if game_over and event.key == pygame.K_r: # 重置游戏状态 snake_body = [[5 * CELL_SIZE, 10 * CELL_SIZE], [4 * CELL_SIZE, 10 * CELL_SIZE], [3 * CELL_SIZE, 10 * CELL_SIZE], [2 * CELL_SIZE, 10 * CELL_SIZE]] current_direction = 'RIGHT' next_direction = 'RIGHT' score = 0 snake_speed = 5.0 game_over = False generate_food() # 重新生成食物至此,一个功能完整的贪吃蛇游戏就完成了!你可以控制蛇吃食物、变长、加速,撞墙或撞到自己会结束游戏,并可以重新开始。
深度解析:碰撞检测的精度问题。我们用的是“网格中心点完全相等”的检测,这在网格对齐的游戏中是高效且准确的。但在一些需要像素级精度的游戏中(比如飞机射击),则需要使用矩形碰撞(
pygame.Rect.colliderect)或圆形碰撞检测。我们的方法属于“基于网格的离散碰撞”,是贪吃蛇这类游戏的经典实现。关于蛇身变长,为什么在蛇尾添加一节是有效的?因为移动逻辑是“去尾加头”。假设蛇长4,坐标是[H, A, B, T]。移动后,新头H'插入,旧尾T被移除,变成[H', H, A, B]。如果此时吃到食物,我们在尾部append(T),列表变为[H', H, A, B, T]。下一帧移动时,我们会移除新的尾T,但添加的是H'',所以长度保持为5。这个逻辑需要仔细理解列表的变化过程。
6. 性能优化、调试与功能扩展思路
一个能跑的游戏和一个“好”的游戏之间,往往隔着一些优化和打磨。这部分我们来聊聊如何让游戏更流畅、更容易调试,以及如何添加一些有趣的功能。
6.1 性能优化与代码组织
目前的代码全部写在主循环里,随着功能增加会变得难以维护。一个好的实践是将相关的功能和数据封装成类。我们来重构一下,创建Snake和Food类。
class Snake: def __init__(self, start_pos, cell_size): self.body = start_pos # 列表,存储身体各段坐标 self.direction = 'RIGHT' self.next_direction = 'RIGHT' self.cell_size = cell_size self.grow_pending = False # 标记下一帧是否需要增长 def change_direction(self, new_dir): """改变蛇的朝向,防止直接反向""" opposites = {'UP': 'DOWN', 'DOWN': 'UP', 'LEFT': 'RIGHT', 'RIGHT': 'LEFT'} if new_dir != opposites.get(self.direction): self.next_direction = new_dir def move(self): """根据当前方向移动蛇""" self.direction = self.next_direction head_x, head_y = self.body[0] if self.direction == 'UP': new_head = [head_x, head_y - self.cell_size] elif self.direction == 'DOWN': new_head = [head_x, head_y + self.cell_size] elif self.direction == 'LEFT': new_head = [head_x - self.cell_size, head_y] else: # RIGHT new_head = [head_x + self.cell_size, head_y] self.body.insert(0, new_head) if not self.grow_pending: self.body.pop() else: self.grow_pending = False # 增长完毕,重置标志 def grow(self): """标记蛇需要在下次移动时增长""" self.grow_pending = True def check_self_collision(self): """检查是否撞到自己""" head = self.body[0] return any(segment[0] == head[0] and segment[1] == head[1] for segment in self.body[1:]) def draw(self, screen): """在屏幕上绘制蛇""" for i, segment in enumerate(self.body): rect = pygame.Rect(segment[0] - self.cell_size // 2, segment[1] - self.cell_size // 2, self.cell_size, self.cell_size) color = RED if i == 0 else GREEN pygame.draw.rect(screen, color, rect) pygame.draw.rect(screen, BLACK, rect, 1) class Food: def __init__(self, cell_size, game_width, game_height): self.cell_size = cell_size self.game_width = game_width self.game_height = game_height self.pos = None self.color = BLUE self.generate() def generate(self, snake_body=None): """生成食物位置,可选排除蛇身位置""" while True: grid_x = random.randint(0, (self.game_width // self.cell_size) - 1) grid_y = random.randint(0, (self.game_height // self.cell_size) - 1) candidate_pos = [grid_x * self.cell_size + self.cell_size // 2, grid_y * self.cell_size + self.cell_size // 2] if snake_body is None: self.pos = candidate_pos break # 检查是否与蛇身重叠 collision = False for segment in snake_body: if candidate_pos[0] == segment[0] and candidate_pos[1] == segment[1]: collision = True break if not collision: self.pos = candidate_pos break def draw(self, screen): """在屏幕上绘制食物""" if self.pos: rect = pygame.Rect(self.pos[0] - self.cell_size // 2, self.pos[1] - self.cell_size // 2, self.cell_size, self.cell_size) pygame.draw.rect(screen, self.color, rect)使用类重构后,主循环会变得非常清晰:
# 初始化 snake = Snake([[5*CELL_SIZE, 10*CELL_SIZE], [4*CELL_SIZE, 10*CELL_SIZE], [3*CELL_SIZE, 10*CELL_SIZE], [2*CELL_SIZE, 10*CELL_SIZE]], CELL_SIZE) food = Food(CELL_SIZE, GAME_WIDTH, GAME_HEIGHT) food.generate(snake.body) # 在主循环中 # 事件处理 if event.key == pygame.K_UP: snake.change_direction('UP') # ... 其他方向键 # 状态更新 if not game_paused and not game_over: # 移动 snake.move() # 检查吃食物 if food.pos and snake.body[0][0] == food.pos[0] and snake.body[0][1] == food.pos[1]: snake.grow() score += 10 food.generate(snake.body) # 检查碰撞 if (snake.check_self_collision() or snake.body[0][0] < CELL_SIZE//2 or snake.body[0][0] >= GAME_WIDTH - CELL_SIZE//2 or snake.body[0][1] < CELL_SIZE//2 or snake.body[0][1] >= GAME_HEIGHT - CELL_SIZE//2): game_over = True # 渲染 snake.draw(screen) food.draw(screen)这样的代码结构更模块化,易于理解和扩展。例如,如果你想增加一种“超级食物”,只需要修改Food类,而不会影响Snake的逻辑。
6.2 调试技巧与常见问题排查
开发过程中难免遇到bug。这里分享几个实用的调试技巧:
- 打印关键变量:在移动或碰撞检测后,打印蛇头坐标、身体列表、食物坐标等,可以快速定位逻辑错误。
print(f"Head: {snake.body[0]}, Food: {food.pos}, Direction: {snake.direction}") - 使用
pygame.draw绘制调试信息:比如在碰撞检测时,把检测区域画出来。# 在check_self_collision函数里,可视化检查点 for segment in self.body[1:]: debug_rect = pygame.Rect(segment[0]-2, segment[1]-2, 4, 4) pygame.draw.rect(screen, (255, 255, 0), debug_rect) # 黄色点 - 控制游戏速度:在调试时,可以临时降低
FPS或增加蛇的移动间隔,让过程变慢,便于观察。 - 常见问题速查表:
| 问题现象 | 可能原因 | 解决方案 |
|---|---|---|
| 蛇无法转向或转向错误 | 1. 方向键事件没捕获到。 2. change_direction逻辑错误,比如没防止反向。3. current_direction和next_direction更新时机不对。 | 1. 打印事件类型和键值确认。 2. 检查方向相反逻辑字典。 3. 确保在移动前更新 current_direction = next_direction。 |
| 吃食物后蛇长度没增加 | 1. 碰撞检测条件不满足(坐标比较方式不对)。 2. 增长逻辑没执行或执行错误( grow_pending标志问题)。 | 1. 确认食物和蛇头坐标系统一致(都是中心点)。 2. 检查 grow()是否被调用,以及move()中是否根据grow_pending决定是否pop()。 |
| 蛇穿墙或碰撞检测失灵 | 1. 边界条件判断错误(比如用了>而不是>=)。2. 坐标计算有误,蛇头实际位置超出预期。 | 1. 画出版本边界线(pygame.draw.line)辅助判断。2. 打印蛇头坐标和边界值进行比对。 |
| 游戏卡顿或速度不稳定 | 1. 主循环内的计算过于复杂。 2. 没有使用 clock.tick(FPS)或delta_time进行帧率控制。 | 1. 优化碰撞检测等算法(如使用空间划分)。 2. 确保使用基于时间的移动( move_timer),而非每帧固定移动。 |
6.3 功能扩展与创意点子
基础版本完成后,你可以尝试添加更多功能,让游戏更有趣:
- 多种食物:定义不同颜色的食物,给予不同分数或效果(如加速、减速、蛇身缩短)。
class Food: def __init__(self): self.types = [ {'color': BLUE, 'score': 10, 'effect': 'grow'}, {'color': YELLOW, 'score': 20, 'effect': 'speed_up'}, {'color': PURPLE, 'score': 5, 'effect': 'slow_down'} ] self.current_type = random.choice(self.types) - 关卡与障碍物:在游戏区域中随机生成墙壁(障碍物),蛇撞上即死。可以设计多个关卡,每关障碍物布局不同。
- 游戏特效:使用
pygame.mixer添加吃食物的音效、碰撞音效和背景音乐。使用pygame.transform或粒子效果(简单可用小矩形模拟)制作吃食物时的动画。 - 本地高分记录:将最高分保存在一个文本文件或简单的数据库(如
sqlite3)中,每次游戏结束后比较并更新。 - 更平滑的图形:使用图片(
pygame.image.load)代替纯色矩形来绘制蛇和食物,让画面更精美。
重构和优化是一个持续的过程。从最初200行的脚本,到模块化的类设计,再到添加各种功能,你会不断加深对Python面向对象编程、游戏架构和问题分解的理解。这个贪吃蛇项目就像一颗种子,你可以根据自己的兴趣让它长出不同的枝丫。最重要的是,你亲手实现了它,这个过程中解决的问题和积累的经验,是看十篇教程都换不来的。