news 2026/10/14 3:49:39

从零开始用Pygame开发贪吃蛇:游戏循环、碰撞检测与坐标转换实战

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从零开始用Pygame开发贪吃蛇:游戏循环、碰撞检测与坐标转换实战

前两天有个朋友问我:“我想写个小游戏,但一看Unity就头大,有没有更轻的入口?”我直接告诉他:去试试Pygame。不是因为它能做出多惊艳的作品,而是因为它足够简单,简单到你能在几个小时内跑通一个完整的小游戏,并且真正理解游戏运行的本质。我最早接触Pygame时,花了不到一个周末就写出了第一个贪吃蛇。那段时间踩过的坑、悟出来的道理,到现在做其他图形项目还在用。所以这篇文章,我想用一套“先拆思路、再写代码、最后修坑”的方式,带你用Pygame开发你的第一个小游戏。

这篇文章不是让你照抄一段代码就完事,而是把背后的设计逻辑、坐标换算、事件循环这些关键点掰开讲清楚。适合刚学Python、想接触游戏开发的初学者,也适合想给学生带兴趣班的老师做参考。整个过程中,你不需要掌握高深的算法,只需要基本的Python语法和一点点耐心。

1. 为什么要用Pygame做第一个小游戏

1.1 Pygame到底适合谁

很多初学者一上来就想做“3D大作”,结果被引擎的编辑器、资源管线和各种组件劝退。Pygame不是引擎,它更像是一盒积木:窗口、图片、矩形、声音、事件,所有东西都是Python对象,你可以直接控制每一个细节。这种“底层但可控”的感觉,恰好是建立游戏思维最好的方式。

Pygame适合三类人:

  • 刚学会Python基础语法的人:你可以不用理解复杂的类继承,先用函数写一个游戏,再慢慢重构。
  • 想理解游戏循环和事件驱动的人:用Pygame你能清晰看到“每一帧做了什么”,而不是被引擎封装得严严实实。
  • 需要快速出原型的人:比如某个创意关卡想验证手感,用Pygame比开一个重量级引擎快得多。

但要注意,Pygame不适合做重度商业游戏,它的性能上限摆在那里。我做第一个小游戏时压根不考虑这些,因为目标不是做产品,而是验证“我能独立完成一个游戏”。

1.2 和网页游戏、Unity相比,Pygame赢在哪里

拿当时我同样试过的几个方案来看,Pygame的优势很突出:

对比项Pygame网页版 Canvas / PhaserUnity / Godot
环境复杂度只要Python和pip需要浏览器调试,还要选框架几个GB的编辑器,学习曲线陡峭
代码可见度所有逻辑都在py文件里逻辑被框架和DOM事件打散脚本和编辑器拖拽混合
适合入门非常适合中等偏重
做第一个游戏的时间半天到两天两天到一周一周以上

我自己用Unity做过一个2D demo,光是场景管理和碰撞体层级就折腾了很久。而用Pygame时,一个窗口、一个while循环就撑起了全部游戏。Pygame不是万能药,但作为“第一个小游戏”的起点,它几乎是最优解。

2. 开发前的准备:环境、目录和游戏循环

2.1 安装Pygame的版本选择

安装很简单,但版本问题值得说一句。我用的是Python 3.10,当时直接执行了:

pip install pygame

如果你在Python 3.12或更高版本上遇到兼容问题,建议用官方预编译的wheel包。Windows用户注意,Pygame有两个分支:一个是社区维护版pygame,另一个是旧版pygame-ce。普通学习直接装pygame就行,性能差别在这个规模下感知不强。

安装完成后,验证一下:

import pygame print(pygame.version.ver)

能打印出版本号,环境就准备好了。很多人卡在“安装了却无法导入”,多半是pip装到了另一个Python环境,比如同时装了Anaconda和系统Python。这时候在终端里输入where python或which python,再用对应的pip重新安装。

2.2 项目目录和最小骨架

做第一个小游戏,我建议用一个干净的目录结构,而不是把所有代码塞进一个文件。推荐这样:

snake_game/ ├── main.py ├── settings.py ├── game.py ├── player.py ├── food.py └── assets/

