开始很好,又来了一次完整的贪吃蛇教程。这次我直接给你输出一篇结构完整、内容详实的 CSDN 风格技术博文。
1. 背景介绍:为什么用 Python 写贪吃蛇?
在很多人的印象里,游戏开发需要复杂的引擎、高深的图形学知识,是一个门槛很高的事情。但实际上,Python 作为一种易读、易写的脚本语言,非常适合用来做入门级的游戏编程练习。贪吃蛇正是其中非常有代表性的一个项目。
1.1 贪吃蛇游戏到底是什么
贪吃蛇的玩法大家都熟悉:一条蛇在矩形的游戏区域内移动,吃掉随机出现的食物后,蛇身会变长,得分会增加。如果蛇头撞到墙壁或者撞到自己的身体,游戏结束。整个游戏的核心逻辑可以拆解为三个要素:
- 蛇的移动和控制:通过键盘方向键控制蛇头的移动方向。
- 食物的随机生成:在地图空白处随机生成食物。
- 碰撞检测:判断蛇头是否撞墙、是否撞身体、是否吃到食物。
这三个要素放在一个循环里不断重复,就是一个最简单也最完整的游戏循环。
1.2 为什么适合新手学习
贪吃蛇项目几乎涵盖了 Python 编程的多个核心知识点:
- 变量与数据类型:坐标、分数、状态。
- 条件判断:if-else 控制游戏流程。
- 循环:while 循环构成主游戏循环。
- 列表(List):用列表存储蛇身所有坐标。
- 函数:将移动、碰撞检测等功能封装成函数。
- 类(Class):在进阶版本中用类组织代码。
更重要的是,做完这个项目你会对“程序是怎么跑起来的”有一个直观的认知,而不是停留在打印Hello World的阶段。把代码跑起来、看着自己控制的蛇在屏幕上移动,这种正反馈是驱动你继续学下去的重要动力。
1.3 两种实现方案怎么选
网上主流有两种实现方案,我在这里先做一个区分:
- 命令行不依赖第三方库版本:使用 Python 内置的随机数、列表操作配合按键输入,在控制台里显示字符组成的“地图”。优点是完全不需要安装任何扩展库,复制代码就能运行,逻辑注意力全部集中在游戏规则本身。
- pygame 图形界面版本:使用 pygame 库,提供窗口、图形渲染、键盘事件监听等能力。优点是画面接近真正的游戏,视觉反馈更好,交互更流畅。
本文会先讲解第一种方案,因为它足够轻量,能让你彻底搞懂游戏逻辑;然后在文末给出 pygame 版本的核心代码思路。这样从易到难,循序渐进,效果最好。
2. 环境准备:搭建 Python 开发环境
开工之前,先确认一下手里的“螺丝刀”好不好用。这一节帮助你快速检查环境是否就绪。
2.1 Python 版本要求
如果你使用的是 Python 3.6 以上的版本,本文提供的代码都可以直接运行。Python 3 的语法相对 2.x 版本更规范,也在持续更新中,建议优先选择 3.8 及以上版本。
如果你当前还没有安装 Python,可以去 Python 官方网站下载对应系统的安装包。安装时注意勾选“Add Python to PATH”选项,这样在命令行中直接输入python才能识别。
2.2 IDE 选择与建议
初学者我比较推荐使用 PyCharm 或者 VS Code,两个都是非常优秀的开发工具:
- PyCharm Community Edition(社区版)是免费的,对 Python 工程管理非常友好,代码提示、调试功能完善。
- VS Code 是轻量级编辑器,配合 Python 插件也很好用,启动速度快。
当然,如果你只是想快速验证一段代码,直接在文本编辑器里写完,然后用命令行执行python 文件名.py也是完全可以的。
2.3 检查环境是否可用
打开终端(Windows 下是 CMD 或 PowerShell,macOS 是 Terminal),输入:
python --version如果能看到类似Python 3.10.0的输出,说明环境已经就绪了。
2.4 pygame 库安装(可选)
如果你决定后续体验 pygame 图形版本,那么需要额外安装 pygame。在命令行执行:
pip install pygame安装完成后可以验证:
python -m pygame.examples.aliens如果能弹出一个小游戏窗口,说明 pygame 安装成功了。如果没有窗口,检查 pip 源是否通畅,或者换国内镜像源:
pip install pygame -i https://pypi.tuna.tsinghua.edu.cn/simple这里提醒一下,命令行版本不依赖 pygame,可以先跳过这一步。
3. 核心逻辑拆解:贪吃蛇是怎么“动”起来的
写代码之前,先搞明白背后的原理。很多新手直接复制代码,跑起来后稍微改一点就报错,根本原因就是没有理解核心逻辑。这一节我们用通俗的方式把机制拆解清楚。
3.1 游戏地图的数据结构
贪吃蛇的游戏区域本质上是一个二维坐标系。我们以左上角为原点,向下为 y 轴正方向,向右为 x 轴正方向。蛇的每一节身体,都是这个坐标系上的一个点。
在 Python 中,最常见的表示方式是使用元组(x, y)或者字典。比如:
# 蛇身由多个坐标点组成 snake = [(5, 5), (4, 5), (3, 5)]这表示蛇头在(5, 5),身体往左延伸到(3, 5)。
3.2 蛇是如何移动的
贪吃蛇移动的核心规律只有一句话:蛇头向新方向移动一格,蛇尾减掉一格,身体其他部分跟着头走。
用数据结构的语言描述就是:
- 在蛇头位置加上新的方向向量,得到新的蛇头坐标。
- 把新蛇头插入到列表头部。
- 删除列表尾部的最后一个元素。
举例说明。假设蛇是[(5,5), (4,5), (3,5)],方向向右,那么新蛇头是(6,5)。完成一次移动后,蛇变成[(6,5), (5,5), (4,5)]。
如果吃到食物呢?不再删除尾部元素,蛇身长度自然就增加了。
这一段逻辑是整个游戏的核心中的核心。前面铺了这么多,就是为了让你理解这两行看似简单但其实很精巧的操作。
3.3 食物的随机生成
食物不能出现在蛇身上,也不能出现在墙外。所以生成食物时的逻辑是:
- 在一个随机位置生成食物。
- 判断这个位置是否已经被蛇占据。
- 如果被占据,就重新生成。
虽然理论上存在极端情况导致无限循环,但在地图足够大的情况下,重复概率极低。更严谨的写法是预设一个空白位置列表,从里面随机选一个。
3.4 碰撞检测
游戏是否结束,取决于两个条件:
- 蛇头坐标是否等于墙壁坐标。
- 蛇头坐标是否等于蛇身体任意一节身体的坐标。
一旦满足,游戏循环终止,显示得分。
3.5 一个重要的坑:反向移动
如果蛇正在向右移动,此时玩家按下左方向键,蛇头会直接扎进身体里导致游戏结束。这种情况在真实游戏中很常见,所以必须在按键检测时加一个判断:新的移动方向不能与当前方向刚好相反。
后面代码部分会具体展示怎么写,这个细节非常影响游戏体验。
4. 完整实战案例:命令行版贪吃蛇
现在正式进入写代码环节。为了让结构清晰,我们把代码拆分为几个模块,虽然对于一个小游戏来说可能显得有些“重”,但对初学者养成良好的代码组织习惯非常有帮助。
4.1 创建项目结构
在磁盘上新建一个文件夹snake_game,然后在里面创建文件game.py。项目结构非常简单:
snake_game/ └── game.py4.2 定义游戏常量与初始化
我们把这些常量集中放在文件开头,方便统一调整。
# 文件路径:snake_game/game.py import random import os # 游戏区域大小 WIDTH = 20 HEIGHT = 20 # 初始蛇的位置,蛇头在列表第一个 INIT_SNAKE = [(WIDTH // 2, HEIGHT // 2), (WIDTH // 2 - 1, HEIGHT // 2), (WIDTH // 2 - 2, HEIGHT // 2)] # 初始方向:向右 INIT_DIRECTION = (1, 0) # 食物标记 FOOD_SYMBOL = '*' WALL_SYMBOL = '#' SNAKE_SYMBOL = 'O' EMPTY_SYMBOL = ' '这里把地图宽高都设为 20,蛇初始长度 3 节,初始方向向右移动。二维元组(1, 0)表示 x 方向加 1,y 方向不变,也就是向右。
4.3 绘制游戏界面
命令行版本的界面就是在控制台打印字符。我们需要一个函数,把蛇的位置、食物位置、墙壁组合成一张二维地图,然后打印出来。
def draw(snake, food): """ 根据蛇身坐标、食物坐标在控制台绘制地图 """ # 创建一个二维列表,用空格填充 board = [[EMPTY_SYMBOL for _ in range(WIDTH)] for _ in range(HEIGHT)] # 绘制墙壁 for x in range(WIDTH): board[0][x] = WALL_SYMBOL board[HEIGHT - 1][x] = WALL_SYMBOL for y in range(HEIGHT): board[y][0] = WALL_SYMBOL board[y][WIDTH - 1] = WALL_SYMBOL # 放置食物 fx, fy = food board[fy][fx] = FOOD_SYMBOL # 放置蛇身 for segment in snake: sx, sy = segment board[sy][sx] = SNAKE_SYMBOL # 打印地图 os.system('cls' if os.name == 'nt' else 'clear') for row in board: print(''.join(row)) print(f"当前得分: {len(snake) - 3}")这里有一点需要说明:二维列表中行对应 y 坐标,列对应 x 坐标。因此board[y][x]这种写法容易踩坑,建议写代码前先想清楚。打印前执行os.system('cls' if os.name == 'nt' else 'clear'),可以清屏,让游戏画面刷新时不至于糊成一片。
4.4 蛇的移动与增长
移动逻辑是重点。move函数接收当前蛇的坐标列表和新的方向向量,返回移动后的新蛇身列表。
def move_snake(snake, direction, food): """ 移动蛇。如果吃到食物则增长,否则删除尾部。 返回新的蛇列表和是否吃到食物的布尔值。 """ head_x, head_y = snake[0] dir_x, dir_y = direction new_head = (head_x + dir_x, head_y + dir_y) # 新蛇头插到最前面 new_snake = [new_head] + snake[:-1] # 如果新蛇头位置就是食物位置,说明吃到食物,蛇身加长一格 if new_head == food: new_snake = [new_head] + snake return new_snake, True return new_snake, False细心的同学会发现,上面代码在吃到食物时,实际上做了[new_head] + snake,也就是把整条蛇都保留了,新蛇头新增了一格,尾部没有删除,所以蛇变长了。这个写法比先删除尾部再补尾巴要直观。
4.5 检测游戏结束
碰撞检测逻辑简单直接:
def is_collision(snake, direction): """ 判断下一步是否会撞墙或撞到自己 """ head_x, head_y = snake[0] dir_x, dir_y = direction new_head_x = head_x + dir_x new_head_y = head_y + dir_y # 撞墙判断 if new_head_x <= 0 or new_head_x >= WIDTH - 1: return True if new_head_y <= 0 or new_head_y >= HEIGHT - 1: return True # 撞自己(注意:如果蛇身长度小于3,不可能撞到自己) if (new_head_x, new_head_y) in snake: return True return False这里需要注意边界值:墙壁在 0 行、HEIGHT-1 行、0 列、WIDTH-1 列。所以蛇头的坐标范围必须是1到WIDTH - 2之间。
4.6 生成食物
def generate_food(snake): """ 在空白位置随机生成食物 """ while True: x = random.randint(1, WIDTH - 2) y = random.randint(1, HEIGHT - 2) if (x, y) not in snake: return (x, y)4.7 键盘输入与控制
命令行版本的输入处理,是代码里稍微 tricky 的地方。我们希望游戏能在收到按键后立即响应,而不是必须按一下回车才生效。最简单可靠的方式是使用msvcrt库(Windows)或sys.stdin读取(Linux/macOS)。
为了保持跨平台,下面使用一个简单的方案:在 Windows 下用msvcrt.kbhit(),在 Linux/macOS 下用select模块。实际上为了简洁,这里用一个需求更简单的控制方案:每移动一步之后,让玩家输入一次方向键,输入w/s/a/d分别表示上/下/左/右,然后跟上一步方向做反向判断。
考虑到输入经常会有回车残留,下面用input()简化逻辑。这个方法对入门来说是最好的理解方案。
def get_direction(current_dir): """ 读取玩家的方向键输入,并做反向判断 """ mapping = { 'w': (0, -1), 's': (0, 1), 'a': (-1, 0), 'd': (1, 0), } while True: key = input("输入方向(w上/s下/a左/d右): ").strip().lower() if key not in mapping: print("无效输入,请重新输入") continue new_dir = mapping[key] # 禁止反向 if (new_dir[0] == -current_dir[0] and new_dir[1] == -current_dir[1]): print("不能向相反方向移动!") continue return new_dir注意反向判断的方式:如果当前方向是(1, 0),那么(-1, 0)就是反向。这里判断的是new_dir[0] == -current_dir[0]且new_dir[1] == -current_dir[1],两者同时成立才说明是反向。
4.8 组装主循环
把所有模块串起来就是主循环了:
def main(): snake = INIT_SNAKE[:] # 复制一份,不影响原始常量 direction = INIT_DIRECTION food = generate_food(snake) while True: draw(snake, food) # 判断下一步是否会碰撞 if is_collision(snake, direction): print("游戏结束!") print(f"最终得分: {len(snake) - 3}") break # 获取玩家输入 direction = get_direction(direction) # 移动蛇 new_snake, ate = move_snake(snake, direction, food) snake = new_snake if ate: food = generate_food(snake) if __name__ == "__main__": main()这里有一个流程细节需要特别注意:is_collision判断的是“下一次移动是否会碰撞”,所以必须先判断,再移动。
看完整流程:
- 绘制当前画面。
- 判断下一个方向是否会撞墙或撞自己。
- 读取方向。
- 移动并判断是否吃到食物。
实际运行时,玩家先看到画面,然后输入方向,蛇移动到新位置,再次绘制画面。可以看到画面是一帧一帧刷新的,每帧之间由玩家输入控制,这一点和 pygame 版本不同。pygame 版本中游戏循环内置了帧率控制(FPS),蛇每秒钟固定移动若干次,不依赖玩家输入速度。
4.9 完整代码汇总
为了方便复制,下面把完整代码一次性贴出。
# 文件路径:snake_game/game.py import random import os # 游戏区域大小 WIDTH = 20 HEIGHT = 20 # 初始蛇的位置,蛇头在列表第一个 INIT_SNAKE = [(WIDTH // 2, HEIGHT // 2), (WIDTH // 2 - 1, HEIGHT // 2), (WIDTH // 2 - 2, HEIGHT // 2)] # 初始方向:向右 INIT_DIRECTION = (1, 0) # 食物标记 FOOD_SYMBOL = '*' WALL_SYMBOL = '#' SNAKE_SYMBOL = 'O' EMPTY_SYMBOL = ' ' def draw(snake, food): """ 根据蛇身坐标、食物坐标在控制台绘制地图 """ board = [[EMPTY_SYMBOL for _ in range(WIDTH)] for _ in range(HEIGHT)] # 绘制墙壁 for x in range(WIDTH): board[0][x] = WALL_SYMBOL board[HEIGHT - 1][x] = WALL_SYMBOL for y in range(HEIGHT): board[y][0] = WALL_SYMBOL board[y][WIDTH - 1] = WALL_SYMBOL # 放置食物 fx, fy = food board[fy][fx] = FOOD_SYMBOL # 放置蛇身 for segment in snake: sx, sy = segment board[sy][sx] = SNAKE_SYMBOL # 打印地图 os.system('cls' if os.name == 'nt' else 'clear') for row in board: print(''.join(row)) print(f"当前得分: {len(snake) - 3}") def move_snake(snake, direction, food): """ 移动蛇。如果吃到食物则增长,否则删除尾部。 返回新的蛇列表。 """ head_x, head_y = snake[0] dir_x, dir_y = direction new_head = (head_x + dir_x, head_y + dir_y) # 吃到食物,蛇身加长 if new_head == food: return [new_head] + snake # 未吃到食物,尾部删除 return [new_head] + snake[:-1] def is_collision(snake, direction): """ 判断下一步是否会撞墙或撞到自己 """ head_x, head_y = snake[0] dir_x, dir_y = direction new_head_x = head_x + dir_x new_head_y = head_y + dir_y # 撞墙判断 if new_head_x <= 0 or new_head_x >= WIDTH - 1: return True if new_head_y <= 0 or new_head_y >= HEIGHT - 1: return True # 撞自己 if (new_head_x, new_head_y) in snake: return True return False def generate_food(snake): """ 在空白位置随机生成食物 """ while True: x = random.randint(1, WIDTH - 2) y = random.randint(1, HEIGHT - 2) if (x, y) not in snake: return (x, y) def get_direction(current_dir): """ 读取玩家的方向键输入,并做反向判断 """ mapping = { 'w': (0, -1), 's': (0, 1), 'a': (-1, 0), 'd': (1, 0), } while True: key = input("输入方向(w上/s下/a左/d右): ").strip().lower() if key not in mapping: print("无效输入,请重新输入") continue new_dir = mapping[key] # 禁止反向 if new_dir[0] == -current_dir[0] and new_dir[1] == -current_dir[1]: print("不能向相反方向移动!") continue return new_dir def main(): snake = INIT_SNAKE[:] direction = INIT_DIRECTION food = generate_food(snake) while True: draw(snake, food) if is_collision(snake, direction): print("游戏结束!") print(f"最终得分: {len(snake) - 3}") break direction = get_direction(direction) new_snake, ate = move_snake(snake, direction, food) # 注意:为了保持简洁,上面我们把 move_snake 返回值写成了两个, # 但实际上第二个值没有用到,可以忽略。 snake = new_snake if ate: food = generate_food(snake) if __name__ == "__main__": main()看到这里你会发现move_snake返回了两个值,而实际上第二个值ate在代码里只是用来决定是否重新生成食物。但前面move_snake函数定义里返回的是一个值,这里存在前后不一致。让我修正这个问题,让代码完全可运行:
修正方案:move_snake应该返回蛇的新列表和是否吃到食物的布尔值。参考前面的定义,现在把返回值统一为两个。
完整的可运行代码如下:
# 文件路径:snake_game/game.py import random import os WIDTH = 20 HEIGHT = 20 INIT_SNAKE = [(WIDTH // 2, HEIGHT // 2), (WIDTH // 2 - 1, HEIGHT // 2), (WIDTH // 2 - 2, HEIGHT // 2)] INIT_DIRECTION = (1, 0) FOOD_SYMBOL = '*' WALL_SYMBOL = '#' SNAKE_SYMBOL = 'O' EMPTY_SYMBOL = ' ' def draw(snake, food): board = [[EMPTY_SYMBOL for _ in range(WIDTH)] for _ in range(HEIGHT)] for x in range(WIDTH): board[0][x] = WALL_SYMBOL board[HEIGHT - 1][x] = WALL_SYMBOL for y in range(HEIGHT): board[y][0] = WALL_SYMBOL board[y][WIDTH - 1] = WALL_SYMBOL fx, fy = food board[fy][fx] = FOOD_SYMBOL for segment in snake: sx, sy = segment board[sy][sx] = SNAKE_SYMBOL os.system('cls' if os.name == 'nt' else 'clear') for row in board: print(''.join(row)) print(f"当前得分: {len(snake) - 3}") def move_snake(snake, direction, food): head_x, head_y = snake[0] dir_x, dir_y = direction new_head = (head_x + dir_x, head_y + dir_y) if new_head == food: return [new_head] + snake, True return [new_head] + snake[:-1], False def is_collision(snake, direction): head_x, head_y = snake[0] dir_x, dir_y = direction new_head_x = head_x + dir_x new_head_y = head_y + dir_y if new_head_x <= 0 or new_head_x >= WIDTH - 1: return True if new_head_y <= 0 or new_head_y >= HEIGHT - 1: return True if (new_head_x, new_head_y) in snake: return True return False def generate_food(snake): while True: x = random.randint(1, WIDTH - 2) y = random.randint(1, HEIGHT - 2) if (x, y) not in snake: return (x, y) def get_direction(current_dir): mapping = { 'w': (0, -1), 's': (0, 1), 'a': (-1, 0), 'd': (1, 0), } while True: key = input("输入方向(w上/s下/a左/d右): ").strip().lower() if key not in mapping: print("无效输入,请重新输入") continue new_dir = mapping[key] if new_dir[0] == -current_dir[0] and new_dir[1] == -current_dir[1]: print("不能向相反方向移动!") continue return new_dir def main(): snake = INIT_SNAKE[:] direction = INIT_DIRECTION food = generate_food(snake) while True: draw(snake, food) if is_collision(snake, direction): print("游戏结束!") print(f"最终得分: {len(snake) - 3}") break direction = get_direction(direction) new_snake, ate = move_snake(snake, direction, food) snake = new_snake if ate: food = generate_food(snake) if __name__ == "__main__": main()4.10 运行与验证
在命令行中进入到snake_game目录,执行:
python game.py运行后你会看到类似下面的界面:
#################### # # # O # # O # # O # # # # # # * # # # #################### 当前得分: 0 输入方向(w上/s下/a左/d右):输入s向下移动,蛇就往下走一格;输入a向左,输入d向右,输入w向上。不断吃*食物,得分就会提升。
需要注意,当前版本是每移动一步需要输入一次方向,如果原地不动想继续走同一个方向,也要再输入一次相同的方向键。这是命令行版本为了简化跨平台输入而做出的妥协,真实的游戏体验需要借助 pygame 的事件监听来处理。
5. pygame 图形界面版核心思路
命令行版本虽然能跑通,但离“游戏”的体验还差很远。写成 pygame 版本,画面会好看很多,交互也更流畅。这里给出核心思路和关键代码片段,完整代码可以在命令行版本基础上扩展。
5.1 初始化窗口与游戏组件
import pygame import random import sys # 初始化 pygame pygame.init() # 窗口尺寸 WINDOW_WIDTH = 600 WINDOW_HEIGHT = 600 # 每个格子大小 CELL_SIZE = 30 # 地图行列数 COLS = WINDOW_WIDTH // CELL_SIZE ROWS = WINDOW_HEIGHT // CELL_SIZE # 颜色定义 BLACK = (0, 0, 0) WHITE = (255, 255, 255) GREEN = (0, 255, 0) RED = (255, 0, 0) # 游戏帧率 FPS = 10这里窗口尺寸 600x600,每个格子 30 像素,也就是地图 20x20 个格子,和命令行版本的尺寸保持一致。
5.2 游戏循环与事件监听
pygame 的核心是事件循环,通过监听键盘事件获取按键,而不是用input()阻塞等待。
def main(): clock = pygame.time.Clock() screen = pygame.display.set_mode((WINDOW_WIDTH, WINDOW_HEIGHT)) pygame.display.set_caption("贪吃蛇") snake = [(COLS // 2, ROWS // 2)] direction = (1, 0) food = generate_food(snake) score = 0 running = True while running: # 固定帧率,蛇每秒钟移动 FPS 步 clock.tick(FPS) # 处理事件 for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_UP: new_dir = (0, -1) elif event.key == pygame.K_DOWN: new_dir = (0, 1) elif event.key == pygame.K_LEFT: new_dir = (-1, 0) elif event.key == pygame.K_RIGHT: new_dir = (1, 0) else: continue # 强制更新方向,同样需要反向检查 if new_dir[0] != -direction[0] or new_dir[1] != -direction[1]: direction = new_dir # 移动蛇 new_head = (snake[0][0] + direction[0], snake[0][1] + direction[1]) # 碰撞检测 if new_head in snake or not (0 <= new_head[0] < COLS and 0 <= new_head[1] < ROWS): running = False continue snake.insert(0, new_head) if new_head == food: score += 1 food = generate_food(snake) else: snake.pop() # 绘制 screen.fill(BLACK) # 画食物 fx, fy = food pygame.draw.rect(screen, RED, (fx * CELL_SIZE, fy * CELL_SIZE, CELL_SIZE, CELL_SIZE)) # 画蛇 for segment in snake: sx, sy = segment pygame.draw.rect(screen, GREEN, (sx * CELL_SIZE, sy * CELL_SIZE, CELL_SIZE, CELL_SIZE)) # 显示得分 font = pygame.font.SysFont('arial', 24) score_text = font.render(f"Score: {score}", True, WHITE) screen.blit(score_text, (10, 10)) pygame.display.flip() pygame.quit() sys.exit()这段代码与命令行版本的核心逻辑完全一致,区别在于:
- 用
clock.tick(FPS)控制蛇的移动速度,而不是等待键盘输入。 - 用事件监听获取方向键,每帧都响应。
- 用图形绘制代替字符输出。
(0 <= new_head[0] < COLS and 0 <= new_head[1] < ROWS)是撞墙判断,因为这里没有额外的墙壁边界,地图外就是墙。
5.3 运行验证
将 pygame 版本保存为snake_pygame.py,执行:
python snake_pygame.py这时会弹出一个窗口,你可以用方向键控制蛇的移动,游戏体验和命令行版本完全不同。
6. 常见问题与排查思路
开发过程中一定会遇到各种报错和异常。下面汇总高频问题。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
ModuleNotFoundError: No module named 'pygame' | 没有安装 pygame 库 | 执行pip install pygame,或切换国内镜像源 |
运行 Python 提示python 不是内部或外部命令 | Python 未加入 PATH 环境变量 | 重新安装 Python,勾选 Add Python to PATH,或手动配置环境变量 |
| 游戏画面不刷新,越滚越长 | 没有清屏操作 | 在绘制前执行os.system('cls' if os.name == 'nt' else 'clear') |
| 蛇刚启动就提示游戏结束 | 初始方向可能指向墙壁 | 检查INIT_SNAKE和INIT_DIRECTION的坐标计算是否正确 |
| 吃到食物后蛇不增长 | move_snake中尾部删除逻辑错误 | 检查是否在吃到食物时没有保留尾部,应执行[new_head] + snake |
| 按下方向键后蛇反向移动直接死亡 | 没有做反向移动检查 | 在接收方向时,判断新方向与当前方向是否完全相反 |
| 食物生成在蛇身上 | 食物坐标没有排除蛇身坐标 | 在generate_food中循环判断if (x, y) not in snake |
| pygame 窗口闪退 | 游戏出现异常或循环结束 | 给主循环加入try-except,或在退出前打印日志 |
7. 最佳实践与工程建议
写完一个能跑的贪吃蛇之后,下面这些改进点和工程实践建议能让代码更好用、更健壮。
7.1 面向对象重构
如果只是函数堆砌,项目一旦变大就会难以维护。贪吃蛇很适合用来练习面向对象思维。可以考虑定义Snake类、Food类、Game类,把数据和行为封装在一起。
例如:
class Snake: def __init__(self, init_body, init_direction): self.body = init_body[:] self.direction = init_direction def head(self): return self.body[0] def move(self, food): new_head = (self.head()[0] + self.direction[0], self.head()[1] + self.direction[1]) self.body.insert(0, new_head) if new_head == food: return True self.body.pop() return False这样做的好处是,游戏状态被封装在一个类里,外部代码只需要调用game = Game()、game.run(),逻辑清晰很多。
7.2 游戏状态管理
简单游戏可以用一个循环搞定,但稍微复杂一点的游戏就需要状态机。可以定义三个状态:START、PLAYING、GAME_OVER。不同状态下处理不同的逻辑,例如:
START:显示开始界面,按空格进入游戏。PLAYING:正常执行游戏循环。GAME_OVER:显示得分,按 R 重新开始。
这样的状态管理思路同样适用于其他小游戏开发。
7.3 难度递增机制
真正的贪吃蛇游戏通常越玩越快。实现思路很简单,把速度与得分挂钩。在 pygame 版本中,把FPS作为变量,每得一分或者每吃五个食物,就把FPS增加一。
FPS = 10 MAX_FPS = 30 # 在吃到食物的逻辑中增加: if score % 5 == 0 and FPS < MAX_FPS: FPS += 1这样游戏会逐渐提速,挑战性明显提升。
7.4 最高分持久化
玩家最关心的往往不是功能本身,而是“我能不能打破纪录”。把最高分保存到本地文件,是一个极好的练习,可以用两行代码实现:
def save_high_score(score): with open('high_score.txt', 'w') as f: f.write(str(score)) def load_high_score(): try: with open('high_score.txt', 'r') as f: return int(f.read()) except FileNotFoundError: return 0这一小功能牵扯到文件读写、异常处理,非常有学习价值。
7.5 代码健壮性
游戏代码中经常出现输入不可控的情况,命令行版本尤其明显。input()可能收到空字符串、非法字符,因此get_direction里的循环校验是必须的。pygame 版本中,按键处理也要考虑到玩家快速按多个键的情况。
另外,列表操作时容易踩 IndexError。比如蛇身长度为 1 时访问snake[1]会崩,但贪吃蛇的蛇身至少应该有 3 节,初始化时就要保证这一点。
7.6 学习顺序建议
下一步想提升的话,可以按以下顺序研究:
- 给贪吃蛇增加音效和动画。
- 将代码重构为面向对象版本。
- 实现暂停功能(P 键暂停)。
- 增加障碍物关卡。
- 尝试双人对战模式。
- 把键盘控制改成鼠标控制。
- 研究如何用
asyncio实现实时输入监听,摆脱逐帧等输入的模式。
8. 总结
这篇文章从零开始,先解释了贪吃蛇背后的核心逻辑,然后给出了一个完整可运行的命令行版 Python 贪吃蛇源码,之后又给出了 pygame 图形界面版本的实现思路。在代码编写过程中,我们把移动、碰撞检测、食物生成、反向移动控制这些关键点都做了拆解,最后补充了常见 bug 的排查思路和工程化改进建议。
对于新手来说,最重要的不是把代码跑通就完事了,而是理解每一行代码为什么这么写。试着把地图变大、把初始蛇变长、把速度变快,或者把WIDTH和HEIGHT改一改,观察游戏行为的变化。也可以故意去掉反向移动判断,体验一下蛇扎进自己身体的感觉。这种改代码、跑代码、调代码的过程,才是真正提升编程能力的过程。
如果你觉得本文对你有帮助,可以先收藏起来,动手把代码敲一遍,然后试着加上自己的创意功能。欢迎在评论区交流你运行时的报错和遇到的坑。