贪吃蛇(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)。
- 状态管理与碰撞检测。
你可以在此基础上继续修改代码,加入自己的创意,打造一个独一无二的贪吃蛇版本!