一开始可能觉得拆这么细很麻烦,但当你改到第20版就会发现,每个模块只做一件事,排查问题容易得多。当然,如果只是练手,先写一个main.py也可以,我后来是重构时才拆开的。

最小骨架就是打开一个窗口,并让它不断刷新而不卡死:

import pygame pygame.init() screen = pygame.display.set_mode((640, 480)) pygame.display.set_caption("我的第一个小游戏") running = True while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False pygame.display.flip() pygame.quit()

这段代码虽然什么都不做,但它是所有Pygame游戏的地基。pygame.init()负责初始化所有模块,set_mode创建窗口,while循环是游戏的“生命线”。没有这个循环,窗口会一闪而过。

2.3 pygame.display.flip和update到底有什么区别

这里有个细节坑了我第一次。pygame.display.flip()是整窗口刷新,pygame.display.update()可以只刷新指定区域。新手阶段统一用flip()没有毛病,因为画面简单。但如果你后面做了大面积动态区域,用update局部刷新能明显省性能。

用个比喻:flip是把整张画布重新贴到屏幕上,update则是只贴你改过的补丁。小游戏里性能瓶颈不在显示,所以放心用flip。

还有帧率控制。经典写法是:

clock = pygame.time.Clock() while running: clock.tick(60) ...

clock.tick(60)的意思是限制每秒最多跑60帧,避免CPU空转。我见过有人不写这行,结果游戏画面加速到每秒几千帧,蛇条一秒钟穿过整个屏幕。帧率控制不只是优化,更是游戏逻辑稳定运行的前提。

3. 核心思路拆解:从零设计一个小游戏

3.1 为什么选贪吃蛇作为第一个项目

我当时选贪吃蛇不是随机的。贪吃蛇涵盖了几乎所有2D游戏的基础元素:

  • 玩家输入(方向键控制)
  • 对象移动(蛇头每帧按方向移动)
  • 碰撞检测(撞墙、撞自己、吃到食物)
  • 随机生成(食物)
  • 状态管理(运行中、游戏结束)
  • 分数系统(吃一个加一分)

这些元素同样适用于俄罗斯方块、飞机躲避、打砖块。可以说,你只要吃透贪吃蛇,再去做其他小游戏,就是在同一套框架里换皮、加规则。我在教一个朋友入门时也让他先做贪吃蛇,他两天后就能自己改出“吃一个食物蛇变短”的变种,这就是框架理解到位的表现。

3.2 先设计数据模型,再写界面

很多人写游戏上来就画窗口,然后走一步看一步。我吃过这个亏,结果代码写得一团乱。正确的做法是先想清楚游戏里有哪些“数据”。

贪吃蛇的数据模型在我看来只有三块:

  • 蛇:一个由多个坐标点组成的列表,snake = [(200, 200), (180, 200), (160, 200)]
  • 方向:当前移动方向,用向量表示,(20, 0)表示向右
  • 食物:一个坐标点,food = (400, 300)

这里的关键是:蛇的身体不需要单独画图形,只需要保存格子坐标。每一帧把列表里的所有坐标画成小方块,看起来就是一条蛇。我见过有新手把每一节身体都做成一个对象,然后管理对象生命周期,反而把自己绕晕了。

为什么用列表顺序很重要?因为蛇移动的本质是“头部伸长、尾部缩短”。如果吃到了食物,尾部就不缩短,整体长度加一。所以对我来说,列表就是蛇的全部。

3.3 像素坐标和网格坐标,别搞混

贪吃蛇最简单的实现是把整个游戏区域按20像素一格分成网格。此时会遇到两个坐标系:

  • 网格坐标:比如(5, 3)表示第5列第3行
  • 像素坐标:比如(100, 60)表示在窗口中的具体位置

转换公式特别简单:

pixel_x = grid_x * grid_size pixel_y = grid_y * grid_size

我当时犯过的错误是直接用像素坐标去移动,每次加20,看起来也能跑,但碰撞检测和食物对齐变得很麻烦。后来全部改成网格坐标,只在绘制时转成像素坐标,代码瞬间清爽了。

这个思路也适用于其他格子类游戏。先让逻辑运行在抽象坐标系里,最后才考虑“画出来”。这样游戏逻辑和界面显示解耦,想换皮肤或者改分辨率都容易。

4. 实操:一步步写一个能玩的贪吃蛇

4.1 初始化窗口和游戏常量

我建议把常量都放在开头,方便后期调整。下面是初始化部分:

import pygame import random pygame.init() GRID_SIZE = 20 CELL_SIZE = 20 WIDTH = 30 HEIGHT = 20 screen = pygame.display.set_mode((WIDTH * CELL_SIZE, HEIGHT * CELL_SIZE)) pygame.display.set_caption("贪吃蛇") clock = pygame.time.Clock() BLACK = (0, 0, 0) GREEN = (0, 200, 0) RED = (200, 0, 0) WHITE = (255, 255, 255)

这里WIDTH = 30表示横向有30格,HEIGHT = 20表示纵向有20格,窗口实际像素是600×400。把格子数量和格子大小分开定义,好处是你想改难度时,不需要改所有坐标。

初始化后还要定义蛇、方向和初始分数。我习惯用一个reset_game()函数来重置所有状态:

def reset_game(): global snake, direction, next_direction, food, score snake = [(WIDTH // 2, HEIGHT // 2)] direction = (1, 0) next_direction = direction food = spawn_food() score = 0

next_direction这个变量的存在是为了防止同一帧内多次按键导致方向冲突,后面会详细说。

4.2 控制蛇的移动与转向

蛇应该每隔一段时间动一次,而不是每帧都动。因为一帧的速度太快了。我设置一个计数器,每走5帧才让蛇移动一格,这样在60帧下有每秒12次移动,既流畅又不会太快。

MOVE_INTERVAL = 5 frame_count = 0 while running: for event in pygame.event.get(): if event.type == pygame.QUIT: running = False elif event.type == pygame.KEYDOWN: if event.key == pygame.K_UP and direction != (0, 1): next_direction = (0, -1) elif event.key == pygame.K_DOWN and direction != (0, -1): next_direction = (0, 1) elif event.key == pygame.K_LEFT and direction != (1, 0): next_direction = (-1, 0) elif event.key == pygame.K_RIGHT and direction != (-1, 0): next_direction = (1, 0) frame_count += 1 if frame_count >= MOVE_INTERVAL: direction = next_direction head = snake[0] new_head = (head[0] + direction[0], head[1] + direction[1]) snake.insert(0, new_head) snake.pop() frame_count = 0

注意方向防反逻辑:当蛇正在向右走时,不能直接按左键掉头,否则蛇头会立刻穿过身体。我这里用direction != (1, 0)来判断“不是向左走”,按左时next_direction = (-1, 0),方向反了就不更新。

还有一个细节:direction = next_direction必须在移动时执行,而不是在按键时执行。因为同一帧里你可能快速按了两个键,如果立即改变方向,第二次按键会基于第一次按键后的方向判断,逻辑就乱套了。我把它称为“延迟应用方向”,这是新手最容易忽略的点。

4.3 食物生成与吃食物

食物不能在蛇身上,也不能在墙外。我写了一个简单函数:

def spawn_food(): while True: pos = (random.randint(0, WIDTH - 1), random.randint(0, HEIGHT - 1)) if pos not in snake: return pos

这个写法看起来很笨,但胜在可靠。如果地图非常大,可以换成在空余格子列表中随机选一个,但早期游戏场景不过600格,while循环几乎不会超过几次。

吃食物的判定发生在蛇移动之后。我移动时先insert新头,再判断是否吃到食物。如果吃到了,就不执行pop(),这样蛇就多长了一节;同时分数加一,再生成新食物:

if new_head == food: score += 1 food = spawn_food() else: snake.pop()

这里有个关键顺序:insert是先把新头加进列表,如果食物正好在旧头移动到的新位置,说明蛇吃到了。很多人会把pop()放在insert之前,导致蛇永远不会增长。我当初调试了半天才发现,增长的本质就是“不收缩尾端”,所以必须把pop()放在吃到食物的分支里。

4.4 绘制画面和游戏结束判定

绘制部分需要把网格坐标转换为像素坐标,然后画方块。我用pygame.Rect来管理位置和大小:

def draw(): screen.fill(BLACK) for segment in snake: rect = pygame.Rect(segment[0] * CELL_SIZE, segment[1] * CELL_SIZE, CELL_SIZE, CELL_SIZE) pygame.draw.rect(screen, GREEN, rect) food_rect = pygame.Rect(food[0] * CELL_SIZE, food[1] * CELL_SIZE, CELL_SIZE, CELL_SIZE) pygame.draw.rect(screen, RED, food_rect) pygame.display.flip()

结束判定我放在移动逻辑之后:

head = snake[0] if head[0] < 0 or head[0] >= WIDTH or head[1] < 0 or head[1] >= HEIGHT: running = False if head in snake[1:]: running = False

撞墙和撞到自己都算结束。这里判断head in snake[1:]会随着蛇变长越来越慢,但对我们的小地图完全够用。如果要优化,可以用一个set记录身体位置,时刻检查,不过那是进阶话题。

结束之后,我通常会显示一个简单的结束画面,而不是直接黑屏退出。早期版本我直接running = False,结果玩家体验很差:没有任何提示就消失。后来我改成在结束画面里展示得分,再按任意键重新开始。这一步看起来很小,却让游戏真正“完整”了。

5. 常见问题与排查技巧实录

5.1 我踩过的5个坑

坑一:按键方向导致蛇反向自杀。我第一次写时没有判断“当前方向”,按了上和左,蛇直接掉头穿过身体。后来加了个next_direction延迟应用才解决。

坑二:食物生成在蛇身上。如果只用random.randint不管蛇的位置,食物可能出现蛇身上,玩家永远吃不到。一定用pos not in snake判断。

坑三:画面闪烁。出现闪烁通常是因为没有先填充背景,或者没有用flip()刷新。解决方法是先screen.fill(BLACK),再绘制所有对象,最后flip()。

坑四:游戏速度不稳定。没有clock.tick(60)时,游戏在性能好的电脑上快得离谱。加上帧率限制后,不管什么机器,手感一致。

坑五:退出时程序卡死。常见原因是没有pygame.quit(),或者销毁了窗口还继续绘制。在循环退出后加一个pygame.quit(),并确保running = False后不再执行逻辑。

5.2 问题排查速查表

现象可能原因检查方法
窗口一闪而过缺少主循环确认有while running
无法导入pygame装了多个Python用pip list检查当前环境
按键没反应事件处理未写在循环里确认pygame.event.get()在循环内
蛇不增长pop()写在分支外确认只有没吃到食物时才pop()
食物频繁刷在蛇身上缺少位置排除检查spawn_food()的判断
画面很卡每帧全图刷新考虑限制帧率或局部刷新
标题栏没有图标没设置帧率这不是问题,先忽略

排查的时候我建议多用临时输出。比如在移动后打印一下head坐标,看看是坐标计算错了还是绘制错了。打印可以快速缩小范围,比对着代码发呆高效十倍。

6. 让游戏更像“作品”:优化、打包与扩展

6.1 性能优化:从能用到够用再到好用

贪吃蛇这种规模,性能压力不大。但有几个习惯值得早点养成:

  • 能用pygame.Rect就少用pygame.draw.circle,矩形绘制速度更快。
  • 不要频繁创建Surface,尽量在初始化时建好,然后blit。
  • 食物和蛇的绘制区域如果很小,可以只刷新这些区域,但这点优化在你做复杂界面之前可以忽略。

我曾经试着在游戏中加入了粒子效果,每个粒子都是一个Surface,结果帧率掉到20。后来发现问题是每帧都重新Surface,改成初始化时批量创建才恢复60。对于第一个小游戏,能跑满60帧就足够,不要过早优化。

6.2 用PyInstaller打包成独立程序

做完游戏,想让朋友直接运行,不能让他装Python。我用PyInstaller打包了很多次,踩过最典型的坑是资源路径问题。

简单打包命令:

pip install pyinstaller pyinstaller -F -w main.py

-F是打包成单文件,-w是运行时不要终端窗口。如果游戏里引用了外部图片或音频,千万不能用相对路径直接写assets/xxx.png,因为PyInstaller会把程序解压到临时目录。我当时的解决方案是写一个函数,动态获取资源路径:

import sys import os def resource_path(relative_path): base_path = getattr(sys, "_MEIPASS", os.path.abspath(".")) return os.path.join(base_path, relative_path)

把这个函数用在加载图片和音频的地方,打包后就能正确找到资源。我一开始没这么做,发给朋友测试时背景图加载失败,黑屏了半天,后来查了官方文档才明白。

6.3 后续可以扩展的方向

游戏既然跑起来了,就别急着收工。我给了自己三个扩展方向,难度递增:

  1. 加障碍物:在棋盘上随机生成固定障碍,碰撞后结束,这是最简单的玩法扩展。只需要维护一个障碍物列表,碰撞判定和食物生成时排除这些坐标即可。
  2. 加速机制:每吃5个食物,移动间隔减少1,游戏越来越快。把MOVE_INTERVAL改成变量,分数变化时更新即可。
  3. 菜单和音效:加入开始界面和背景音效,让游戏有完整的“包装”。这一步会接触更多Pygame的模块,比如pygame.mixer和状态切换。

如果还想更深入,可以尝试做一个“多关卡壁障”。也就是每关结束后,重新生成一个更大的地图,同时保留之前的速度。这些扩展都会让你对状态机有更深的理解——游戏本质上是一堆状态的转移。

我个人的体会是,Pygame教我的不是某个库的API,而是“把抽象的逻辑变成可见画面”的能力。每次做完一个小游戏,我都会把逻辑模块拆出来复用,后来做第二个小游戏时,直接套用了这个贪吃蛇的代码骨架,只花了一个晚上就完成了新玩法。如果你也刚起步,建议别贪多,先把一个最简单的游戏做到能玩、能打包、能分享,你就会发现游戏开发并没有想象中那么难。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/14 3:49:28

MQTT服务(EMQX)安装实战:三套方案与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/10/14 3:48:56

SpringBoot机器人商城管理系统:核心模块设计与订单库存实战

说出来可能有点夸张&#xff0c;但前端展示万人同时在线的商品秒杀页面&#xff0c;后端管理端可能寒酸到只能录商品改库存。这套基于SpringBoot的机器人商城管理系统&#xff0c;本质上是把机器人这种高客单、参数复杂的“大件商品”从手动Excel表格管理&#xff0c;拉扯到正经…

作者头像 李华
网站建设 2026/10/14 3:48:29

DeepSeek Harness 插件开发实战:笔记与会话助手完整教程

最近在折腾 DeepSeek Harness 插件时&#xff0c;我做了两款非常贴近日常使用的小插件&#xff1a;一款用来沉淀问答过程中的笔记&#xff0c;一款用来增强会话体验。整体开发周期不长&#xff0c;发布后一周不到就攒了接近 4K Star&#xff0c;说明这种“解决真实痛点、能直接…

作者头像 李华
网站建设 2026/10/14 3:48:01

Spring AI Alibaba Graph:从ReAct循环到图编排的Java Agent工程化实践

这次我们来看一个 Java 后端与大模型结合的工程化方向&#xff1a;在 Spring Boot 生态里&#xff0c;用 Spring AI Alibaba 的 Graph 能力&#xff0c;把一个传统 ReAct/Loop 的 AI Agent&#xff0c;重构成显式、可编排、可观测的图结构工程。先直接说结论&#xff1a;这个方…

作者头像 李华
网站建设 2026/10/14 3:47:56

富文本编辑器截图粘贴实现:从剪贴板到图片上传的全链路解析

直接从截图粘贴这个动作说起。你在内部办公系统里写一篇材料&#xff0c;顺手CtrlV把一张截图糊进去&#xff0c;松手后图片就出现在正文里了。这个操作在很多办公场景下是天经地义的事&#xff0c;可一旦放到项目选型或者自研富文本工具的场景里&#xff0c;它往往变成第一个被…

作者头像 李华
网站建设 2026/10/14 3:46:48

如何定位慢查询

一、慢查询发生场景聚合查询多表查询表数据量过大查询深度分页查询表象&#xff1a; 页面加载过慢、接口压测响应时间过长&#xff08;超过 1s&#xff1b;实际中 500ms 以上就值得关注&#xff09;。二、如何定位1. 开启慢查询日志 —— 找到慢 SQL-- 查看是否开启 SHOW VARIA…

作者头像 李华